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 вместе.
- Как достичь этого с помощью этой структуры (правильно настроив пути отображения)?
Ответы
Ответ 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 на этом
тема для доставки соединения-ответа
не работает, как вы испытали (как и я).
Способ решить вашу ситуацию (как я сделал мою):
- Удалите @SubscribeMapping - он работает только с префиксом /app
- Подпишитесь на /topic, как вы это обычно делаете (без префикса /app)
Реализуйте ApplicationListener
Если вы хотите напрямую ответить одному клиенту, используйте назначение пользователя (см. websocket-stomp-user-destination
или вы также можете подписаться на подпуть, например, /topic/my-id-42, тогда вы можете отправить сообщение в эту подтему (я не знаю о вашем конкретном случае использования, у меня есть выделенные подписки, и я перебираю их, если хочу сделать трансляцию)
Отправьте сообщение в методе 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" );