1

I have a need to apply some pre-checks and common steps on the all the jms listeners like validating the raw message against a schema (JSON schema). Example -

@Component
public class MyService {

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

Now, before the spring converts the Message from the queue to Order, I need to do the following -

  1. log the original message with headers into a custom logger.
  2. validate the json message (text message) against a json schema (lets assume I have only one schema here for the simplicity)
  3. If schema validation fail, log the error and throw exception
  4. If schema validation passes, continue to control to spring to do the conversion and continue with the process order method.

Does the spring JMS architecture provides any way to inject the above need? I know AOP crosses the mind, but I am not sure will it work with @JmsListener.

Arpit
  • 92
  • 3
  • 15

1 Answers1

1

A rather simple technique would be to set autoStartup to false on the listener container factory.

Then, use the JmsListenerEndpointRegistry bean to get the listener container.

Then getMessageListener(), wrap it in an AOP proxy and setMessageListener().

Then start the container.

There might be a more elegant way, but I think you'd have to get into the guts of the listener creation code, which is quite involved.

EDIT

Example with Spring Boot:

@SpringBootApplication
public class So49682934Application {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    public static void main(String[] args) {
        SpringApplication.run(So49682934Application.class, args);
    }

    @JmsListener(id = "listener1", destination = "so49682934")
    public void listen(Foo foo) {
        logger.info(foo.toString());
    }

    @Bean
    public ApplicationRunner runner(JmsListenerEndpointRegistry registry, JmsTemplate template) {
        return args -> {
            DefaultMessageListenerContainer container =
                    (DefaultMessageListenerContainer) registry.getListenerContainer("listener1");
            Object listener = container.getMessageListener();
            ProxyFactory pf = new ProxyFactory(listener);
            NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor(new MyJmsInterceptor());
            advisor.addMethodName("onMessage");
            pf.addAdvisor(advisor);
            container.setMessageListener(pf.getProxy());
            registry.start();
            Thread.sleep(5_000);
            Foo foo = new Foo("baz");
            template.convertAndSend("so49682934", foo);
        };
    }

    @Bean
    public MessageConverter converter() {
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        converter.setTargetType(MessageType.TEXT);
        converter.setTypeIdPropertyName("typeId");
        return converter;
    }

    public static class MyJmsInterceptor implements MethodInterceptor {

        private final Logger logger = LoggerFactory.getLogger(getClass());

        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            Message message = (Message) invocation.getArguments()[0];
            logger.info(message.toString());
            // validate
            return invocation.proceed();
        }

    }

    public static class Foo {

        private String bar;

        public Foo() {
            super();
        }

        public Foo(String bar) {
            this.bar = bar;
        }

        public String getBar() {
            return this.bar;
        }

        public void setBar(String bar) {
            this.bar = bar;
        }

        @Override
        public String toString() {
            return "Foo [bar=" + this.bar + "]";
        }

    }

}

and

spring.jms.listener.auto-startup=false

and

m2018-04-06 11:42:04.859 INFO 59745 --- [enerContainer-1] e.So49682934Application$MyJmsInterceptor : ActiveMQTextMessage {commandId = 5, responseRequired = true, messageId = ID:gollum.local-60138-1523029319662-4:2:1:1:1, originalDestination = null, originalTransactionId = null, producerId = ID:gollum.local-60138-1523029319662-4:2:1:1, destination = queue://so49682934, transactionId = null, expiration = 0, timestamp = 1523029324849, arrival = 0, brokerInTime = 1523029324849, brokerOutTime = 1523029324853, correlationId = null, replyTo = null, persistent = true, type = null, priority = 4, groupID = null, groupSequence = 0, targetConsumerId = null, compressed = false, userID = null, content = null, marshalledProperties = null, dataStructure = null, redeliveryCounter = 0, size = 1050, properties = {typeId=com.example.So49682934Application$Foo}, readOnlyProperties = true, readOnlyBody = true, droppable = false, jmsXGroupFirstForConsumer = false, text = {"bar":"baz"}}

2018-04-06 11:42:04.882 INFO 59745 --- [enerContainer-1] ication$$EnhancerBySpringCGLIB$$e29327b8 : Foo [bar=baz]

EDIT2

Here's how to do it via infrastructure...

@SpringBootApplication
@EnableJms
public class So496829341Application {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    public static void main(String[] args) {
        SpringApplication.run(So496829341Application.class, args);
    }

    @JmsListener(id = "listen1", destination="so496829341")
    public void listen(Foo foo) {
        logger.info(foo.toString());
    }

    @Bean
    public ApplicationRunner runner(JmsTemplate template) {
        return args -> {
            Thread.sleep(5_000);
            template.convertAndSend("so496829341", new Foo("baz"));
        };
    }

    @Bean
    public MessageConverter converter() {
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        converter.setTargetType(MessageType.TEXT);
        converter.setTypeIdPropertyName("typeId");
        return converter;
    }

    @Bean(JmsListenerConfigUtils.JMS_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)
    public static JmsListenerAnnotationBeanPostProcessor bpp() {
        return new JmsListenerAnnotationBeanPostProcessor() {

            @Override
            protected MethodJmsListenerEndpoint createMethodJmsListenerEndpoint() {
                return new MethodJmsListenerEndpoint() {

                    @Override
                    protected MessagingMessageListenerAdapter createMessageListener(
                            MessageListenerContainer container) {
                        MessagingMessageListenerAdapter listener = super.createMessageListener(container);
                        ProxyFactory pf = new ProxyFactory(listener);
                        pf.setProxyTargetClass(true);
                        NameMatchMethodPointcutAdvisor advisor = new NameMatchMethodPointcutAdvisor(new MyJmsInterceptor());
                        advisor.addMethodName("onMessage");
                        pf.addAdvisor(advisor);
                        return (MessagingMessageListenerAdapter) pf.getProxy();
                    }

                };
            }

        };
    }

    public static class MyJmsInterceptor implements MethodInterceptor {

        private final Logger logger = LoggerFactory.getLogger(getClass());

        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            Message message = (Message) invocation.getArguments()[0];
            logger.info(message.toString());
            // validate
            return invocation.proceed();
        }

    }

    public static class Foo {

        private String bar;

        public Foo() {
            super();
        }

        public Foo(String bar) {
            this.bar = bar;
        }

        public String getBar() {
            return this.bar;
        }

        public void setBar(String bar) {
            this.bar = bar;
        }

        @Override
        public String toString() {
            return "Foo [bar=" + this.bar + "]";
        }

    }

}

Note: the BPP must be static and @EnableJms is required since the presence of this BPP disables boot's.

2018-04-06 13:44:41.607 INFO 82669 --- [enerContainer-1] .So496829341Application$MyJmsInterceptor : ActiveMQTextMessage {commandId = 5, responseRequired = true, messageId = ID:gollum.local-63685-1523036676402-4:2:1:1:1, originalDestination = null, originalTransactionId = null, producerId = ID:gollum.local-63685-1523036676402-4:2:1:1, destination = queue://so496829341, transactionId = null, expiration = 0, timestamp = 1523036681598, arrival = 0, brokerInTime = 1523036681598, brokerOutTime = 1523036681602, correlationId = null, replyTo = null, persistent = true, type = null, priority = 4, groupID = null, groupSequence = 0, targetConsumerId = null, compressed = false, userID = null, content = null, marshalledProperties = null, dataStructure = null, redeliveryCounter = 0, size = 1050, properties = {typeId=com.example.So496829341Application$Foo}, readOnlyProperties = true, readOnlyBody = true, droppable = false, jmsXGroupFirstForConsumer = false, text = {"bar":"baz"}}

2018-04-06 13:44:41.634 INFO 82669 --- [enerContainer-1] ication$$EnhancerBySpringCGLIB$$9ff4b13f : Foo [bar=baz]

EDIT3

Avoiding AOP...

@SpringBootApplication
@EnableJms
public class So496829341Application {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    public static void main(String[] args) {
        SpringApplication.run(So496829341Application.class, args);
    }

    @JmsListener(id = "listen1", destination="so496829341")
    public void listen(Foo foo) {
        logger.info(foo.toString());
    }

    @Bean
    public ApplicationRunner runner(JmsTemplate template) {
        return args -> {
            Thread.sleep(5_000);
            template.convertAndSend("so496829341", new Foo("baz"));
        };
    }

    @Bean
    public MessageConverter converter() {
        MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
        converter.setTargetType(MessageType.TEXT);
        converter.setTypeIdPropertyName("typeId");
        return converter;
    }

    @Bean(JmsListenerConfigUtils.JMS_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)
    public static JmsListenerAnnotationBeanPostProcessor bpp() {
        return new JmsListenerAnnotationBeanPostProcessor() {

            @Override
            protected MethodJmsListenerEndpoint createMethodJmsListenerEndpoint() {
                return new MethodJmsListenerEndpoint() {

                    @Override
                    protected MessagingMessageListenerAdapter createMessageListener(
                            MessageListenerContainer container) {
                        final MessagingMessageListenerAdapter listener = super.createMessageListener(container);
                        return new MessagingMessageListenerAdapter() {

                            @Override
                            public void onMessage(Message jmsMessage, Session session) throws JMSException {
                                logger.info(jmsMessage.toString());
                                // validate
                                listener.onMessage(jmsMessage, session);
                            }

                        };
                    }

                };
            }

        };
    }

    public static class Foo {

        private String bar;

        public Foo() {
            super();
        }

        public Foo(String bar) {
            this.bar = bar;
        }

        public String getBar() {
            return this.bar;
        }

        public void setBar(String bar) {
            this.bar = bar;
        }

        @Override
        public String toString() {
            return "Foo [bar=" + this.bar + "]";
        }

    }

}

EDIT4

To access other annotations on the listener method, it can be done, but reflection is needed to get a reference to the Method...

@JmsListener(id = "listen1", destination="so496829341")
@Schema("foo.bar")
public void listen(Foo foo) {
    logger.info(foo.toString());
}

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Schema {

    String value();

}

@Bean(JmsListenerConfigUtils.JMS_LISTENER_ANNOTATION_PROCESSOR_BEAN_NAME)
public static JmsListenerAnnotationBeanPostProcessor bpp() {
    return new JmsListenerAnnotationBeanPostProcessor() {

        @Override
        protected MethodJmsListenerEndpoint createMethodJmsListenerEndpoint() {
            return new MethodJmsListenerEndpoint() {

                @Override
                protected MessagingMessageListenerAdapter createMessageListener(
                        MessageListenerContainer container) {
                    final MessagingMessageListenerAdapter listener = super.createMessageListener(container);
                    InvocableHandlerMethod handlerMethod =
                            (InvocableHandlerMethod) new DirectFieldAccessor(listener)
                                    .getPropertyValue("handlerMethod");
                    final Schema schema = AnnotationUtils.getAnnotation(handlerMethod.getMethod(), Schema.class);
                    return new MessagingMessageListenerAdapter() {

                        @Override
                        public void onMessage(Message jmsMessage, Session session) throws JMSException {
                            logger.info(jmsMessage.toString());
                            logger.info(schema.value());
                            // validate
                            listener.onMessage(jmsMessage, session);
                        }

                    };
                }

            };
        }

    };
}
Community
  • 1
  • 1
Gary Russell
  • 166,535
  • 14
  • 146
  • 179
  • Can you share any documentation or example to do so? Also, Isn't there a way where I can extend the functionality of @JmsListener? – Arpit Apr 06 '18 at 14:58
  • See my edit for the simple solution; yes, you can extend `@JmsListener` but it's quite involved - you would have to extend the `JmsListenerAnnotationBeanPostProcessor`, and implement a custom `JmsListenerEndpoint` (probably a subclass of the standard `MethodJmsListenerEndpoint`). – Gary Russell Apr 06 '18 at 15:48
  • I made a second edit to show how to override the `@JmsListener` bean post processor instead. And a third edit to do the same, but avoiding AOP; just wrap the adapter in another adapter. – Gary Russell Apr 06 '18 at 18:47
  • Thanks for the code, I will try it and let you know. Can you help me the documentation or tutorial which can provide this level of insight of implementation? – Arpit Apr 08 '18 at 01:40
  • It's not really documented beyond what's in the [reference manual](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/integration.html#jms-annotated). This is rather an advanced technique; I just happen to have familiarity with it due to my work with similar functionality in spring-amqp and spring-kafka, where I am the project lead. – Gary Russell Apr 08 '18 at 01:59
  • I am not the lead on Spring JMS; I suggest you open a new feature [JIRA Issue](https://jira.spring.io/browse/SPR) against Spring Framework. `@RabbitListener` already has mechanisms for this (`afterReceivePostProcessors` and an `adviceChain`). But I agree it would be nice to have a consistent mechanism across all projects. – Gary Russell Apr 10 '18 at 18:40
  • Thanks Gary, The edit 2 works for me as it makes a general applicable to all the listener. Since you are lead on the projects, I would like to make a request as this are causing same issues with lots of project - 1. Easy way for above code to inject pre or post processing to JmsListener 2. JmsListener is unable to provide loose coupling with MOM, example spring provides various annotation JmsListener, RabbitListener, KafkaListener. Therefore we have to create custom framework on top of Spring jms to make codebase technology independent. If spring can provide loose coupling it will be great. – Arpit Apr 10 '18 at 18:48
  • Is there a way to get other annotations on the method on which JmsListener is annotated. In your edit 3, I want to pass the schema details and other information to be used in the custom onmessage method. – Arpit Apr 11 '18 at 13:59
  • It can be done with a bit of reflection - see EDIT4. – Gary Russell Apr 11 '18 at 14:29
  • Awesome you are so helpful, I was looking for accessing the InvocableHandlerMethod without using Reflection. If reflection is the only way then will use it. Thanks – Arpit Apr 11 '18 at 15:08
  • Why do we have to created the BPP bean as static ? Is there a way where we can make it work without static. I wanted to add more functionality where I need to access more beans. – Arpit May 30 '18 at 18:55
  • Making it static means it will be registered earlier (BPPs have to be registered before any beans they might process). It might work, but YMMV. You can pass in other beans to the factory method: `public static JmsListenerAnnotationBeanPostProcessor bpp(SomeOtherBean bean) {` – Gary Russell May 30 '18 at 19:28
  • Oh Yes, I am able to pass beans. Other thing I found is that If i move the BPP definition into another configuration class which doesn't has @SpringBoot annotation, it is not invoked or registered. Why is it behaving like this? – Arpit May 30 '18 at 20:31