I am injecting the SimpMessagingTemplate into some of my @Component classes in my Springboot app, but simpMessagingTemplate.convertAndSend(dest, payload) only sends from the @Controller class where the websocket endpoints are handled.
Controller
@MessageMapping("/gpio_ws") @SendTo("/topic/gpio") public GPIOMessage sendGPIOMessage(GPIOMessage message) { log.info("Sending WS message: " + message); simpMessagingTemplate.convertAndSend("/topic/gpio", message); }I have some Javascript in the frontend with Stomp and SockJS which connects and sends messages through this endpoint without a problem.
Example use in another class
// 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)); } }));I did have this working the other day and I'm now at a complete loss.
I checked the bean by clicking the little icon in IntelliJ and all instances of SimpMessagingTemplate seem to be pointing to the same bean.
Can SimpMessagingTemplate work outside the class where the endpoint is declared? Why not?
Edit: Here is the websocket config
@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(); }
}A snippet of the Javascript. During putting together the websocket code, I used JS to receive and send, but for this particular problem I just need it to receive. Although I have left the send/receive functionality at the moment so I can test the WS is still working.
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(); } }
}Edit:
Added @PostConstruct to two classes where I am trying to use SimpMessagingTemplate.
@PostConstruct
void onMade() { log.info("Bean initialised");
}Starting the app, result seems to show both classes are being found and initialised by 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 initialisedSimpleMessageBrokerHandler seems to have zero destinations, I'm not sure if this is a problem or as the app runs this changes.
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]]]]Edit:
I added some more log calls to see what is happening with SimpMessagingTemplate in the class after it's initialised.
log.info(template.getDefaultDestination());
log.info(template.getUserDestinationPrefix());
log.info(template.getMessageChannel().toString());Result
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]I'm just guessing here, but the default destination should not be null? Have I got a configuration problem, or should I set the default destination?
1 Answer
Not entirely sure what you are doing with the missing config etc. Not sure what you mean by "checking the bean" bit either.
Here is my set up for a basic "notification" channel.
- Client hits the STOMP endpoint
/swns/startto open the STOMP connection. - The
/swns/startendpoint adds the user's principal name and STOMP session ID to a in memmory store. - Client
stompClient.subscribe()to/notification/itemwhich is a STOMP broker topic. - I can then send message to that user or all users using the class I made below.
The config:
@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"); }
}The Controller
@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); } }
}Client Code (is in React and using Redux)
const store = configureStore({ reducer: notificationSlice
})
var sock = new SockJS('); 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) }); 6