|
For the latest stable version, please use Spring Framework 6.2.12! |
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.Messageor any of its subclasses (provided that it matches the incoming message type). -
The
jakarta.jms.Sessionfor optional access to the native JMS API (for example, for sending a custom reply). -
The
org.springframework.messaging.Messagethat represents the incoming JMS message. Note that this message holds both the custom and the standard headers (as defined byJmsHeaders). -
@Header-annotated method arguments to extract a specific header value, including standard JMS headers. -
A
@Headers-annotated argument that must also be assignable tojava.util.Mapfor getting access to all headers. -
A non-annotated element that is not one of the supported types (
MessageorSession) 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;
}
}