I have a Websocket-stomp server based on Spring and its SimpleBroker implementation (not utilizing an external broker).
I would like to enable STOMP RECEIPT messages.
How I could configure my code to send these automatically?
In Spring Integration test for the STOMP protocol we have this code:
//SimpleBrokerMessageHandler doesn't support RECEIPT frame, hence we emulate it this way
@Bean
public ApplicationListener<SessionSubscribeEvent> webSocketEventListener(
final AbstractSubscribableChannel clientOutboundChannel) {
return event -> {
Message<byte[]> message = event.getMessage();
StompHeaderAccessor stompHeaderAccessor = StompHeaderAccessor.wrap(message);
if (stompHeaderAccessor.getReceipt() != null) {
stompHeaderAccessor.setHeader("stompCommand", StompCommand.RECEIPT);
stompHeaderAccessor.setReceiptId(stompHeaderAccessor.getReceipt());
clientOutboundChannel.send(
MessageBuilder.createMessage(new byte[0], stompHeaderAccessor.getMessageHeaders()));
}
};
}
A solution similar to the post of Artem Bilan using a separated class to implement the listener.
@Component
public class SubscribeListener implements ApplicationListener<SessionSubscribeEvent> {
@Autowired
AbstractSubscribableChannel clientOutboundChannel;
@Override
public void onApplicationEvent(SessionSubscribeEvent event) {
Message<byte[]> message = event.getMessage();
StompHeaderAccessor stompHeaderAccessor = StompHeaderAccessor.wrap(message);
if (stompHeaderAccessor.getReceipt() != null) {
StompHeaderAccessor receipt = StompHeaderAccessor.create(StompCommand.RECEIPT);
receipt.setReceiptId(stompHeaderAccessor.getReceipt());
receipt.setSessionId(stompHeaderAccessor.getSessionId());
clientOutboundChannel.send(MessageBuilder.createMessage(new byte[0], receipt.getMessageHeaders()));
}
}
}
all of the above send receipt frames too early. the following gets you what you need.
ref: https://github.com/spring-projects/spring-framework/issues/21848
@Configuration
static class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private MessageChannel outChannel;
@Autowired
public WebSocketConfig(MessageChannel clientOutboundChannel) {
this.outChannel = clientOutboundChannel;
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ExecutorChannelInterceptor() {
@Override
public void afterMessageHandled(Message<?> inMessage,
MessageChannel inChannel, MessageHandler handler, Exception ex) {
StompHeaderAccessor inAccessor = StompHeaderAccessor.wrap(inMessage);
String receipt = inAccessor.getReceipt();
if (StringUtils.isEmpty(receipt)) {
return;
}
StompHeaderAccessor outAccessor = StompHeaderAccessor.create(StompCommand.RECEIPT);
outAccessor.setSessionId(inAccessor.getSessionId());
outAccessor.setReceiptId(receipt);
outAccessor.setLeaveMutable(true);
Message<byte[]> outMessage =
MessageBuilder.createMessage(new byte[0], outAccessor.getMessageHeaders());
outChannel.send(outMessage);
}
});
}
}
This is what I ended up doing.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketMessageBrokerConfigurer
implements org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer {
private MessageChannel outChannel;
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(new ExecutorChannelInterceptor() {
@Override
public void afterMessageHandled(Message<?> inMessage,
MessageChannel inChannel, MessageHandler handler, Exception ex) {
if(outChannel == null) {
return;
}
StompHeaderAccessor inAccessor = StompHeaderAccessor.wrap(inMessage);
String receipt = inAccessor.getReceipt();
if (StringUtils.isEmpty(receipt)) {
return;
}
StompHeaderAccessor outAccessor = StompHeaderAccessor.create(StompCommand.RECEIPT);
outAccessor.setSessionId(inAccessor.getSessionId());
outAccessor.setReceiptId(receipt);
outAccessor.setLeaveMutable(true);
Message<byte[]> outMessage =
MessageBuilder.createMessage(new byte[0], outAccessor.getMessageHeaders());
outChannel.send(outMessage);
}
});
}
public void configureClientOutboundChannel(ChannelRegistration registration) {
registration.interceptors(new ExecutorChannelInterceptor() {
@Override
public void afterMessageHandled(Message<?> message, MessageChannel channel, MessageHandler handler, @Nullable Exception ex) {
outChannel = channel;
}
});
}
}
Also for the tests you have to configure the stompClient with a taskScheduler. This is so the tasks for handling receipts can happen.
stompClient.setTaskScheduler(taskScheduler);
Then the Session
and the Receiptable
objects need to be configured. For instance use the addReceiptTask
on the Subscription object.
@Override
public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
session.setAutoReceipt(true);
StompSession.Subscription subscription = session.subscribe(topic, this);
subscription.addReceiptTask(() -> {
logger.info("Receipt for subscription ID {}", subscription.getSubscriptionId());
connectedFuture.complete(null);
});
logger.info("Stomp Subscription ID is '{}' for topic '{}'.",
subscription.getSubscriptionId(), topic);
}
As you can see there is also a connectedFuture which completes when the subscription receipt is handled. This allows the tests to know when they can start sending data to the client.
The answer provided by Artem Bilan only works for SUBSCRIBE frames. Here is another one that captures any incoming frame with receipt header. Only the class with the @EnableWebSocketMessageBroker annotation needs to be extended, other classes (like the ones with @Controller annotation) remain the same.
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.Map;
import java.util.List;
import java.util.HashMap;
import java.util.ArrayList;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.messaging.simp.config.SimpleBrokerRegistration;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.MessageChannel;
import org.springframework.beans.factory.annotation.Autowired;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
private static final Logger LOGGER = Logger.getLogger( WebSocketConfig.class.getName() );
private MessageChannel outChannel;
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors( new InboundMessageInterceptor() );
}
@Override
public void configureClientOutboundChannel(ChannelRegistration registration) {
registration.interceptors( new OutboundMessageInterceptor() );
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// prefixes are application-dependent
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/note");
}
class InboundMessageInterceptor extends ChannelInterceptorAdapter {
@SuppressWarnings("unchecked")
public Message preSend(Message message, MessageChannel channel) {
LOGGER.log( Level.SEVERE, "preSend: "+message );
GenericMessage genericMessage = (GenericMessage)message;
MessageHeaders headers = genericMessage.getHeaders();
String simpSessionId = (String)headers.get( "simpSessionId" );
if( ( SimpMessageType.MESSAGE.equals( headers.get( "simpMessageType" ) ) &&
StompCommand.SEND.equals( headers.get( "stompCommand" ) ) ) ||
( SimpMessageType.SUBSCRIBE.equals( headers.get( "simpMessageType" ) ) &&
StompCommand.SUBSCRIBE.equals( headers.get( "stompCommand" ) ) ) &&
( simpSessionId != null ) ) {
Map<String,List<String>> nativeHeaders = (Map<String,List<String>>)headers.get( "nativeHeaders" );
if( nativeHeaders != null ) {
List<String> receiptList = nativeHeaders.get( "receipt" );
if( receiptList != null ) {
String rid = (String)receiptList.get(0);
LOGGER.log( Level.SEVERE, "receipt requested: "+rid );
sendReceipt( rid, simpSessionId );
}
}
}
return message;
}
private void sendReceipt( String rid, String simpSessionId ) {
if( outChannel != null ) {
HashMap<String,Object> rcptHeaders = new HashMap<String,Object>();
rcptHeaders.put( "simpMessageType", SimpMessageType.OTHER );
rcptHeaders.put( "stompCommand", StompCommand.RECEIPT );
rcptHeaders.put( "simpSessionId", simpSessionId );
HashMap<String,List<String>> nativeHeaders = new HashMap<String,List<String>>();
ArrayList<String> receiptElements = new ArrayList<String>();
receiptElements.add( rid );
nativeHeaders.put( "receipt-id", receiptElements );
rcptHeaders.put( "nativeHeaders",nativeHeaders );
GenericMessage<byte[]> rcptMsg = new GenericMessage<byte[]>( new byte[0],new MessageHeaders( rcptHeaders ) );
outChannel.send( rcptMsg );
} else
LOGGER.log( Level.SEVERE, "receipt NOT sent" );
}
}
class OutboundMessageInterceptor extends ChannelInterceptorAdapter {
public void postSend(Message message,
MessageChannel channel,
boolean sent) {
LOGGER.log( Level.SEVERE, "postSend: "+message );
outChannel = channel;
}
}
}
Indeed, it is much more complicated than it should be and obtaining the outChannel is not very elegant. But it works. :-)