Действительно ли 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 }));
...
То, что я хотел бы случиться:
- Когда клиент подписывается на
/topic/greetings
', методtry1
выполнен. - Когда клиент отправляет сообщение
/app/hello
', он должен получить приветственное сообщение, которое будет@SendTo
'/topic/greetings
".
Результаты:
Если клиент подписывается на
/topic/greetings
, методtry1
НЕВОЗМОЖНО поймать его.Когда клиент отправляет сообщение
/app/hello
",greeting
Метод был выполнен, и клиент получил приветственное сообщение. Таким образом, мы поняли, что он был подписан на/topic/greetings
'правильно.Но помните 1. не удалось. После некоторой попытки стало возможным, когда клиент подписался на
'/app/topic/greetings'
то есть с префиксом/app
(Это понятно по конфигурации).Сейчас 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 на эту тему для доставки ответа на соединение
не работает, как вы испытали (как и я).
Способ решить вашу ситуацию (как я сделал мой):
- Удалите @SubscribeMapping - он работает только с префиксом /app
- Подпишитесь на /topic, как вы это обычно делаете (без префикса app /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*
}
}
Привет, Мерт, хотя твой вопрос задан более 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);
В моем случае мне больше не нужно.