Подтвердить что ты не робот

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

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

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

Когда клиент подписался на "/приложение/чат", вместо "/topic/chat" , эта подписка перейдет к методу, который отображается с использованием @SubscribeMapping:

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

Вот что Spring ref. говорит:

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

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

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

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

EDIT:

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

Серверный:

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

@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 }));
...

Что бы я хотел:

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

Результаты:

  • Если клиент подписывается на /topic/greetings, метод try1 является UNABLE, чтобы его поймать.

  • Когда клиент отправляет msg в '/app/hello', был выполнен метод greeting, и клиент получил сообщение приветствия. Таким образом, мы поняли, что он был правильно подписан <<28 > .

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

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

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

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

Ответ 1

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

(акцент мой)

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

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

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

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

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

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

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

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

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

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

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

  • вы хотите, чтобы клиент знал, когда что-то происходит, асинхронно: SUBSCRIBE к определенной теме /topic/hello
  • вы хотите передать сообщение: отправить сообщение в определенную тему /topic/hello
  • вы хотите получить немедленную обратную связь для чего-то, например, для инициализации состояния вашего приложения: SUBSCRIBE к приложению приложения /app/hello с контроллером, отвечающим сообщением сразу.
  • вы хотите отправить одно или несколько сообщений любому приложению приложения /app/hello: используйте комбинацию @MessageMapping, @SendTo или шаблон обмена сообщениями.

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

Ответ 2

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

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

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

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

  1. Удалите @SubscribeMapping - он работает только с префиксом /app
  2. Подпишитесь на /topic, как вы это обычно делаете (без префикса /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*
       }
  }

Ответ 3

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

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

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

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

Ответ 4

Может быть, это не совсем связано, но когда я подписывался на "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);

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

Ответ 5

Привет, Мерт, хотя твой вопрос задан более 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, предназначенные для повторных широковещательных рассылок, и приложение не нуждается в участии. Клиент также может подписаться на некоторый пункт назначения/приложение, и контроллер может вернуть значение в ответ на эту подписку, не вовлекая посредника, не сохраняя или не используя подписку снова (фактически единовременный обмен запросом-ответом). Одним из вариантов использования этого является заполнение пользовательского интерфейса начальными данными при запуске.

Ответ 6

Я сталкиваюсь с какой-то проблемой, в моем случае я удаляю application-destination-prefix = "/app", 1 и 2 работают отлично, поэтому вы можете попробовать удалить config.setApplicationDestinationPrefixes( "/app" );