Spring Boot - WebSocket - Doesn't show subscribers [duplicate] - spring

This question already has answers here:
Principal is null for every Spring websocket event
(2 answers)
Closed last month.
I was going over the basic Spring Boot WebSocket Tutorial: https://spring.io/guides/gs/messaging-stomp-websocket/
I decided to modify it to print out how many users are subscribed to a channel in the console but couldn't figure it out for hours. I've seen a few StackOverflow posts but they don't help. The last one I check was this: https://stackoverflow.com/a/51113021/11200149 which says to add try this:
#Autowired private SimpUserRegistry simpUserRegistry;
public Set<SimpUser> getUsers() {
return simpUserRegistry.getUsers();
}
So, I added the above to my controller, and here is the change:
#Controller
public class GreetingController {
#Autowired
private SimpUserRegistry userRegistry;
#MessageMapping("/hello")
#SendTo("/topic/greetings")
public Greeting greeting(HelloMessage message) throws Exception {
Set<SimpUser> subscribedUsers = userRegistry.getUsers();
System.out.println("User amount: " + subscribedUsers.size()); // always prints: 0
return new Greeting("Hello, " + HtmlUtils.htmlEscape(message.getName()) + "!");
}
}
This always prints 0:
System.out.println("User amount: " + subscribedUsers.size());
I'm coming from Socket.IO so maybe things work a bit differently because I've seen people implement their own manual Subscription Service classes. In socket.io this would be a piece of cake so I would assume Spring Boot would have this, but I just can't seem to find it.
Edit: This post does a great explanation for this problem.
Principal is null for every Spring websocket event

Maybe you can try to add custom HandshakeHandler class into registry and override the determineUser method to return the Principal object that containing subscriber name so that the SimpUserRegistry can work properly.
If you would like to see the effect, the below is what I'm trying.
app.js (sending out a user name through request parameter)
function connect() {
var socket = new SockJS('/gs-guide-websocket?name=' + $('#name').val());
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
setConnected(true);
console.log('Connected: ' + frame);
stompClient.subscribe('/topic/greetings', function (greeting) {
showGreeting(JSON.parse(greeting.body).content);
});
});
}
custom class extends DefaultHandshakeHandler.class
#Component
public class WebSocketHandShakeHandler extends DefaultHandshakeHandler {
#Override
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpServletRequest httpServletRequest = servletRequest.getServletRequest();
String name = httpServletRequest.getParameter("name");
return new MyPrincipal(name);
}
}
custom object implement Principal.class
#Data
#NoArgsConstructor
#AllArgsConstructor
public class MyPrincipal implements Principal {
private String name;
}
WebSocketConfig.class
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Autowired
private WebSocketHandShakeHandler webSocketHandShakeHandler;
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/gs-guide-websocket")
.setHandshakeHandler(webSocketHandShakeHandler)
.withSockJS();
}
}
Show all subscribers
#RestController
public class ApiController {
#Autowired
private SimpUserRegistry simpUserRegistry;
#GetMapping("/users")
public List<String> connectedEquipments() {
return this.simpUserRegistry
.getUsers()
.stream()
.map(SimpUser::getName).toList();
}
}
Result
By the way, you can check the DefaultSimpUserRegistry.class to observe the process of putting name into subscribers user map.

Related

spring boot interceptor for specific api, should not be invoked for all the api's

2 api's were exposed in a spring boot controller class. I have a requirement to intercept only 1 api and SHOULD NOT intercept other api. Can someone assist how to do this?
Below is the code
public class HeaderValidationInterceptor extends HandlerInterceptorAdapter{
private static final Logger logger = Logger.getLogger(HeaderValidationInterceptor.class);
//before the actual handler will be executed
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler)
throws Exception {
validateHeaderParam(request);
request.setAttribute("startTime", startTime);
return true;
}
}
Also I have a configuration class to add interceptor as below
Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
#Autowired
HeaderValidationInterceptor headerValidationInterceptor;
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(headerValidationInterceptor)
}
}
Controller class
#RestController
public class MyController {
#Autowired
private ICityService cityService;
#GetMapping(value = "/cities")
public List<City> getCities() {
List<City> cities = cityService.findAll();
return cities;
}
#GetMapping(value = "/cities/{cityId}")
public City getCityById(#PathVariable("cityId") String cityId) {
City city = cityService.findCityById(cityId);
return cities;
}
}
Inside your interceptor, you can check the request URI for the endpoint you want to intercept.
You can use a regular expression to match the URI. Following for /cities/{cityId} endpoint.
if (request.getRequestURI().matches("(\\/cities\\/[a-zA-Z0-9]+\\/?)")) {
validateHeaderParam(request);
request.setAttribute("startTime", startTime);
}
I'm not sure what is that want to do in your interceptor, but for your example you can do this inside your controller as well. Like this,
#GetMapping(value = "/cities/{cityId}")
public City getCityById(#PathVariable("cityId") String cityId, HttpServletRequest request) {
// Here you can use HttpServletRequest and do your validation
// validateHeaderParam(request);
// request.setAttribute("startTime", startTime);
City city = cityService.findCityById(cityId);
return cities;
}

Spring get open websocket connections

I am using Spring Boot Websocket to enable my Spring Boot 2 microservice to deal with websocket connections.
My config:
#Configuration
#EnableWebSocket
public class WsConfig implements WebSocketConfigurer {
#Autowired
WebSocketHandler webSocketHandler;
#Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
DefaultHandshakeHandler handshakeHandler = new DefaultHandshakeHandler();
handshakeHandler.setSupportedProtocols(HANDSHAKE_PROTOCOL);
registry.addHandler(webSocketHandler, WS_HANDLER_PATH + WILDCARD)
.setAllowedOrigins("*")
.setHandshakeHandler(handshakeHandler);
}
}
My clients are able to connect to my Service via Websocket. I am implementing WebSocketHandler interface to handle messages and log connections.
Now my question: Is there a way to show all current websocket users/sessions?
I was trying to use the SimpUserRegistry:
#Configuration
public class UserConfig {
final private SimpUserRegistry userRegistry = new DefaultSimpUserRegistry();
#Bean
public SimpUserRegistry userRegistry() {
return userRegistry;
}
}
and to show the users via a REST endpoint
#RestController
public class WebSocketManager {
private final SimpUserRegistry userRegistry;
public WebSocketManager(SimpUserRegistry userRegistry) {
this.userRegistry = userRegistry;
}
#GetMapping(path = "/users")
public List<String> getConnectedUsers() {
userRegistry.getUsers().stream()
.map(SimpUser::getName)
.forEach(System.out::println);
System.out.println("Users " + userRegistry.getUsers());
System.out.println("UsersCount " + userRegistry.getUserCount());
return this.userRegistry
.getUsers()
.stream()
.map(SimpUser::getName)
.collect(Collectors.toList());
}
}
But this always gives my an empty list: [] even when obviously WS connections are established.
Is this SimpUserRegistry working with the Websocket system of Spring which is configured with the WebSocketConfigurer and #EnableWebSocket? What am I doing wrong? Any tips or alternatives?
Thank you in advance!

How to send message to WebSocket client from Spring WebSocket server using STOMP?

I have two Spring Boot WebSocket applications using STOMP:
WebSocket server
WebSocket client
I am able to send a WebSocket message from the client and respond to it from the server. However, now I would like to send a WebSocket message to the client triggered by an event on the server side.
Can someone tell me a way to do this?
Here is what I have now on the server side:
WebSocketConfig.java:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic/");
config.setApplicationDestinationPrefixes("/app");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/alarm");
}
}
WebSocketController.java:
#Controller
public class WebSocketController {
#MessageMapping("/alarm")
#SendTo("/topic/message")
public void processMessageFromClient(#Payload String message, Principal principal) throws Exception {
System.out.println("WEBSOCKET MESSAGE RECEIVED" + message);
}
#RequestMapping(value = "/start/{alarmName}", method = RequestMethod.POST)
public String start(#PathVariable String alarmName) throws Exception {
System.out.println("Starting " + alarmName);
/* SEND MESSAGE TO WEBSOCKET CLIENT HERE */
return "redirect:/";
}
}
I found the answer on the official spring documentation.
You just need to inject a SimpMessagingTemplate.
My controller now looks like this:
#Controller
public class WebSocketController {
private SimpMessagingTemplate template;
#Autowired
public WebSocketController(SimpMessagingTemplate template) {
this.template = template;
}
#MessageMapping("/alarm")
#SendTo("/topic/message")
public void processMessageFromClient(#Payload String message, Principal principal) throws Exception {
System.out.println("WEBSOCKET MESSAGE RECEIVED" + message);
}
#RequestMapping(value = "/start/{alarmName}", method = RequestMethod.POST)
public String start(#PathVariable String alarmName) throws Exception {
System.out.println("Starting " + alarmName);
this.template.convertAndSend("/topic/message", alarmName);
return "redirect:/";
}
}

Getting Spring simpMessagingTemplate to work with websocket

I have been trying to get simpMessagingTemplate to send to websocket in Spring but to no avail. From what I can see of related stackoverflow posts and other guides, I have provided the necessary configuration and mapping of paths.
My code is shown as below:
RestController (which I use to invoke sending of the message to the websocket):
#RestController
public class RestControllers {
#Autowired
private SimpMessagingTemplate template;
#RequestMapping("/test")
public String doTest() {
Message m = new Message();
m.setFrom("foo");
m.setText("bar");
template.convertAndSend("/app/chat/test-topic", m);
return m.toString();
}
}
Controller:
#Controller
public class ChatController
{
#MessageMapping("/chat/{topic}")
#SendTo("/topic/messages")
public OutputMessage send(#DestinationVariable("topic") String topic,
Message message) throws Exception
{
System.out.println("THE MESSAGE WAS RECEIVED:" + message.toString());
return new OutputMessage(message.getFrom(), message.getText(), topic);
}
}
Configuration:
#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) //?? alternative only?
{
registry.addEndpoint("/chat").setAllowedOrigins("*").withSockJS();
}
}

Send notification to offline user with persistent queue STOMP

I have this code: Client-side with javascript:
socket = new SockJS(context.backend + '/myWebSocketEndPoint');
stompClient = Stomp.over(socket);
stompClient.connect({},function (frame) {
stompClient.subscribe('/queue/'+clientId+'/notification', function(response){
alert(angular.fromJson(response.body));
});
});
In this code, a client when connects, subscribe to receive notification using '/queue/'+ his client id + '/notification/. So i have a queue for every client. I use stomp with sockjs
In my server (Java + spring boot) i have a notification listener which when an event is published, it send a notification to all clients. So i have:
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer{
#Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/queue");
}
#Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/myWebSocketEndPoint")
.setAllowedOrigins("*")
.withSockJS();
}
}
the class MenuItemNotificationChannel who call MenuItemNotificationSender to send the notification to the users.
#Component
public class MenuItemNotificationChannel extends AbstractNotificationChannel {
#Autowired
private MenuItemNotificationSender menuItemNotificationSender;
#Autowired
private UserRepository userRepository;
#Override
public void sendNotification(KitaiEvent<?> event, Map<String, Object> notificationConfiguration) throws Exception {
String menuItem = Optional.ofNullable((String) notificationConfiguration.get(MENU_ENTRY_KEY)).orElseThrow(IllegalArgumentException::new);
List<User> userList = userRepository.findAll();
for(User u: userList){
menuItemNotificationSender.sendNotification(new MenuItemDto(menuItem),u.getId());
}
MenuItemNotificationSender class is:
#Component
public class MenuItemNotificationSender {
#Autowired
private SimpMessagingTemplate messagingTemplate;
#Autowired
public MenuItemNotificationSender(SimpMessagingTemplate messagingTemplate){
this.messagingTemplate = messagingTemplate;
}
public void sendNotification(MenuItemDto menuItem,Long id) {
String address = "/queue/"+id+"/notification";
messagingTemplate.convertAndSend(address, menuItem);
}
}
This code works perfectly: notifications are sent to every user. But if a user is not online, notifications are losts. My questions are:
How can i verify whit stomp what subscriptions are active and what are not?? (If i can verify if a subscription is active, i solve my problem because i save notification for users offline and then send them when they do login)
Can i use persistent queues? (i read something about it, but i have not understand if i can use it only with stomp and sockjs)
Sorry for my english! :D
You can put a spring event listener on the session connected event and the session disconnect event
I tested this one with spring 4.3.4
#Component
public class WebSocketSessionListener
{
private static final Logger logger = LoggerFactory.getLogger(WebSocketSessionListener.class.getName());
private List<String> connectedClientId = new ArrayList<String>();
#EventListener
public void connectionEstablished(SessionConnectedEvent sce)
{
MessageHeaders msgHeaders = sce.getMessage().getHeaders();
Principal princ = (Principal) msgHeaders.get("simpUser");
StompHeaderAccessor sha = StompHeaderAccessor.wrap(sce.getMessage());
List<String> nativeHeaders = sha.getNativeHeader("userId");
if( nativeHeaders != null )
{
String userId = nativeHeaders.get(0);
connectedClientId.add(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Connessione websocket stabilita. ID Utente "+userId);
}
}
else
{
String userId = princ.getName();
connectedClientId.add(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Connessione websocket stabilita. ID Utente "+userId);
}
}
}
#EventListener
public void webSockectDisconnect(SessionDisconnectEvent sde)
{
MessageHeaders msgHeaders = sde.getMessage().getHeaders();
Principal princ = (Principal) msgHeaders.get("simpUser");
StompHeaderAccessor sha = StompHeaderAccessor.wrap(sde.getMessage());
List<String> nativeHeaders = sha.getNativeHeader("userId");
if( nativeHeaders != null )
{
String userId = nativeHeaders.get(0);
connectedClientId.remove(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Disconnessione websocket. ID Utente "+userId);
}
}
else
{
String userId = princ.getName();
connectedClientId.remove(userId);
if( logger.isDebugEnabled() )
{
logger.debug("Disconnessione websocket. ID Utente "+userId);
}
}
}
public List<String> getConnectedClientId()
{
return connectedClientId;
}
public void setConnectedClientId(List<String> connectedClientId)
{
this.connectedClientId = connectedClientId;
}
}
When a client is connected you add in the List of clients id the client id; when it disconnects you remove it
Then you can inject this bean or its List where you want to check if the client is active or less and then you can check if the client id is between the connected clients ID you can send the message, otherwise you must save it and resend later
On client side you can do something like this:
var socket = new SockJS('/ws');
stompClient = Stomp.over(socket);
stompClient.connect({userId:"customUserId"}, function (frame) {
});
Angelo
why not using some events like below, you can export classes to differents files and use SessionConnectedEvent and SessionDisconnectEvent OR SessionSubscribeEvent and SessionUnsubscribeEvent.
see doc here http://docs.spring.io/spring/docs/current/spring-framework-reference/html/websocket.html#websocket-stomp-appplication-context-events
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectedEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import org.springframework.web.socket.messaging.SessionSubscribeEvent;
import org.springframework.web.socket.messaging.SessionUnsubscribeEvent;
#Component
public class SessionConnectedListener extends SessionsListener implements ApplicationListener<SessionConnectedEvent> {
#Override
public void onApplicationEvent(SessionConnectedEvent event) {
users.add(event.getUser().getName());
}
}
#Component
class SessionDisconnectListener extends SessionsListener implements ApplicationListener<SessionDisconnectEvent> {
#Override
public void onApplicationEvent(SessionDisconnectEvent event) {
users.remove(event.getUser().getName());
}
}
#Component
class SessionSubscribeListener extends SessionsListener implements ApplicationListener<SessionSubscribeEvent> {
#Override
public void onApplicationEvent(SessionSubscribeEvent event) {
users.add(event.getUser().getName());
}
}
#Component
class SessionUnsubscribeListener extends SessionsListener implements ApplicationListener<SessionUnsubscribeEvent> {
#Override
public void onApplicationEvent(SessionUnsubscribeEvent event) {
users.remove(event.getUser().getName());
}
}
class SessionsListener {
protected List<String> users = Collections.synchronizedList(new LinkedList<String>());
public List<String> getUsers() {
return users;
}
}
and change your code :
#Autowired
private SessionsListener sessionsListener;
#Override
public void sendNotification(KitaiEvent<?> event, Map<String, Object> notificationConfiguration) throws Exception {
String menuItem = Optional.ofNullable((String) notificationConfiguration.get(MENU_ENTRY_KEY)).orElseThrow(IllegalArgumentException::new);
List<String> userList = sessionsListener.getUsers();
for(String u: userList){
menuItemNotificationSender.sendNotification(new MenuItemDto(menuItem),u);
}

Resources