Does Spring @SubscribeMapping really subscribe the client to some topic?

爱⌒轻易说出口 提交于 2019-11-28 16:22:47

By default the return value from an @SubscribeMapping method is sent as a message directly back to the connected client and does not pass through the broker.

(emphasis mine)

Here the Spring Framework documentation is describing what happens with the response message, not the incoming SUBSCRIBE message.

So to answer your questions:

  • yes, the client is subscribed to the topic
  • yes, the clients subscribed to that topic will receive a message if you use that topic to send it
  • the message broker is in charge of managing subscriptions

More on subscription management

With the SimpleMessageBroker, the message broker implementation lives in your application instance. Subscription registrations are managed by the DefaultSubscriptionRegistry. When receiving messages, the SimpleBrokerMessageHandler handles SUBSCRIPTION messages and register subscriptions (see implementation here).

With a "real" message broker like RabbitMQ, you've configured a Stomp broker relay that forwards messages to the broker. In that case, the SUBSCRIBE messages are forwarded to the broker, in charge of managing subscriptions (see implementation here).

Update - more on STOMP message flow

If you take a look at the reference documentation on STOMP message flow, you'll see that:

  • Subscriptions to "/topic/greeting" pass through the "clientInboundChannel" and are forwarded to the broker
  • Greetings sent to "/app/greeting" pass through the "clientInboundChannel" and are forwarded to the GreetingController. The controller adds the current time, and the return value is passed through the "brokerChannel" as a message to "/topic/greeting" (destination is selected based on a convention but can be overridden via @SendTo).

So here, /topic/hello is a broker destination; messages sent there are directly forwarded to the broker. While /app/hello is an application destination, and is supposed to produce a message to be sent to /topic/hello, unless @SendTo says otherwise.

Now your updated question is somehow a different one, and without a more precise use case it's difficult to say which pattern is the best to solve this. Here are a few:

  • you want the client to be aware whenever something happens, asynchronously: SUBSCRIBE to a particular topic /topic/hello
  • you want to broadcast a message: send a message to a particular topic /topic/hello
  • you want to get immediate feedback for something, for example to initialize the state of your application: SUBSCRIBE to an application destination /app/hello with a Controller responding with a message right away
  • you want to send one or more messages to any application destination /app/hello: use a combination of @MessageMapping, @SendTo or a messaging template.

If you want a good example, then check out this chat application demonstrating a log of Spring websocket features with a real world use case.

Pauli

So having both:

  • Using a topic to handle subscription
  • Using @SubscribeMapping on that topic to deliver a connection-response

does not work as you experienced (as well as me).

The way to solve your situation (as I did mine) is:

  1. Remove the @SubscribeMapping - it only works with /app prefix
  2. Subscribe to the /topic just as you would naturally (w/o /app prefix)
  3. Implement an ApplicationListener

    1. If you want to directly reply to a single client use a user destination (see websocket-stomp-user-destination or you could also subscribe to a sub-path e.g. /topic/my-id-42 then you can send a message to this subtopic (I don't know about your exact use case, mine is that I have dedicated subscriptions and I iterate over them if I want to do a broadcast)

    2. Send a message in your onApplicationEvent method of the ApplicationListener as soon as you receive a StompCommand.SUBSCRIBE

Subscription Event Handler:

@Override
  public void onApplicationEvent(SessionSubscribeEvent event) {
      Message<byte[]> message = event.getMessage();
      StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
      StompCommand command = accessor.getCommand();
      if (command.equals(StompCommand.SUBSCRIBE)) {
          String sessionId = accessor.getSessionId();
          String stompSubscriptionId = accessor.getSubscriptionId();
          String destination = accessor.getDestination();
          // Handle subscription event here
          // e.g. send welcome message to *destination*
       }
  }

I faced with the same problem, and finally switched to solution when I subscribe to both /topic and /app on a client, buffering everything received on /topic handler until /app-bound one will download all the chat history, that is what @SubscribeMapping returns. Then I merge all recent chat entries with those received on a /topic - there could be duplicates in my case.

Another working approach was to declare

registry.enableSimpleBroker("/app", "/topic");
registry.setApplicationDestinationPrefixes("/app", "/topic");

Obviously, not perfect. But worked :)

Maybe it's not totally related, but when I was subscribing to 'app/test', it was impossible to receive messages sent to 'app/test'.

So I found that adding a broker was the problem (don't know why btw).

So here is my code before :

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app");
        config.enableSimpleBroker("/topic");
    }

After :

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.setApplicationDestinationPrefixes("/app");
        // problem line deleted
    }

Now when I subscribe to 'app/test', this is working :

    template.convertAndSend("/app/test", stringSample);

In my case, I don't need more.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!