Spring JMS Validate Messages using JSR-303 Bean Validation
In this tutorial we demonstrate how to validate JMS messages using JSR-303 Bean Validation Annotations. We also show how to create a custom external Validator
to validate input parameters of the JMS payload.
Project Structure
Let’s start by looking at the project structure.
Maven Dependencies
We use Apache Maven to manage our project dependencies. Make sure the following dependencies reside on the class-path. We use the JSR-303 Bean Validation reference implementation of hiberante
Make sure the org.hibernate:hibernate-validation
dependency resides on the class-path.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.memorynotfound.integration.jms.activemq</groupId>
<artifactId>custom-validations</artifactId>
<version>1.0.0-SNAPSHOT</version>
<url>https://memorynotfound.com</url>
<name>Spring Integration + ActiveMQ - ${project.artifactId}</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.7.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>activemq-broker</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.el</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
Spring JMS ActiveMQ Configuration
Spring Boot can automatically configure a ConnectionFactory
when it detects that ActiveMQ is available on the class-path. If the broker is present, an embedded broker is started and configured automatically (as long as no broker URL is specified through configuration). For convenience, we created and configured an embedded activeMQ server. The application.yml
file is located in the src/main/resources/
folder. This configuration file creates and configures an embedded ActiveMQ broker.
spring:
# Embedded ActiveMQ Configuration
activemq:
broker-url: vm://embedded?broker.persistent=false,useShutdownHook=false
in-memory: true
non-blocking-redelivery: true
packages:
trust-all: false
trusted: com.memorynotfound
pool:
block-if-full: true
block-if-full-timeout: -1
create-connection-on-startup: true
enabled: false
expiry-timeout: 0
idle-timeout: 30000
max-connections: 1
maximum-active-session-per-connection: 500
reconnect-on-exception: true
time-between-expiration-check: -1
use-anonymous-producers: true
# Spring JMS Settings
jms:
listener:
acknowledge-mode: auto
auto-startup: true
concurrency: 2
max-concurrency: 2
pub-sub-domain: false
template:
default-destination:
delivery-mode: non_persistent
priority: 100
qos-enabled: true
receive-timeout: 1000
time-to-live: 36000
# Logging configuration print only current thread and messages for tutorial purposes
logging:
pattern:
console: "[%thread]:%msg%n"
level:
- ".=info"
- "com.memorynotfound=debug"
- "org.springframework=info"
Spring ActiveMQ Configuration
The @EnableJms
enables JMS listener annotated endpoints that are created under the cover by JmsListenerContainerFactory
. The JmsListenerContainerFactory
is responsible to create the listener container responsible for a particular endpoint. The @EnableJms
annotation also enables detection of JmsListener
annotations on any Spring-managed beans in the container.
We register a DefaultMessageHandlerMethodFactory
and set a custom LocalValidatorFactoryBean
which we configure using the HibernateValidator
to enable the JSR-303
Bean Validations.
The MappingJackson2MessageConverter
uses Jackson
to convert messages to and from JSON.
package com.memorynotfound.integration;
import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jms.annotation.EnableJms;
import org.springframework.jms.annotation.JmsListenerConfigurer;
import org.springframework.jms.config.JmsListenerEndpointRegistrar;
import org.springframework.jms.support.converter.MappingJackson2MessageConverter;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.jms.support.converter.MessageType;
import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@EnableJms
@Configuration
public class ActiveMQConfig implements JmsListenerConfigurer {
public static final String ORDER_QUEUE = "order-queue";
@Override
public void configureJmsListeners(JmsListenerEndpointRegistrar registrar) {
registrar.setMessageHandlerMethodFactory(methodFactory());
}
@Bean
public DefaultMessageHandlerMethodFactory methodFactory() {
DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
factory.setValidator(validatorFactory());
return factory;
}
@Bean
public Validator validatorFactory(){
LocalValidatorFactoryBean factory = new LocalValidatorFactoryBean();
factory.setProviderClass(HibernateValidator.class);
return factory;
}
@Bean
public MessageConverter messageConverter() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setTargetType(MessageType.TEXT);
converter.setTypeIdPropertyName("_type");
return converter;
}
}
JSR-303 Bean Validation
We can use JSR-303
annotations. These annotations are used to validate properties of our Order
object.
package com.memorynotfound.integration;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
import java.math.BigDecimal;
public class Order implements Serializable {
@NotNull
private String from;
@NotNull
private String to;
private BigDecimal amount;
public Order() {
}
public Order(String from, String to, BigDecimal amount) {
this.from = from;
this.to = to;
this.amount = amount;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
@Override
public String toString() {
return "Order{" +
"from='" + from + '\'' +
", to='" + to + '\'' +
", amount=" + amount +
'}';
}
}
Custom External Bean Validation
If you don’t want the JSR-303
bean validation annotations on your object, you can also create an external Validator
. The following is an example implementation of the same as the above.
package com.memorynotfound.integration;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
public class OrderValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return clazz.isAssignableFrom(Order.class);
}
@Override
public void validate(Object target, Errors errors) {
Order order = (Order) target;
if (order == null){
errors.reject(null, "order cannot be null");
} else {
if (order.getFrom() == null){
errors.rejectValue("from", null, "from cannot be null");
}
if (order.getTo() == null){
errors.rejectValue("to", null, "to cannot be null");
}
if (order.getAmount() == null){
errors.rejectValue("amount", null, "to cannot be null");
}
}
}
}
You need to configure the custom Validator
in the DefaultMessageHandlerMethodFactory
.
@Bean
public DefaultMessageHandlerMethodFactory methodFactory() {
DefaultMessageHandlerMethodFactory factory = new DefaultMessageHandlerMethodFactory();
factory.setValidator(new OrderValidator());
return factory;
}
Sending Messages to a JMS Queue
Now we have configured the ActiveMQ message broker, we can start sending messages to an ActiveMQ Queue. We use the JmsTemplate
to send JMS messages to the queue. We simply need to pass in a destination
and message
arguments and the JmsTemplate
handles the rest.
package com.memorynotfound.integration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Service;
import static com.memorynotfound.integration.ActiveMQConfig.ORDER_QUEUE;
@Service
public class OrderSender {
private static Logger log = LoggerFactory.getLogger(OrderSender.class);
@Autowired
private JmsTemplate jmsTemplate;
public void sendQueue(Order order) {
log.info("sending with convertAndSend() to " + ORDER_QUEUE + " <" + order + ">");
jmsTemplate.convertAndSend(ORDER_QUEUE, order);
}
}
Receiving messages from a JMS Queue
The @JmsListener
annotation marks a method to be the target of a JMS message listener on the specified destination
. The @Valid
annotation specifies that the payload will be validated upon incoming request.
package com.memorynotfound.integration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.stereotype.Component;
import javax.validation.Valid;
import static com.memorynotfound.integration.ActiveMQConfig.ORDER_QUEUE;
@Component
public class OrderConsumer {
private static Logger log = LoggerFactory.getLogger(OrderConsumer.class);
@JmsListener(destination = ORDER_QUEUE)
public void receiveMessage(@Payload @Valid Order order) {
log.info("received <" + order + ">");
}
}
Spring JMS Validate Messages using JSR-303 Bean Validation
We bootstrap the application using Spring Boot. When the application is initialized, we simply send a couple of messages to a JMS queue and print the output to the console.
package com.memorynotfound.integration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;
@SpringBootApplication
public class Run implements ApplicationRunner {
private static Logger log = LoggerFactory.getLogger(Run.class);
@Autowired
private OrderSender orderSender;
@Override
public void run(ApplicationArguments applicationArguments) throws Exception {
log.info("Spring JMS Validate Messages using JSR-303 Bean Validation");
orderSender.sendQueue(new Order(null, "lilly", new BigDecimal(99)));
orderSender.sendQueue(new Order("me", "lukas", new BigDecimal(48)));
orderSender.sendQueue(new Order("me", "underwood", new BigDecimal(73)));
orderSender.sendQueue(new Order("me", "nancy", new BigDecimal(2)));
log.info("Waiting for all ActiveMQ JMS Messages to be consumed");
TimeUnit.SECONDS.sleep(3);
System.exit(-1);
}
public static void main(String[] args) throws Exception {
SpringApplication.run(Run.class, args);
}
}
Example Output
The previous application prints the following output to the console. The first instance has some invalid constraint validations and will produce a validation error. The latter are validated correctly and are handled correctly.
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.5.7.RELEASE)
...
Spring JMS Validate Messages using JSR-303 Bean Validation
sending with convertAndSend() to order-queue <Order{from='null', to='lilly', amount=99}>
sending with convertAndSend() to order-queue <Order{from='me', to='lukas', amount=48}>
sending with convertAndSend() to order-queue <Order{from='me', to='underwood', amount=73}>
sending with convertAndSend() to order-queue <Order{from='me', to='nancy', amount=2}>
Waiting for all ActiveMQ JMS Messages to be consumed
Execution of JMS message listener failed, and no ErrorHandler has been set.
org.springframework.jms.listener.adapter.ListenerExecutionFailedException: Listener method could not be invoked with incoming message
Endpoint handler details:
Method [public void com.memorynotfound.integration.OrderConsumer.receiveMessage(com.memorynotfound.integration.Order)]
Bean [com.memorynotfound.integration.OrderConsumer@5caf425e]
; nested exception is org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException: Could not resolve method parameter at index 0 in public void com.memorynotfound.integration.OrderConsumer.receiveMessage(com.memorynotfound.integration.Order): 1 error(s): [Field error in object 'order' on field 'from': rejected value [null]; codes [NotNull.order.from,NotNull.from,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [order.from,from]; arguments []; default message [from]]; default message [may not be null]] , failedMessage=org.springframework.jms.listener.adapter.AbstractAdaptableMessageListener$MessagingMessageConverterAdapter$LazyResolutionMessage@df4844b
at org.springframework.jms.listener.adapter.MessagingMessageListenerAdapter.invokeHandler(MessagingMessageListenerAdapter.java:108)
at org.springframework.jms.listener.adapter.MessagingMessageListenerAdapter.onMessage(MessagingMessageListenerAdapter.java:69)
at org.springframework.jms.listener.AbstractMessageListenerContainer.doInvokeListener(AbstractMessageListenerContainer.java:721)
at org.springframework.jms.listener.AbstractMessageListenerContainer.invokeListener(AbstractMessageListenerContainer.java:681)
at org.springframework.jms.listener.AbstractMessageListenerContainer.doExecuteListener(AbstractMessageListenerContainer.java:651)
at org.springframework.jms.listener.AbstractPollingMessageListenerContainer.doReceiveAndExecute(AbstractPollingMessageListenerContainer.java:317)
at org.springframework.jms.listener.AbstractPollingMessageListenerContainer.receiveAndExecute(AbstractPollingMessageListenerContainer.java:255)
at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.invokeListener(DefaultMessageListenerContainer.java:1166)
at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.executeOngoingLoop(DefaultMessageListenerContainer.java:1158)
at org.springframework.jms.listener.DefaultMessageListenerContainer$AsyncMessageListenerInvoker.run(DefaultMessageListenerContainer.java:1055)
at java.lang.Thread.run(Thread.java:745)
Caused by: org.springframework.messaging.handler.annotation.support.MethodArgumentNotValidException: Could not resolve method parameter at index 0 in public void com.memorynotfound.integration.OrderConsumer.receiveMessage(com.memorynotfound.integration.Order): 1 error(s): [Field error in object 'order' on field 'from': rejected value [null]; codes [NotNull.order.from,NotNull.from,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [order.from,from]; arguments []; default message [from]]; default message [may not be null]]
at org.springframework.messaging.handler.annotation.support.PayloadArgumentResolver.validate(PayloadArgumentResolver.java:201)
at org.springframework.messaging.handler.annotation.support.PayloadArgumentResolver.resolveArgument(PayloadArgumentResolver.java:129)
at org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:112)
at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:135)
at org.springframework.messaging.handler.invocation.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:107)
at org.springframework.jms.listener.adapter.MessagingMessageListenerAdapter.invokeHandler(MessagingMessageListenerAdapter.java:104)
... 10 common frames omitted
received <Order{from='me', to='underwood', amount=73}>
received <Order{from='me', to='lukas', amount=48}>
received <Order{from='me', to='nancy', amount=2}>
References
- ActiveMQ Official Website
- ActiveMQ API JavaDoc
- Spring Boot Common Application Properties
- Spring Boot ActiveMQ Documentation
- JmsTemplate JavaDoc
- @JmsListener JavaDoc
- Spring JMS Validation Documentation