#java #spring #spring-boot #websocket
#java #весна #весенняя загрузка #websocket
Вопрос:
Я внедряю SimpMessagingTemplate
в некоторые из моих @Component
классов в моем приложении Springboot, но simpMessagingTemplate.convertAndSend(dest, payload)
отправляет только из @Controller
класса, в котором обрабатываются конечные точки websocket.
Контроллер
@MessageMapping("/gpio_ws")
@SendTo("/topic/gpio")
public GPIOMessage sendGPIOMessage(GPIOMessage message) {
log.info("Sending WS message: " message);
simpMessagingTemplate.convertAndSend("/topic/gpio", message);
}
У меня есть некоторый Javascript во внешнем интерфейсе с Stomp и SockJS, который подключается и отправляет сообщения через эту конечную точку без проблем.
Пример использования в другом классе
// constructor injected, it won't allow field injection here
private final SimpMessagingTemplate template;
@Autowired
public PiGPIO(SimpMessagingTemplate template) {
this.template = template;
}
// in a method
Arrays.stream(piFace.getInputPins())
.forEach(inputPin -> inputPin.addListener(new GpioPinListenerDigital() {
@Override
public void handleGpioPinDigitalStateChangeEvent(GpioPinDigitalStateChangeEvent
event) {
log.info("PiFace input had an event - PIN: " event.getPin().getName()
" STATE: " event.getState().getName());
template.convertAndSend("/topic/gpio", new
GPIOMessage(event.getPin().getName(),
event.getState().getName().equals("LOW INPUT") ? 1 : 0));
}
}));
На днях у меня это работало, и теперь я в полной растерянности.
Я проверил компонент, щелкнув маленький значок в IntelliJ, и все экземпляры SimpMessagingTemplate
, похоже, указывают на один и тот же компонент.
Может SimpMessagingTemplate
ли работать вне класса, в котором объявлена конечная точка? Почему бы и нет?
Редактировать: вот конфигурация websocket
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic");
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/gpio_ws");
registry.addEndpoint("/gpio_ws").withSockJS();
registry.addEndpoint("/track"); // for another SockJS, which works
registry.addEndpoint("/track").withSockJS();
}
}
Фрагмент Javascript. Во время составления кода websocket я использовал JS для получения и отправки, но для этой конкретной проблемы мне просто нужно, чтобы он получал. Хотя на данный момент я оставил функцию отправки / получения, чтобы я мог проверить, что WS все еще работает.
function connect() {
const sock_track = new SockJS('/track');
stomp_track = Stomp.over(sock_track);
stomp_track.connect({}, function (frame) {
console.log('stomp_track connected: ' frame)
})
const sock_gpio = new SockJS('/gpio_ws');
stomp_gpio = Stomp.over(sock_gpio);
stomp_gpio.connect({}, function(frame) {
console.log('stomp_gpio connected: ' frame);
stomp_gpio.subscribe('/topic/gpio', function(messageOutput) {
parseMessage(JSON.parse(messageOutput.body));
console.log(messageOutput.body)
});
});
}
function sendMessage() {
const pin = document.getElementById('pin').value;
const state = document.getElementById('state').value;
console.log("state: " state " pin: " pin)
stomp_gpio.send("/app/gpio_ws", {},
JSON.stringify({'pin':pin, 'state':state}));
}
function trackOn() {
stomp_track.send("/app/track", {}, true)
}
function trackOff() {
stomp_track.send("/app/track", {}, false)
}
Edit:
The class where SimpMessagingTemplate
template.convertAndSend()
previously worked, before I had to refactor as I had a circular dependency (I injected PiGPIO here, and GPIOService into PiGPIO).
So previously, GPIOService was injected to PiGPIO and within PiGPIO the method sendEvent()
was called and this successfully sent a message to the JS client.
@Service
public class GPIOService {
private static final Logger log = LoggerFactory.getLogger(GPIOService.class);
private final SimpMessagingTemplate template;
private final PiGPIO piGPIO;
@Autowired
public GPIOService(SimpMessagingTemplate template, PiGPIO piGPIO) {
this.template = template;
this.piGPIO = piGPIO;
}
/** Sends a message to the websocket on /gpio endpoint.
* @param message is a model of the PIN state, 1 being high, 0 low */
public void sendEvent(GPIOMessage message) {
log.info("GPIOMessage sent to /topic/gpio");
this.template.convertAndSend("/topic/gpio", message);
}
public void trackPower(boolean isPowered) {
if (isPowered) {
piGPIO.turnOnTrack();
} else {
piGPIO.turnOffTrack();
}
}
}
Редактировать:
Добавлено @PostConstruct
в два класса, которые я пытаюсь использовать SimpMessagingTemplate
.
@PostConstruct
void onMade() {
log.info("Bean initialised");
}
Запуск приложения, результат, похоже, показывает, что оба класса найдены и инициализированы Spring?
2020-11-16 10:34:16.153 INFO 22346 --- [ main] io.github.siaust.slotcar.service.PiGPIO : Bean initialised
2020-11-16 10:34:16.168 INFO 22346 --- [ main] i.g.siaust.slotcar.service.GPIOService : Bean initialised
SimpleMessageBrokerHandler
кажется, у него нулевые назначения, я не уверен, является ли это проблемой или при запуске приложения это меняется.
2020-11-16 10:34:24.448 INFO 22346 --- [ main] o.s.m.s.b.SimpleBrokerMessageHandler : BrokerAvailabilityEvent[available=true, SimpleBrokerMessageHandler [DefaultSubscriptionRegistry[cache[0 destination(s)], registry[0 sessions]]]]
Редактировать:
Я добавил еще несколько вызовов журнала, чтобы посмотреть, что происходит с SimpMessagingTemplate
классом после его инициализации.
log.info(template.getDefaultDestination());
log.info(template.getUserDestinationPrefix());
log.info(template.getMessageChannel().toString());
Результат
2020-11-16 11:37:17.522 INFO 23925 --- [ main] io.github.siaust.slotcar.service.PiGPIO : null
2020-11-16 11:37:17.523 INFO 23925 --- [ main] io.github.siaust.slotcar.service.PiGPIO : /user/
2020-11-16 11:37:17.524 INFO 23925 --- [ main] io.github.siaust.slotcar.service.PiGPIO : ExecutorSubscribableChannel[brokerChannel]
Я просто предполагаю здесь, но назначения по умолчанию не должно быть null
? У меня проблема с конфигурацией, или я должен установить назначение по умолчанию?
Комментарии:
1. Может ли это быть проблемой назначения сеанса, которая в итоге была опущена? Я имею в виду, что шаблон обмена сообщениями требует сеанса ws для выполнения операций, и я не вижу ничего
@SubscribeMapping
подобного во втором примере.2. Пожалуйста, смотрите Мою правку для получения дополнительной информации. Должен ли я аннотировать метод, для которого я использую
SimpMessagingTemplate
экземплярconvertAndSend()
,@SubscribeMapping
и подписаться на требуемую мне конечную точку?3. Нет, вы вообще не привязаны к @SubscribeMapping, но я полагаю, что эта проблема находится где-то в другом месте. Взглянув на ваш код свежим взглядом, я должен задаться вопросом, почему ваш код структурирован так, как контроллер вызывает службу, а не наоборот? Если для этого нет причин, то наиболее удобный способ — принять это поведение и преобразовать код в рабочий шаблон
4. Моя основная причина заключается в том, что класс service не может знать, когда отправлять сообщение, поскольку слушатели создаются в другом классе. Это явно плохой дизайн, и я был в процессе его выяснения. Вероятно, мне придется вернуться к тому, как это было, работать, если смогу, и выяснить, как заставить слушателей уведомлять о событии в классе обслуживания, возможно, с шаблоном наблюдателя, я не уверен.
5. Возможно, вы также можете использовать некоторых
@EventListener
издателей событий , чтобы заставить это работать должным образом. При необходимости я могу прикрепить рабочую конфигурацию.
Ответ №1:
Не совсем уверен, что вы делаете с отсутствующей конфигурацией и т.д. Не уверен, что вы подразумеваете под битом «проверки компонента».
Вот моя настройка для базового канала «уведомления».
- Клиент попадает в конечную точку STOMP
/swns/start
, чтобы открыть соединение STOMP. - Конечная
/swns/start
точка добавляет основное имя пользователя и идентификатор сеанса STOMP в хранилище памяти. - Клиент
stompClient.subscribe()
, к/notification/item
которому относится тема STOMP broker. - Затем я могу отправить сообщение этому пользователю или всем пользователям, используя класс, который я создал ниже.
Конфигурация:
@Configuration
@EnableWebSocketMessageBroker
public class STOMPConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/notifications").setAllowedOrigins("*");
registry.addEndpoint("/notifications").setAllowedOrigins("*").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/notification");
registry.setApplicationDestinationPrefixes("/swns");
}
}
Контроллер
@Controller
@Log4j2
public class SocksController {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private NotificationDispatcher notificationDispatcher;
//Send STOMP message to /swns/start to begin a STOMP WSbased connection.
@MessageMapping("/start")
public void send(StompHeaderAccessor stompHeaderAccessor) {
//Can get Spring Principal/Session user from the STOMP header (awesome!).
final Principal user = stompHeaderAccessor.getUser();
log.info("{} initiated a STOMP based websocket.", user != null ? user.getName() : "ANON");
//Add the user's principal name as the key and their STOMP session Id to the static vol HashMap<String,String> in
//the NotificationDispatcher.
NotificationDispatcher.getPrincipalNameToSockSessionMap().put(user.getName(), stompHeaderAccessor.getSessionId());
}
@PostConstruct
void onMade() {
log.info("////////////////////// GIMME UR SOX //////////////////////////////");
}
}
@Component
@EnableScheduling
@Log4j2
public class NotificationDispatcher {
@Getter
@Setter
private volatile static HashMap<String, String> principalNameToSockSessionMap = new HashMap<>();
@Autowired
private SimpMessagingTemplate simpMessagingTemplate;
public void sendToUser(String principalName,String destination,Notification notification) throws NotificationException {
if(!principalNameToSockSessionMap.containsKey(principalName)){
throw new NotificationException(String.format("Can not get session for principal name `%s` as there is no session in RAM map.",principalName));
}
String sessionId = principalNameToSockSessionMap.get(principalName);
log.info("Sending targeted notification to {} - {}", principalName, sessionId);
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
headerAccessor.setSessionId(sessionId);
headerAccessor.setLeaveMutable(true);
simpMessagingTemplate.convertAndSendToUser(
sessionId,
destination!=null?destination:"/notification/item",
notification,
headerAccessor.getMessageHeaders());
}
@EventListener
public void sessionDissconectHandler(SessionDisconnectEvent sessionDisconnectEvent) {
String sessionId = sessionDisconnectEvent.getSessionId();
log.info("Disconnecting : {}", sessionId);
principalNameToSockSessionMap.remove(sessionId);
log.info("Current Sessions Count : {}", principalNameToSockSessionMap.size());
}
@Data
public static class Notification {
private final String value;
public Notification(String s) {
this.value = s;
}
}
public static class NotificationException extends Exception{
public NotificationException(String s) {
super(s);
}
}
}
Клиентский код (находится в React и использует Redux)
const store = configureStore({
reducer: notificationSlice
})
var sock = new SockJS('http://localhost/api/notifications');
const stompClient = Stomp.over(sock);
console.log('open');
stompClient.connect({}, function () {
console.log(`STOMP connected : ${stompClient.connected}`)
stompClient.send("/swns/start", {});
stompClient.subscribe('/user/notification/item', function(menuItem){
console.log("MESSAGE!")
console.log(menuItem)
store.dispatch(setMessage(menuItem.body))
});
console.info('connected!')
},
(error)=>{
console.error(error)
});
Комментарии:
1. Спасибо за ответ, я пытаюсь найти какую-то идею из вашего кода. Я отредактировал свой основной пост, чтобы включить
WebSocketConfig
и JavaScipt . Я проверял компонент Spring, потому что я считаю, в моем ограниченном понимании, что в этом случае он не инициализируется или, так сказать, находится на неправильном «канале». Когда он работал, я ввел класс вPiGPIO
класс, в котором был методsendMessage
, вызывающийSimpMessagingTemplate
sconvertAndSend
. С тех пор мне пришлось провести рефакторинг, но я предполагаюSimpMessagingTemplate
, что по какой-то причине он был правильно инициализирован в этом конкретном классе?2.
// constructor injected, it won't allow field injection here private final SimpMessagingTemplate template;
Указывает, что класс не распознается менеджером контекста компонента Applcation (забыл его имя). Был ли он правильно аннотирован с@Component
помощью и@EnableWebSocketMessageBroker
аннотаций?3. Класс, в котором
SimpMessagingTemplate
работал, был снабжен@Service
аннотациями, которые я пытался добавить в класс, из которого он вызывается в данный момент, но безрезультатно. Текущий класс аннотируется@Component
. Мне пришлось провести рефакторинг, поскольку у меня возникла проблема с циклической зависимостью, иначе я бы оставил все как есть. Я буду редактировать в классе, гдеconvertAndSend
сработал вызов.4. При
SimpMessagingTemplate
этом это уже определенный компонент с добавленной зависимостью от websocket, и поэтому, если вам не нужно его изменять по какой-либо причине, он должен иметь возможность@Autowired
входить в любой контекстно-зависимый класс (т.е.@Component/Sevice/Configuration
). Я бы добавил @PostConstruct для ваших компонентов, чтобы узнать, запускаются ли они? Как у меня:@PostConstruct void onMade() { log.info("////////////////////// GIMME UR SOX //////////////////////////////"); }
5. У меня такое чувство, что это может быть здесь:
this.template.convertAndSend("/topic/gpio", message);
где для STOMP вы должны включать заголовки и т.д. См codesandnotes.be/2020/03/31 /…