SimpMessagingTemplate.convertAndSend() работает в классе @Controller, но нигде больше

#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:

Не совсем уверен, что вы делаете с отсутствующей конфигурацией и т.д. Не уверен, что вы подразумеваете под битом «проверки компонента».

Вот моя настройка для базового канала «уведомления».

  1. Клиент попадает в конечную точку STOMP /swns/start , чтобы открыть соединение STOMP.
  2. Конечная /swns/start точка добавляет основное имя пользователя и идентификатор сеанса STOMP в хранилище памяти.
  3. Клиент stompClient.subscribe() , к /notification/item которому относится тема STOMP broker.
  4. Затем я могу отправить сообщение этому пользователю или всем пользователям, используя класс, который я создал ниже.

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

 @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 s convertAndSend . С тех пор мне пришлось провести рефакторинг, но я предполагаю 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 /…