42

I am using Spring Websocket with STOMP, Simple Message Broker. In my @Controller I use method-level @SubscribeMapping, which should subscribe the client to a topic so that the client would receive the messages of that topic afterwards. Let say, the client subscribes to the topic "chat":

stompClient.subscribe('/app/chat', ...);

As the client subscribed to "/app/chat", instead of "/topic/chat", this subscription would go to the method which is mapped using @SubscribeMapping:

@SubscribeMapping("/chat")
public List getChatInit() {
    return Chat.getUsers();
}

Here is what Spring ref. says:

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. This is useful for implementing request-reply message interactions; for example, to fetch application data when the application UI is being initialized.

Okay, this was what I would want, but just partially!! Sending some init-data after subscribing, well. But what about subscribing? It seems to me that the thing what happened here is just a request-reply, like a service. The subscription is just consumed. Please clarify me if this is the case.

  • Did the client subscribe to some where, if the broker is not involved in this?
  • If later I want to send some message to "chat" subscriptors, would the client receive it? It doesnt seem so.
  • Who realizes subscriptions really? Broker? Or some one else?

If here the client is not being subscribed to any where, I wonder why we call this as "subscribe"; because the client receives just one message and not future messages.

EDIT:

To make sure that the subscription has been realized, what I tried is as following:

SERVER-side:

Configuration:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {

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

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/hello").withSockJS();
    }
}

Controller:

@Controller
public class GreetingController {

    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    public Greeting greeting(HelloMessage message) throws Exception {
        System.out.println("inside greeting");
        return new Greeting("Hello, " + message.getName() + "!");
    }

    @SubscribeMapping("/topic/greetings")
    public Greeting try1() {
        System.out.println("inside TRY 1");
        return new Greeting("Hello, " + "TRY 1" + "!");
    }
}

CLIENT-side:

...
    stompClient.subscribe('/topic/greetings', function(greeting){
                        console.log('RECEIVED !!!');
                    });
    stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name }));
...

What I would like to happen:

  1. When client subscribes to '/topic/greetings', the method try1 is executed.
  2. When the client sends msg to '/app/hello', it should receive the greetings msg which would be @SendTo '/topic/greetings'.

Results:

  1. If the client subscribes to /topic/greetings, the method try1 is UNABLE to catch it.

  2. When the client sends msg to '/app/hello', greeting method was executed, and the client received the greetings message. So we understood that it had been subscribed to '/topic/greetings' correctly.

  3. But remember 1. was failed. After some try, it has been possible when the client subscribed to '/app/topic/greetings', i.e. prefixed with /app (This is understandable by configuration).

  4. Now 1. is working, however this time 2. is failed: When the client sends msg to '/app/hello', yes, greeting method was executed, but the client did NOT receive the greetings message. (Because probably now the client was subscribed to the topic prefixed with '/app', which was unwanted.)

So, what I got is either 1 or 2 of what I would like, but not these 2 together.

  • How do I achieve this with this structure (configuring mapping paths correctly) ?
Mert Mertce
  • 1,494
  • 2
  • 18
  • 28

5 Answers5

23

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.

Brian Clozel
  • 46,620
  • 12
  • 129
  • 152
  • 1
    It seems to me that if you reach with '/app', it goes to app but not to broker; and with '/topic' it goes to broker but not to app. – Mert Mertce Mar 17 '15 at 14:50
  • I have discovered that when client subscribes to '/app/topic', it is subscribed exactly to '/app/topic', without removing '/app' prefix, which makes this subscribtion registry wasted, because SimpleBrokerMessageHandler never sends message to '/app' prefixed subscribtions. It is just filtered out with "checkDestinationPrefix" within SimpleBrokerMessageHandler – Mert Mertce Mar 17 '15 at 17:54
  • 1
    After edit: The only thing I wanted was to capture the subscribtion with @SubscribeMapping and at the same time the client to be subscribed to the topic without "/app" prefix; just like in the MessageMapping. But in SubscribeMapping this is not the case. Subscribe message goes to Broker with "/app" prefix. So, the client is not subscribed to the topic. In this case Subscribtion turns into a request-reply, nothing more. This is not a subscription in real. – Mert Mertce Mar 17 '15 at 22:31
  • For example, in that chat example, the broker would never be able to send messages to "/chat.participants" subscriptors, because those clients would be subscribed to "app/chat.particicipants". – Mert Mertce Mar 17 '15 at 22:34
  • Yes, as stated in the documentation. SubscribeMapping here maps `SUBSCRIBE` messages to a Controller action. – Brian Clozel Mar 18 '15 at 09:14
  • 1
    Yes, as in the doc, but here what I was questioning is the intention. The client could just subscribe to '/topic', but he subscribes to '/app/topic'. What is the intention here? Mapping the subscribtion to the Controller, and don't forget, ALSO subscribe to '/topic'. But as a result the client is being subscribed to '/app/topic', which was not that we intended to do. Brian, don't you see a distortion here? Do i miss some thing? – Mert Mertce Mar 18 '15 at 18:26
  • I'm also trying to understand that fully. Yes, `SubscribeMapping`s map `SUBSCRIBE` to a controller action, but after that controller action executed one time, is that subscription used for anything ever again? – rainerfrey Feb 09 '17 at 19:41
  • 2
    Intuitively, one might expect that in addition to returning the value of the controller action, a subscription will be created automatically to `/topic/chat.participants`, similar to the `MessageMapping` forwarding to the corresponding broker destination. But I don't see this happening, AFAICT, the client has to subscribe to `/topic/chat.participants` separately. – rainerfrey Feb 09 '17 at 19:45
16

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*
       }
  }
mpromonet
  • 8,785
  • 40
  • 48
  • 80
Pauli
  • 461
  • 4
  • 10
  • 2
    Thank you for your intent to help, especially after some time. Your answer would be useful for those who read later. So +1. In my case, however, as I remember, onApplicationEvent was running before actual successful subscribing, and returning an answer to subscriptor there was feinting the subscriptor, ie, answering as if he was successfully subscribed, where this is not true yet. I saw that default Spring Stomp Broker has important limitations, and I decided to use another "real" broker once I develop enough my project. – Mert Mertce Jul 22 '15 at 07:33
6

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 :)

Stanislav Mamontov
  • 1,574
  • 13
  • 21
  • This is working well, but what is the drawback ? is this configuration impact /topic notification ? – mpromonet Mar 11 '18 at 12:30
  • I don't think you need the `"/app"` in `enableSimpleBroker()`. I had the same problem as the OP, and solved it by just adding `"/topic"` to the application destinations. – Joffrey May 06 '18 at 19:47
  • Despite this solution is not perfect, it helped me to understand that method "setApplicationDestinationPrefixes" marks routes that will go through controllers of your application (marked with MessageMapping and SubscribeMapping). Otherwise, it will try to go via a simple broker and do not call your methods. – Viktor Molokanov Jan 28 '19 at 11:16
4

Hi Mert though your question is ask over 4 years ago but I'll still try to answer it since I scratched my head on the same problem recently and finally solved it.

The key part here is @SubscribeMapping is a one-time request-response exchange, therefore the try1() method in your controller will be triggered only once right after client codes runs

stompClient.subscribe('/topic/greetings', callback)

after that there's no way to trigger try1() by stompClient.send(...)

Another problem here is the controller is part of the application message handler, which receives destination with prefix /app ripped, so in order to reach @SubscribeMapping("/topic/greetings") you actually have to write client code like this

stompClient.subscribe('/app/topic/greetings', callback)

since conventionally topic is mapped with brokers to avoid ambiguity I recommend to modify your code to

@SubscribeMapping("/greetings")

stompClient.subscribe('/app/greetings', callback)

and now console.log('RECEIVED !!!') should work.

The official doc also recommends use case scenario of @SubscribeMapping on initial UI rendering.

When is this useful? Assume that the broker is mapped to /topic and /queue, while application controllers are mapped to /app. In this setup, the broker stores all subscriptions to /topic and /queue that are intended for repeated broadcasts, and there is no need for the application to get involved. A client could also also subscribe to some /app destination, and a controller could return a value in response to that subscription without involving the broker without storing or using the subscription again (effectively a one-time request-reply exchange). One use case for this is populating a UI with initial data on startup.

mzoz
  • 1,036
  • 1
  • 10
  • 21
2

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.

FloFlow
  • 101
  • 7