package com.ankurm.ssews; import java.security.Principal; import java.time.Instant; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessageType; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Controller; /** * The chat. Three handlers, each demonstrating a different routing rule. * * @see docs/04-stomp.md */ @Controller public class ChatController { private final SimpMessagingTemplate template; ChatController(SimpMessagingTemplate template) { this.template = template; } /** Explicit fan-out destination. Client SENDs to /app/chat.send, subscribers get /topic/room. */ @MessageMapping("/chat.send") @SendTo("/topic/room") public ChatMessage send(@Payload ChatMessage in, SimpMessageHeaderAccessor headers) { String from = in.from() == null ? headers.getSessionId() : in.from(); return new ChatMessage(from, in.text(), Instant.now()); } /** * No {@code @SendTo}. The return value still goes somewhere: the default destination is the * broker prefix plus the mapping, i.e. {@code /topic/chat.echo}. That default is why a * handler you thought was private is readable by anyone who guesses the path. */ @MessageMapping("/chat.echo") public ChatMessage echo(@Payload ChatMessage in) { return new ChatMessage("echo", in.text(), Instant.now()); } /** * Point-to-point. Sent to one session's private destination; every other subscriber to * /user/queue/whisper gets nothing, because the broker rewrote the destination per session. * *
The three extra lines are not optional and their absence is silent. With no * authenticated {@link Principal} the "user" is the STOMP session id, and * {@code DefaultUserDestinationResolver} can only turn a session id into a real destination * if the message also carries that session id in its headers. Call the two-argument * {@code convertAndSendToUser(sessionId, dest, payload)} instead and the message is resolved * against a user registry that has never heard of that name: no exception, no log line, no * delivery. {@code setLeaveMutable(true)} is required too — without it the accessor is * immutable by the time the resolver looks, and the session id is lost again. */ @MessageMapping("/chat.whisper") public void whisper(@Payload ChatMessage in, SimpMessageHeaderAccessor headers, Principal principal) { String targetSessionId = in.from(); SimpMessageHeaderAccessor out = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE); out.setSessionId(targetSessionId); out.setLeaveMutable(true); template.convertAndSendToUser(targetSessionId, "/queue/whisper", new ChatMessage(principal == null ? headers.getSessionId() : principal.getName(), in.text(), Instant.now()), out.getMessageHeaders()); } }