Nearly 2 years ago, excellent WebSocket Support appeared in Spring 4, easily usable using STOMP over Websockets / SockJS on the client side, backed by a pluggable broker on the server side, which can either be simple broker using scheduled executor services to handle message or a full fledged RabbitMQ or ActiveMQ solution.
Using
@EnableWebSocketMessageBroker |
@EnableWebSocketMessageBroker
enables the first solution without much fuss, integrating an existing and running ActiveMQ STOMP transport is nearly as easy:
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/topic")
.setRelayPort(1234)
.setClientLogin("client-user")
.setClientPasscode("client-password")
.setSystemLogin("sys-user")
.setSystemPasscode("sys-password");
registry.setApplicationDestinationPrefixes("/app");
} |
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/topic")
.setRelayPort(1234)
.setClientLogin("client-user")
.setClientPasscode("client-password")
.setSystemLogin("sys-user")
.setSystemPasscode("sys-password");
registry.setApplicationDestinationPrefixes("/app");
}
I’ve already used (and i am using) such a solution in this application, as described here. What you’re doing is not instantiating your own scheduler and transport, but relaying everything to the existing transport which is nice when you don’t want to was resources.
What’s missing here, is some deeper integration.
Using Springs JmsTemplate it’s really easy to send JMS messages and it’s also easy to connect simple beans or services through messages:
Imagine an arbitrary service, not knowing anything about jms:
class SomeCmd {
}
public class SomeService {
public void someVoidMethod(final SomeCmd someCmd) {
System.out.println("Something incredible");
}
} |
class SomeCmd {
}
public class SomeService {
public void someVoidMethod(final SomeCmd someCmd) {
System.out.println("Something incredible");
}
}
Connecting this to a queue is as easy as:
import javax.jms.ConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.listener.SimpleMessageListenerContainer;
import org.springframework.jms.listener.adapter.MessageListenerAdapter;
@Configuration
class Config {
@Bean
public SimpleMessageListenerContainer someServiceContainer(final SomeService someService, final ConnectionFactory connectionFactory) {
// Create an adapter for some service
final MessageListenerAdapter messageListener = new MessageListenerAdapter(someService);
// Connect the method
messageListener.setDefaultListenerMethod("someVoidMethod");
// to a queue
final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setDestinationName("some.queue");
container.setMessageListener(messageListener);
container.setConnectionFactory(connectionFactory);
return container;
}
} |
import javax.jms.ConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.listener.SimpleMessageListenerContainer;
import org.springframework.jms.listener.adapter.MessageListenerAdapter;
@Configuration
class Config {
@Bean
public SimpleMessageListenerContainer someServiceContainer(final SomeService someService, final ConnectionFactory connectionFactory) {
// Create an adapter for some service
final MessageListenerAdapter messageListener = new MessageListenerAdapter(someService);
// Connect the method
messageListener.setDefaultListenerMethod("someVoidMethod");
// to a queue
final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setDestinationName("some.queue");
container.setMessageListener(messageListener);
container.setConnectionFactory(connectionFactory);
return container;
}
}
Sending messages is a onliner:
jmsTemplate.convertAndSend("some.queue", new SomeCmd()); |
jmsTemplate.convertAndSend("some.queue", new SomeCmd());
If that method isn’t a void method, the result can automatically be passed to another queue:
class SomeCmd {
}
class SomeResult {
}
public class SomeService {
public SomeResult someNonVoidMethod(final SomeCmd someCmd) {
return new SomeResult();
}
}
// Change in the config
messageListener.setDefaultListenerMethod("someNonVoidMethod");
// And added to the config:
messageListener.setDefaultResponseQueueName("some.response.queue"); |
class SomeCmd {
}
class SomeResult {
}
public class SomeService {
public SomeResult someNonVoidMethod(final SomeCmd someCmd) {
return new SomeResult();
}
}
// Change in the config
messageListener.setDefaultListenerMethod("someNonVoidMethod");
// And added to the config:
messageListener.setDefaultResponseQueueName("some.response.queue");
What if i want to have the result right available on a web site using WebSockets and STOMP? Although the name sounds similar, the SimpMessagingTemplate has nothing todo with the JmsTemplate, at least not immediately.
As it turns out, it’s relatively simple redirecting the output of bean to the STOMP queue, knowing
Note that the prefix in stomp /queue/ or /topic/ is removed from the string before passing it to ActiveMQ as a JMS destination. Also note that the default separator in MOM systems is . (DOT). So FOO.BAR is the normal syntax of a MOM queue – the Stomp equivalent would be /queue/FOO.BAR
Working with Destinations with Stomp
In the above example
messageListener.setDefaultResponseQueueName("some.response.queue");
// becomes
messageListener.setDefaultResponseTopicName("some/response/topic"); |
messageListener.setDefaultResponseQueueName("some.response.queue");
// becomes
messageListener.setDefaultResponseTopicName("some/response/topic");
Which means: Everything SomeService#someNonVoidMethod returns is send to a STOMP topic call /topic/some/response/topic.
Nice. But it turns out, SimpMessagingTemplate converts the messages body to a nice, readable JSON format, internally proceeded by Jacksons Object Mapper. Without a custom converter, we’ll end up with a MapMessage. To make the outcome of the topic the same, regardless wether produced using SimpMessagingTemplate or redirecting the outcome from a JMS queue, we need a converter. If you’re using ActiveMQ like i you can use a “Message transformations”, but that has some drawbacks: The connection needs to be opened with a special header and what is worse, the Json generation is based on Jettison (which i cannot link anymore because the Codehause page stopped working) which is basically impossible to customize.
So instead, i assume there are ObjectMessages entering my queue (send through JmsTemplate to reach my service) and outgoing stuff should be in form of TextMessages. With that assumption, just use a slightly adapted MappingJackson2MessageConverter:
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.ObjectMessage;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.listener.SimpleMessageListenerContainer;
import org.springframework.jms.listener.adapter.MessageListenerAdapter;
import org.springframework.jms.support.converter.MappingJackson2MessageConverter;
import org.springframework.jms.support.converter.MessageConversionException;
import org.springframework.jms.support.converter.MessageType;
@Configuration
class Config {
@Bean
public SimpleMessageListenerContainer someServiceContainer(final SomeService someService, final ConnectionFactory connectionFactory) {
// Create an adapter for some service
final MessageListenerAdapter messageListener = new MessageListenerAdapter(someService);
// Connect the method [1]
messageListener.setDefaultListenerMethod("someNonVoidMethod");
// Direct every outcome of "someNonVoidMethod" to a topic, that is
// subscribable via stompClient.subscribe('/topic/some/response/topic', {});
messageListener.setDefaultResponseTopicName("some/response/topic");
// and take care of converting someResult to a JSON payload, otherwise we'll end up with a
final MappingJackson2MessageConverter messageConverter = new MappingJackson2MessageConverter() {
@Override
public Object fromMessage(Message message) throws JMSException, MessageConversionException {
return message instanceof ObjectMessage ? ((ObjectMessage) message).getObject() : super.fromMessage(message);
}
@Override
protected TextMessage mapToTextMessage(Object object, Session session, ObjectMapper objectMapper) throws JMSException, IOException {
final TextMessage rv = super.mapToTextMessage(object, session, objectMapper);
rv.setStringProperty("content-type", "application/json;charset=UTF-8");
return rv;
}
};
messageConverter.setTargetType(MessageType.TEXT);
// [2] to a queue
final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setDestinationName("some.queue");
container.setMessageListener(messageListener);
container.setConnectionFactory(connectionFactory);
return container;
}
} |
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.ObjectMessage;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.listener.SimpleMessageListenerContainer;
import org.springframework.jms.listener.adapter.MessageListenerAdapter;
import org.springframework.jms.support.converter.MappingJackson2MessageConverter;
import org.springframework.jms.support.converter.MessageConversionException;
import org.springframework.jms.support.converter.MessageType;
@Configuration
class Config {
@Bean
public SimpleMessageListenerContainer someServiceContainer(final SomeService someService, final ConnectionFactory connectionFactory) {
// Create an adapter for some service
final MessageListenerAdapter messageListener = new MessageListenerAdapter(someService);
// Connect the method [1]
messageListener.setDefaultListenerMethod("someNonVoidMethod");
// Direct every outcome of "someNonVoidMethod" to a topic, that is
// subscribable via stompClient.subscribe('/topic/some/response/topic', {});
messageListener.setDefaultResponseTopicName("some/response/topic");
// and take care of converting someResult to a JSON payload, otherwise we'll end up with a
final MappingJackson2MessageConverter messageConverter = new MappingJackson2MessageConverter() {
@Override
public Object fromMessage(Message message) throws JMSException, MessageConversionException {
return message instanceof ObjectMessage ? ((ObjectMessage) message).getObject() : super.fromMessage(message);
}
@Override
protected TextMessage mapToTextMessage(Object object, Session session, ObjectMapper objectMapper) throws JMSException, IOException {
final TextMessage rv = super.mapToTextMessage(object, session, objectMapper);
rv.setStringProperty("content-type", "application/json;charset=UTF-8");
return rv;
}
};
messageConverter.setTargetType(MessageType.TEXT);
// [2] to a queue
final SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setDestinationName("some.queue");
container.setMessageListener(messageListener);
container.setConnectionFactory(connectionFactory);
return container;
}
}
This creates a configuration for creating a JMS Message Queue who’s outcome is send to a topic in the same format as Springs SimpMessagingTemplate would create, allowing to send messages to an arbitrary service whose return values are in turn passed (among others) to listeners on a WebSocket. This prevents manually connecting return values from a service to SimpMessagingTemplate, for example by injecting SimpMessagingTemplate into the service and manually calling convert and send. The service can therefore be a simple bean, not knowing anything about Spring, Jms, STOMP or Websockets.
Filed in English posts, Java
|