当前位置: 首页 > 知识库问答 >
问题:

如何在非反应性Spring EventListener和反应性Flux之间进行桥接

米子轩
2023-03-14

通过调用Flux.push直接创建通量并在push的lambada表达式中使用接收器,与使用DirectProcessor提供的接收器有什么区别?

在一个Flux只发出几个事件的最小示例中,我可以

Flux.<String>push(emitter -> {
   emitter.next("One");
   emitter.next("Two");
   emitter.complete();
 });

与使用DirectProcessor

var emitter = DirectProcessor.<String>create().sink();
emitter.next("One");
emitter.next("Two");
emitter.complete();

为了澄清一下:我知道我可以在这里使用Flux.Just,但我的用例实际上是在Spring的@EventListener和Spring WebFlux之间建立一个桥梁,在这里我想为每个传入的特定资源SSE请求创建一个Flux,然后将事件发布到这个Flux。

另一方面,它有不能处理背压的局限性。因此,如果您通过DirectProcessor推送N个元素,但至少有一个订阅服务器请求的元素少于N,DirectProcessor就会向它的订阅服务器发出IllegalStateException信号。

那是什么意思?

[编辑:]在问题的早期版本中,我使用的是flux.generate()而不是flux.push(),这显然是错误的,因为generate最多只能创建一个事件。

html" target="_blank">public enum ProcessState {
  CREATED(false),
  VERIFIED(false),
  AUTHORIZATION_PENDING(false),
  AUTHORIZED(false),
  ACTIVE(true);

  @Getter
  private final boolean terminalState;

  ProcessState(boolean terminalState) {
    this.terminalState = terminalState;
  }

}

每当更改任何用户的状态时,非反应业务逻辑将发布stateChangedevents:

@Data
@RequiredArgsConstructor
public class StateChangedEvent {
  private final UUID userId;
  private final ProcessState newState;
}

这就是我最初的问题的来源。我如何建立一个桥梁,将这个领域的事件转换成一个通量流?我的要求:

  • 新客户端注册后,应立即推送进程的当前状态
  • 只要达到“终端”登机状态,通量流就应终止。
@Component
@RequiredArgsConstructor
class EventBridge {

  @RequiredArgsConstructor(access = PRIVATE)
  private static class Subscriber {
    private final UUID userId;
    private final FluxSink<ProcessState> sink;
    private boolean eventEmitted;
  }

  private final UserRepository repository;
  private final Map<UUID, Subscriber> subscribers = new ConcurrentHashMap<>();

  @EventListener
  void stateChanged(StateChangedEvent event) {
    notifySubscribers(event);
  }

  Flux<ProcessState> register(UUID userId) {
    return Flux.push(emitter -> addSubscriber(userId, emitter));
  }

  private Subscriber addSubscriber(UUID userId, FluxSink<ProcessState> sink) {
    var subscriptionId = randomUUID();
    var subscriber = new Subscriber(userId, sink);
    subscribers.put(subscriptionId, subscriber);
    sink
      .onRequest(n -> poll(subscriber))
      .onDispose(() -> removeSubscriber(subscriptionId));
    return subscriber;
  }

  private void poll(Subscriber subscriber) {
    emit(subscriber, loadCurrentState(subscriber), true);
  }

  private ProcessState loadCurrentState(Subscriber subscriber) {
    return repository.findById(subscriber.userId).getProcessState();
  }

  private void removeSubscriber(UUID subscriptionId) {
    subscribers.remove(subscriptionId);
  }

  private void notifySubscribers(StateChangedEvent event) {
    subscribers.values().stream()
      .filter(subscriber -> subscriber.userId.equals(event.getUserId()))
      .forEach(subscriber -> emit(subscriber, event.getNewState(), false));
  }

  private void emit(Subscriber subscriber, ProcessState processState, boolean onlyIfFirst) {
    synchronized (subscriber) {
      if (onlyIfFirst && subscriber.eventEmitted) {
        return;
      }
      subscriber.sink.next(processState);
      if (processState.isTerminalState()) {
        subscriber.sink.complete();
      }
      subscriber.eventEmitted = true;
    }
  }

}
@RestController
@RequiredArgsConstructor
class UserController {

  private final EventBridge eventBridge;

  @GetMapping(value = "/{userId}", produces = TEXT_EVENT_STREAM_VALUE)
  Flux<ServerSentEvent<ProcessState>> readAsStream(@PathVariable UUID userId) {
    return eventBridge.register(userId).map(response -> ServerSentEvent.builder((ProcessState) response).build());
  }

}

我的bridge代码中有几个问题我无法理解:

>

  • 我真的必须同步我的subscriber实例,以避免从poll写入初始状态的过时事件吗?如果不这样做,就会发生StateChange事件到达并在从存储库读取当前状态之前发布,然后将其按顺序推出。当然,在没有synchronized关键字的情况下,必须有一种更优雅的Flux式方法来处理这个问题。

    我们已经排除了flux.generate,它似乎可以使用flux.pushflux.create将生成更多的SSE事件?为什么?恐怕,我不明白这三者之间的区别。

    与其使用flux上的静态方法,我应该使用directprocessor或这里的任何其他处理器吗?我对整个Reactor栈是新的,而且Spring Reactor文档对我来说太模糊了,TBH。再问一遍:区别在哪里?那我上面提到的关于背压的评论呢?

  • 共有1个答案

    暨弘毅
    2023-03-14

    所以,如果我能正确理解你想做的事情,我认为你的解决方案可以大大简化。

    @Component
    public class EventBridge {
    
        private final UserRepository repository;
        private final ReplayProcessor<StateChangedEvent> processor;
        private final FluxSink<StateChangedEvent> sink;
    
    
        EventBridge(UserRepository repository){
            this.repository= repository;
            //Replays events from last 100S for every new subscriber
            this.processor = ReplayProcessor.createTimeout(Duration.ofSeconds(100L));
            //Sink provides thread safe next,complete and error for subscribers
            this.sink = processor.sink();
        }
    
        public void changeState(StateChangedEvent event) {
            //Literally just pass event into sink, calls onNext on subscribers
            sink.next(event);
        }
    
        public Flux<ProcessState> streamProcessStateForUser(UUID uuid){
            return
                    //Search repository first, this isn't great since it blocks until 
                    //the repo returns, although that seems to be what you want
                    //Also I added an Unknown to ProcessState, since it's better than 
                    //it being null. 
                    //Also you should probably return optional from repo. 
                Flux.concat(
                        Flux.just(
                                userRepo.findById(uuid).map(User::getProcessState).orElse(ProcessState.UNKNOWN)
                        ),
                        processor
                                //Check the uuid matches the event
                                .filter(stateChangedEvent -> stateChangedEvent.getUserId().equals(uuid))
                                //Time out after 100 seconds, not needed but may be useful for you
                                .take(Duration.ofSeconds(100L))
                                //Complete flux when at terminal state
                                .takeUntil(stateChangedEvent -> stateChangedEvent.getNewState().isTerminalState())
                                //Convert to ProcessState from StateChangedEvent
                                .map(StateChangedEvent::getNewState)
                );
        }
    
    }
    

    应该能保持其他一切不变。

     类似资料:
    • 我正在尝试用quarkus、hibernate和postgres开发一个多租户应用程序。 Hibernate Responsive通过让implement支持多租户: http://hibernate.org/reactive/documentation/1.0/reference/html_single/#_custom_connection_management_and_multitenanc

    • 我试图理解反应流和反应流之间的区别,特别是在RxJava的上下文中? 我所能理解的最多的是,反应流在规范中有一些背压的概念,但在RxJava/Reactive中已经存在了请求(n)接口。 我不介意ELI5的回答。

    • 有人能解释一下 和 我不知道确切的意思。

    • 导航异步、非阻塞和反应性是一项非常有用的工作。。。给定2个非阻塞、无功、垂直。基于x/quarkus的微服务A和B,其中约束条件是A必须通过http与B通信。如果我想让服务保持被动(非阻塞): 我应该使用Vertex web客户端吗?文档说明它是一个异步客户机,但我假设它是vert。基于x的它也是非阻塞的吗?(我在这里区分异步和非阻塞) 任何帮助都会很好。谢谢

    • 我想实现一个WebFilter,它读取传入请求的特定头,用这个头的值调用一个GET请求到另一个反应RESTendpoint,然后用GET响应的值修改原始请求。 我想在WebFilter中实现这一点,因为我不想将此函数调用添加到我的中的每个函数中。 目前我有这个: 这显然现在不起作用。我的网络客户端正在返回单声道,所以我不能在我的调用中直接使用它,因为这需要一个字符串。出于显而易见的原因,我也不能真

    • 我当前的尝试在下面,但这只打印一个值。我希望采样定期继续,以便每秒打印一个值。如何做到这一点?