Annotation-driven Listener Endpoints

The easiest way to receive a message asynchronously is to use the annotated listener endpoint infrastructure. In a nutshell, it lets you expose a method of a managed bean as a JMS listener endpoint. The following example shows how to use it:

@Component
public class MyService {

	@JmsListener(destination = "myDestination")
	public void processOrder(String data) { ... }
}

The idea of the preceding example is that, whenever a message is available on the jakarta.jms.Destination myDestination, the processOrder method is invoked accordingly (in this case, with the content of the JMS message, similar to what the MessageListenerAdapter provides).

The annotated endpoint infrastructure creates a message listener container behind the scenes for each annotated method, by using a JmsListenerContainerFactory. Such a container is not registered against the application context but can be easily located for management purposes by using the JmsListenerEndpointRegistry bean.

@JmsListener is a repeatable annotation on Java 8, so you can associate several JMS destinations with the same method by adding additional @JmsListener declarations to it.

Enable Listener Endpoint Annotations

To enable support for @JmsListener annotations, you can add @EnableJms to one of your @Configuration classes, as the following example shows:

@Configuration
@EnableJms
public class AppConfig {

	@Bean
	public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() {
		DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
		factory.setConnectionFactory(connectionFactory());
		factory.setDestinationResolver(destinationResolver());
		factory.setSessionTransacted(true);
		factory.setConcurrency("3-10");
		return factory;
	}
}

By default, the infrastructure looks for a bean named jmsListenerContainerFactory as the source for the factory to use to create message listener containers. In this case (and ignoring the JMS infrastructure setup), you can invoke the processOrder method with a core pool size of three threads and a maximum pool size of ten threads.

You can customize the listener container factory to use for each annotation or you can configure an explicit default by implementing the JmsListenerConfigurer interface. The default is required only if at least one endpoint is registered without a specific container factory. See the javadoc of classes that implement JmsListenerConfigurer for details and examples.

If you prefer XML configuration, you can use the <jms:annotation-driven> element, as the following example shows:

<jms:annotation-driven/>

<bean id="jmsListenerContainerFactory"
		class="org.springframework.jms.config.DefaultJmsListenerContainerFactory">
	<property name="connectionFactory" ref="connectionFactory"/>
	<property name="destinationResolver" ref="destinationResolver"/>
	<property name="sessionTransacted" value="true"/>
	<property name="concurrency" value="3-10"/>
</bean>

Programmatic Endpoint Registration

JmsListenerEndpoint provides a model of a JMS endpoint and is responsible for configuring the container for that model. The infrastructure lets you programmatically configure endpoints in addition to the ones that are detected by the JmsListener annotation. The following example shows how to do so:

@Configuration
@EnableJms
public class AppConfig implements JmsListenerConfigurer {

	@Override
	public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
		SimpleJmsListenerEndpoint endpoint = new SimpleJmsListenerEndpoint();
		endpoint.setId("myJmsEndpoint");
		endpoint.setDestination("anotherQueue");
		endpoint.setMessageListener(message -> {
			// processing
		});
		registrar.registerEndpoint(endpoint);
	}
}

In the preceding example, we used SimpleJmsListenerEndpoint, which provides the actual MessageListener to invoke. However, you could also build your own endpoint variant to describe a custom invocation mechanism.

Note that you could skip the use of @JmsListener altogether and programmatically register only your endpoints through JmsListenerConfigurer.

Annotated Endpoint Method Signature

So far, we have been injecting a simple String in our endpoint, but it can actually have a very flexible method signature. In the following example, we rewrite it to inject the Order with a custom header:

@Component
public class MyService {

	@JmsListener(destination = "myDestination")
	public void processOrder(Order order, @Header("order_type") String orderType) {
		...
	}
}

The main elements you can inject in JMS listener endpoints are as follows:

  • The raw jakarta.jms.Message or any of its subclasses (provided that it matches the incoming message type).

  • The jakarta.jms.Session for optional access to the native JMS API (for example, for sending a custom reply).

  • The org.springframework.messaging.Message that represents the incoming JMS message. Note that this message holds both the custom and the standard headers (as defined by JmsHeaders).

  • @Header-annotated method arguments to extract a specific header value, including standard JMS headers.

  • A @Headers-annotated argument that must also be assignable to java.util.Map for getting access to all headers.

  • A non-annotated element that is not one of the supported types (Message or Session) is considered to be the payload. You can make that explicit by annotating the parameter with @Payload. You can also turn on validation by adding an extra @Valid.

The ability to inject Spring’s Message abstraction is particularly useful to benefit from all the information stored in the transport-specific message without relying on transport-specific API. The following example shows how to do so:

@JmsListener(destination = "myDestination")
public void processOrder(Message<Order> order) { ... }

Handling of method arguments is provided by DefaultMessageHandlerMethodFactory, which you can further customize to support additional method arguments. You can customize the conversion and validation support there as well.

For instance, if we want to make sure our Order is valid before processing it, we can annotate the payload with @Valid and configure the necessary validator, as the following example shows:

@Configuration
@EnableJms
public class AppConfig implements JmsListenerConfigurer {

	@Override
	public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
		registrar.setMessageHandlerMethodFactory(myJmsHandlerMethodFactory());
	}

	@Bean
	public DefaultMessageHandlerMethodFactory myHandlerMethodFactory() {
		DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
		factory.setValidator(myValidator());
		return factory;
	}
}

Response Management

The existing support in MessageListenerAdapter already lets your method have a non-void return type. When that is the case, the result of the invocation is encapsulated in a jakarta.jms.Message, sent either in the destination specified in the JMSReplyTo header of the original message or in the default destination configured on the listener. You can now set that default destination by using the @SendTo annotation of the messaging abstraction.

Assuming that our processOrder method should now return an OrderStatus, we can write it to automatically send a response, as the following example shows:

@JmsListener(destination = "myDestination")
@SendTo("status")
public OrderStatus processOrder(Order order) {
	// order processing
	return status;
}
If you have several @JmsListener-annotated methods, you can also place the @SendTo annotation at the class level to share a default reply destination.

If you need to set additional headers in a transport-independent manner, you can return a Message instead, with a method similar to the following:

@JmsListener(destination = "myDestination")
@SendTo("status")
public Message<OrderStatus> processOrder(Order order) {
	// order processing
	return MessageBuilder
			.withPayload(status)
			.setHeader("code", 1234)
			.build();
}

If you need to compute the response destination at runtime, you can encapsulate your response in a JmsResponse instance that also provides the destination to use at runtime. We can rewrite the previous example as follows:

@JmsListener(destination = "myDestination")
public JmsResponse<Message<OrderStatus>> processOrder(Order order) {
	// order processing
	Message<OrderStatus> response = MessageBuilder
			.withPayload(status)
			.setHeader("code", 1234)
			.build();
	return JmsResponse.forQueue(response, "status");
}

Finally, if you need to specify some QoS values for the response such as the priority or the time to live, you can configure the JmsListenerContainerFactory accordingly, as the following example shows:

@Configuration
@EnableJms
public class AppConfig {

	@Bean
	public DefaultJmsListenerContainerFactory jmsListenerContainerFactory() {
		DefaultJmsListenerContainerFactory factory = new DefaultJmsListenerContainerFactory();
		factory.setConnectionFactory(connectionFactory());
		QosSettings replyQosSettings = new QosSettings();
		replyQosSettings.setPriority(2);
		replyQosSettings.setTimeToLive(10000);
		factory.setReplyQosSettings(replyQosSettings);
		return factory;
	}
}