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 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.Map
for getting access to all headers. -
A non-annotated element that is not one of the supported types (
Message
orSession
) 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;
}
}