Действительно ли Spring @SubscribeMapping подписывает клиента на какую-то тему?

Я использую Spring Websocket с STOMP, Simple Message Broker. В моем @Controller Я использую метод уровня @SubscribeMapping, который должен подписать клиента на тему, чтобы клиент мог получать сообщения по этой теме впоследствии. Допустим, клиент подписывается на тему "чат":

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

Поскольку клиент подписался на "/ app / chat", а не на "/ topic / chat", эта подписка будет идти к методу, который отображается с помощью @SubscribeMapping:

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

Вот то, что весна исх. говорит:

По умолчанию возвращаемое значение из метода @SubscribeMapping отправляется в виде сообщения непосредственно обратно подключенному клиенту и не проходит через посредник. Это полезно для реализации взаимодействия между запросом и ответом; например, для извлечения данных приложения, когда пользовательский интерфейс приложения инициализируется.

Хорошо, это было то, что я хотел бы, но только частично! Отправка некоторых init-данных после подписки, хорошо. Но как насчет подписки? Мне кажется, что то, что здесь произошло, это просто запрос-ответ, как услуга. Подписка только что потребляется. Пожалуйста, уточните мне, если это так.

  • Подписывался ли клиент на что-то, где, если брокер не вовлечен в это?
  • Если позже я захочу отправить какое-нибудь сообщение подписчикам чата, получит ли его клиент? Это не так.
  • Кто реализует подписки на самом деле? Маклер? Или кто-то еще?

Если здесь клиент не подписан ни на что, мне интересно, почему мы называем это "подписаться"; потому что клиент получает только одно сообщение, а не будущие сообщения.

РЕДАКТИРОВАТЬ:

Чтобы убедиться, что подписка была реализована, я попытался сделать следующее:

Серверный:

Конфигурация:

@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
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" + "!");
    }
}

Сторона клиента:

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

То, что я хотел бы случиться:

  1. Когда клиент подписывается на/topic/greetings', метод try1 выполнен.
  2. Когда клиент отправляет сообщение/app/hello', он должен получить приветственное сообщение, которое будет @SendTo '/topic/greetings".

Результаты:

  1. Если клиент подписывается на /topic/greetings, метод try1 НЕВОЗМОЖНО поймать его.

  2. Когда клиент отправляет сообщение/app/hello", greeting Метод был выполнен, и клиент получил приветственное сообщение. Таким образом, мы поняли, что он был подписан на/topic/greetings'правильно.

  3. Но помните 1. не удалось. После некоторой попытки стало возможным, когда клиент подписался на '/app/topic/greetings'то есть с префиксом /app (Это понятно по конфигурации).

  4. Сейчас 1. работает, однако на этот раз 2. не удалось: когда клиент отправляет сообщение "/app/hello', да, greeting метод был выполнен, но клиент НЕ получил сообщение приветствия. (Потому что, вероятно, теперь клиент был подписан на тему с префиксом '/app', который был нежелательным.)

Итак, я получил 1 или 2 из того, что хотел бы, но не эти 2 вместе.

  • Как мне добиться этого с этой структурой (правильная настройка путей отображения)?

3 ответа

По умолчанию возвращаемое значение из метода @SubscribeMapping отправляется в виде сообщения непосредственно обратно подключенному клиенту и не проходит через посредник.

(акцент мой)

Здесь документация Spring Framework описывает, что происходит с ответным сообщением, а не с входящим SUBSCRIBE сообщение.

Итак, чтобы ответить на ваши вопросы:

  • да, клиент подписан на тему
  • да, клиенты, подписавшиеся на эту тему, получат сообщение, если вы используете эту тему для ее отправки
  • брокер сообщений отвечает за управление подписками

Подробнее об управлении подписками

С SimpleMessageBrokerреализация брокера сообщений живет в вашем экземпляре приложения. Регистрация подписки управляется DefaultSubscriptionRegistry, При получении сообщений SimpleBrokerMessageHandler ручки SUBSCRIPTION сообщения и зарегистрировать подписку ( см. реализацию здесь).

С таким "реальным" брокером сообщений, как RabbitMQ, вы настроили ретранслятор Stomp Broker, который пересылает сообщения брокеру. В этом случае SUBSCRIBE сообщения пересылаются брокеру, отвечающему за управление подписками ( см. реализацию здесь).

Обновление - больше о потоке сообщений STOMP

Если вы посмотрите справочную документацию по потоку сообщений STOMP, вы увидите, что:

  • Подписки на "/topic/ приветствие" проходят через "clientInboundChannel" и передаются посреднику
  • Приветствия, отправленные в "/app/ приветствие", проходят через "clientInboundChannel" и передаются в GreetingController. Контроллер добавляет текущее время, а возвращаемое значение передается через "brokerChannel" в качестве сообщения в "/topic/ приветствие" (место назначения выбирается в соответствии с соглашением, но может быть переопределено через @SendTo).

Так вот, /topic/hello является брокером назначения; отправленные туда сообщения напрямую пересылаются брокеру. В то время как /app/hello является пунктом назначения приложения и должен выдавать сообщение для отправки /topic/helloесли @SendTo говорит иначе.

Теперь ваш обновленный вопрос как-то другой, и без более точного варианта использования трудно сказать, какой шаблон лучше всего решить. Вот несколько из них:

  • Вы хотите, чтобы клиент был в курсе, когда что-то происходит, асинхронно: ПОДПИСАТЬСЯ на конкретную тему /topic/hello
  • Вы хотите передать сообщение: отправить сообщение на определенную тему /topic/hello
  • Вы хотите получить немедленную обратную связь для чего-либо, например, чтобы инициализировать состояние вашего приложения: ПОДПИСАТЬСЯ на место назначения приложения /app/hello с контроллером, отвечающим сообщением сразу
  • Вы хотите отправить одно или несколько сообщений в любое место назначения приложения /app/hello: используйте комбинацию @MessageMapping, @SendTo или шаблон сообщения.

Если вы хотите хороший пример, то посмотрите это приложение чата, демонстрирующее журнал функций веб-сокета Spring с реальным примером использования.

Итак, имея оба:

  • Использование темы для обработки подписки
  • Использование @SubscribeMapping на эту тему для доставки ответа на соединение

не работает, как вы испытали (как и я).

Способ решить вашу ситуацию (как я сделал мой):

  1. Удалите @SubscribeMapping - он работает только с префиксом /app
  2. Подпишитесь на /topic, как вы это обычно делаете (без префикса app /app)
  3. Реализовать ApplicationListener

    1. Если вы хотите напрямую ответить одному клиенту, используйте назначение пользователя (см. Websocket-stomp-user-destination или вы также можете подписаться на подпуть, например /topic/my-id-42, тогда вы можете отправить сообщение этому подтема (я не знаю о вашем конкретном случае использования, у меня есть выделенные подписки, и я перебираю их, если хочу сделать трансляцию)

    2. Отправьте сообщение в методе onApplicationEvent объекта ApplicationListener, как только вы получите StompCommand.SUBSCRIBE.

Обработчик событий подписки:

@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*
       }
  }

Привет, Мерт, хотя твой вопрос задан более 4 лет назад, но я все равно попытаюсь на него ответить, так как недавно почесал голову над той же проблемой и, наконец, решил ее.

Ключевой частью здесь является @SubscribeMappingэто одноразовый обмен запрос-ответ, поэтомуtry1() метод в вашем контроллере будет запущен только один раз сразу после запуска клиентских кодов

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

после этого нет возможности вызвать try1() по stompClient.send(...)

Другая проблема заключается в том, что контроллер является частью обработчика сообщений приложения, который получает адрес назначения с префиксом /app разорвал, чтобы добраться до @SubscribeMapping("/topic/greetings") вам действительно нужно написать такой клиентский код

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

так как обычно topic сопоставлен с брокерами, чтобы избежать двусмысленности, я рекомендую изменить ваш код, чтобы

@SubscribeMapping("/greetings")

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

и сейчас console.log('RECEIVED !!!') должно сработать.

Официальный документ также рекомендует использовать случай сценарий@SubscribeMapping при первоначальном рендеринге пользовательского интерфейса.

Когда это полезно? Предположим, что брокер сопоставлен с /topic и /queue, а контроллеры приложений сопоставлены с /app. В этой настройке брокер хранит все подписки на /topic и /queue, которые предназначены для повторных широковещательных рассылок, и приложению не нужно вмешиваться. Клиент также может подписаться на какое-то место назначения / app, и контроллер может вернуть значение в ответ на эту подписку без участия брокера без повторного сохранения или использования подписки (фактически, обмен одноразовый запрос-ответ). Один из вариантов использования для этого - заполнение пользовательского интерфейса начальными данными при запуске.

Я столкнулся с той же проблемой и, наконец, переключился на решение, когда подписался на оба /topic а также /app на клиенте, буферизация всего полученного на /topic обработчик до /appпривязанный загрузит всю историю чата, вот что @SubscribeMapping возвращается. Затем я объединяю все последние записи чата с полученными на /topic - могут быть дубликаты в моем случае.

Другим подходом к работе было объявить

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

Очевидно, не идеально. Но сработало:)

Может быть, это не совсем связано, но когда я подписывался на "app/test", было невозможно получать сообщения, отправленные "app/test".

Таким образом, я обнаружил, что добавление брокера было проблемой (не знаю почему, кстати).

Итак, вот мой код раньше:

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

После:

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

Теперь, когда я подписываюсь на "app/test", это работает:

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

В моем случае мне больше не нужно.

Другие вопросы по тегам