I'm struggling with rewriting RabbitMQ application configuration from XML to Java. Sadly once the code is executed, quite general error appears:
org.springframework.amqp.rabbit.listener.exception.ListenerExecutionFailedException:
Failed to invoke target method 'receiveMessage' with
argument type = [class [B], value = [{[B#3bd0e47}]
...
Caused by: java.lang.NoSuchMethodException: com.mycompany.MessageListener.receiveMessage([B)
Application works if I base my configuration on XML, listed below.
I tried to rewrite it, basing on Spring Integration, AMQP, Rabbit documentation. Nevertheless, spring configuration documentation is mostly xml based, hence my question.
XML conf:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/integration/amqp
http://www.springframework.org/schema/integration/amqp/spring-integration-amqp.xsd
http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<rabbit:connection-factory id="connectionFactory" host="mycompany-host"
username="mycompany-username"
password="mycompany-password"
virtual-host="mycompany-vhost"/>
<rabbit:template id="mycompany-template" connection-factory="connectionFactory" />
<rabbit:admin id="admin" connection-factory="connectionFactory" />
<!-- ##### -->
<rabbit:queue id="queue-id" name="queue-name" declared-by="admin"/>
<rabbit:direct-exchange name="mycompany-incoming-events" declared-by="admin">
<rabbit:bindings>
<rabbit:binding queue="queue-name" key="" />
</rabbit:bindings>
</rabbit:direct-exchange>
<!-- ##### -->
<int-amqp:inbound-channel-adapter channel="mycompany-channel"
queue-names="queue-name" connection-factory="connectionFactory" />
<int:chain input-channel="mycompany-channel">
<int:transformer>
<bean class="com.mycompany.MyCompanyParser"/>
</int:transformer>
<int:filter expression="payload.header != null"/>
<int:transformer>
<bean class="com.mycompany.MyCompanyHeaderEnricher"/>
</int:transformer>
<int:recipient-list-router>
<int:recipient channel="dataSubmittedChannel"/>
</int:recipient-list-router>
</int:chain>
<int:chain input-channel="dataSubmittedChannel">
<int:filter expression="headers.mycompany_enriched_header.name().equals('MY_COMPANY_CONSTRAINT')" />
<int:service-activator>
<bean class="com.mycompany.MessageListener"/>
</int:service-activator>
</int:chain>
</beans>
Java listener:
#Component
public class MessageListener {
public void receiveMessage(final MyCompanyParsedType msg){
System.out.println(msg.toString());
}
}
After some rewriting I managed to came up with this Java based configuration:
import com.nxcs3.gamefetcher.configuration.SampleConfiguration;
import com.nxcs3.gamefetcher.listener.GameMessageListener;
import nxcs.drept.nxcs2events.EventHeadersEnricher;
import nxcs.drept.nxcs2events.EventParser;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter;
import org.springframework.integration.dsl.IntegrationFlow;
#SpringBootApplication
public class MyCompanySpringBootApp {
public static final String MESSAGE_QUEUE = "queue-name";
public static final String MESSAGE_EXCHANGE = "mycompany-incoming-events";
public static void main(String[] args) {
SpringApplication.run(MyCompanySpringBootApp.class);
}
#Bean
public DirectExchange exchange(){
return new DirectExchange(MESSAGE_EXCHANGE);
}
#Bean
public Queue queue(){
return new Queue(MESSAGE_QUEUE, true);
}
#Bean
public Binding binding(Queue queue){
return BindingBuilder.bind(queue).to(exchange()).with(MESSAGE_QUEUE);
}
#Bean
MessageListenerAdapter listenerAdapter(MessageListener receiver) {
return new MessageListenerAdapter(receiver, "receiveMessage");
}
#Bean
public IntegrationFlow flow(){
return f -> f.log()
.transform(new MyCompanyParser())
.filter("payload.header != null")
.transform(new MyCompanyHeaderEnricher())
.filter("headers.mycompany_enriched_header.name().equals('MY_COMPANY_CONSTRAINT')");
}
#Bean
SimpleMessageListenerContainer container(ConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames(MESSAGE_QUEUE);
container.setMessageListener(listenerAdapter);
return container;
}
}
I supply connection details through yaml.
As I mentioned previously, I clearly miss something.
Any ideas where did the configuration went wrong?
Added section after comments, proposed solution:
So I removed MessageListenerAdapter and replaced it using AmqpInboundChannelAdapter and #ServiceActivator
Result would look like:
#SpringBootApplication
public class MyCompanySpringBootApp {
public static final String MESSAGE_QUEUE = "queue-name";
public static final String MESSAGE_EXCHANGE = "mycompany-incoming-events";
public static void main(String[] args) {
SpringApplication.run(MyCompanySpringBootApp.class);
}
#Bean
public DirectExchange exchange(){
return new DirectExchange(MESSAGE_EXCHANGE);
}
#Bean
public Queue queue(){
return new Queue(MESSAGE_QUEUE, true);
}
#Bean
public Binding binding(Queue queue){
return BindingBuilder.bind(queue).to(exchange()).with(MESSAGE_QUEUE);
}
#Bean
public AmqpInboundChannelAdapter
channelAdapter(SimpleMessageListenerContainer container){
AmqpInboundChannelAdapter amqpInboundChannelAdapter = new
AmqpInboundChannelAdapter(container);
amqpInboundChannelAdapter.setOutputChannelName("adapter");
return amqpInboundChannelAdapter;
}
#Bean
public MessageListener handler(){
return new MessageListener();
}
#Bean
public IntegrationFlow flow(){
return f -> f.log()
.transform(new MyCompanyParser())
.filter("payload.header != null")
.transform(new MyCompanyHeaderEnricher())
.filter("headers.mycompany_enriched_header.name().equals('MY_COMPANY_CONSTRAINT')");
}
#Bean
SimpleMessageListenerContainer container(ConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) {
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames(MESSAGE_QUEUE);
container.setMessageListener(listenerAdapter);
return container;
}
}
and listener:
#Component
public class MessageListener {
#ServiceActivator(inputChannel = "adapter")
public void receiveMessage(final MyCompanyParsedType msg){
System.out.println(msg.toString());
}
}
Which brings us a little bit closer, because messages are being accepted and processed inside of receiveMessage method.
However somehow coming messages do not pass through IntegrationFlow filters. Messages appear to be totally random. I added imports
The MessageListenerAdapter uses a SimpleMessageConverter by default.
And its logic is based on the presence of the contentType property.
According to your error, that sounds like there is no this property in the consumed message, therefore it falls back to the message.getBody(), which is byte[] anyway.
You may consider to specify a desired MessageConverter into that MessageListenerAdapter, e.g. SerializerMessageConverter with the ignoreContentType = true.
Related
I think I am missing something here..I am trying to create simple rabbit listner which can accept custom object as message type. Now as per doc it says
In versions prior to 1.6, the type information to convert the JSON had to be provided in message headers, or a custom ClassMapper was required. Starting with version 1.6, if there are no type information headers, the type can be inferred from the target method arguments.
I am putting message manually in to queue using rabbit mq adm in dashboard,getting error like
Caused by: org.springframework.messaging.converter.MessageConversionException: Cannot convert from [[B] to [com.example.Customer] for GenericMessage [payload=byte[21], headers={amqp_receivedDeliveryMode=NON_PERSISTENT, amqp_receivedRoutingKey=customer, amqp_deliveryTag=1, amqp_consumerQueue=customer, amqp_redelivered=false, id=81e8a562-71aa-b430-df03-f60e6a37c5dc, amqp_consumerTag=amq.ctag-LQARUDrR6sUcn7FqAKKVDA, timestamp=1485635555742}]
My configuration:
#Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory("localhost");
connectionFactory.setUsername("test");
connectionFactory.setPassword("test1234");
connectionFactory.setVirtualHost("/");
return connectionFactory;
}
#Bean
RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
return rabbitTemplate;
}
#Bean
public AmqpAdmin amqpAdmin() {
RabbitAdmin rabbitAdmin = new RabbitAdmin(connectionFactory());
return rabbitAdmin;
}
#Bean
public Jackson2JsonMessageConverter jackson2JsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
Also question is with this exception message is not put back in the queue.
I am using spring boot 1.4 which brings amqp 1.6.1.
Edit1 : I added jackson converter as above (prob not required with spring boot) and given contenty type on rmq admin but still got below, as you can see above I am not configuring any listener container yet.
Caused by: org.springframework.messaging.converter.MessageConversionException: Cannot convert from [[B] to [com.example.Customer] for GenericMessage [payload=byte[21], headers={amqp_receivedDeliveryMode=NON_PERSISTENT, amqp_receivedRoutingKey=customer, content_type=application/json, amqp_deliveryTag=3, amqp_consumerQueue=customer, amqp_redelivered=false, id=7f84d49d-037a-9ea3-e936-ed5552d9f535, amqp_consumerTag=amq.ctag-YSemzbIW6Q8JGYUS70WWtA, timestamp=1485643437271}]
If you are using boot, you can simply add a Jackson2JsonMessageConverter #Bean to the configuration and it will be automatically wired into the listener (as long as it's the only converter). You need to set the content_type property to application/json if you are using the administration console to send the message.
Conversion errors are considered fatal by default because there is generally no reason to retry; otherwise they'd loop for ever.
EDIT
Here's a working boot app...
#SpringBootApplication
public class So41914665Application {
public static void main(String[] args) {
SpringApplication.run(So41914665Application.class, args);
}
#Bean
public Queue queue() {
return new Queue("foo", false, false, true);
}
#Bean
public Jackson2JsonMessageConverter converter() {
return new Jackson2JsonMessageConverter();
}
#RabbitListener(queues = "foo")
public void listen(Foo foo) {
System.out.println(foo);
}
public static class Foo {
public String bar;
public String getBar() {
return this.bar;
}
public void setBar(String bar) {
this.bar = bar;
}
#Override
public String toString() {
return "Foo [bar=" + this.bar + "]";
}
}
}
I sent this message
With this result:
2017-01-28 21:49:45.509 INFO 11453 --- [ main] com.example.So41914665Application : Started So41914665Application in 4.404 seconds (JVM running for 5.298)
Foo [bar=baz]
Boot will define an admin and template for you.
Ran into the same issue, turns out that, git stash/merge messed up with my config, I need to include this package again in my main again:
#SpringBootApplication(scanBasePackages = {
"com.example.amqp" // <- git merge messed this up
})
public class TeamActivityApplication {
public static void main(String[] args) {
SpringApplication.run(TeamActivityApplication.class, args);
}
}
I am new to rabbitmq. I am using spring-rabbit 1.3.5 Release.
I want to register multiple message listner. How to do that?
I can register a single message listner.
Here is my code:
1)Interface which extends MessageListner interface
public interface MessageQueueManager extends MessageListener{
public String createQueue(String queueName);
public void sendMessage(String message, String destinationQueueName) throws Exception;
}
2) Here is the implementation:
#Service("messageQueueManager")
public class MessageQueueManagerImpl implements MessageQueueManager {
#Autowired
private AmqpAdmin admin;
#Autowired
private AmqpTemplate template;
#Autowired
private ConnectionFactory connectionFactory;
#Autowired
private SimpleMessageListenerContainer container;
#Override
public void onMessage(Message message) {
// Different message can behave differently.
}
#Override
public String createQueue(String queueName) {
// survive a server restart
boolean durable = true;
// keep it even if nobody is using it
boolean autoDelete = false;
boolean exclusive = false;
// create queue
Queue newQueue = new Queue(queueName, durable, exclusive, autoDelete);
queueName = admin.declareQueue(newQueue);
// create binding with exchange
// Producer sends to an Exchange and a Consumer receives from a Queue, the bindings that connect Queues to Exchanges are critical for connecting those producers and consumers via messaging.
/*admin.declareBinding(new Binding(queueName, DestinationType.QUEUE,
"directExchange", queueName, new HashMap<String, Object>()));*/
Binding binding = BindingBuilder.bind(newQueue).to(DirectExchange.DEFAULT).with(queueName);
admin.declareBinding(binding);
// add queue to listener
container.addQueues(newQueue);
// start listener
container.start();
return queueName;
}
#Override
public void sendMessage(String message, String destinationQueueName)
throws Exception {
template.convertAndSend("directExchange", destinationQueueName,
MessageBuilder.withBody(message.getBytes()).build());
}
}
3)Listner register in applicationContext.xml file
<!-- Listener container for setting up concurrent listeners for queues -->
<bean id="simpleMessageListenerContainer"
class="org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer">
<constructor-arg index="0" ref="connectionFactory" />
<property name="missingQueuesFatal" value="false" />
<property name="concurrentConsumers" value="5" />
<property name="autoStartup" value="false" />
<property name="messageListener" ref="messageQueueManager" />
</bean>
So here SimpleMessageListenerContainer class can take only one messageListner. Do I need to declare multiple SimpleMessageListenerContainer instance to register different messageListner?
I want to register this class as a message listner.
#Service("myMessageListener")
public class MessageHandler implements MessageListener {
#Override
public void onMessage(Message message) {
log.info("Received message: " + message);
log.info("Text: " + new String(message.getBody()));
}
}
1)Register your queues:
<rabbit:queue id="spring.queue" auto-delete="false" durable="true" exclusive="false" name="spring.queue"/>
<rabbit:queue id="user.login.notification" auto-delete="false" durable="true" exclusive="false" name="user.login.notification"/>
2)Declare the bindings:
<rabbit:direct-exchange name="directExchange" auto-delete="false">
<rabbit:bindings>
<rabbit:binding queue="spring.queue" key="spring.queue" />
<rabbit:binding queue="user.login.notification" key="user.login.notification MAIYAM" />
</rabbit:bindings>
</rabbit:direct-exchange>
3)Tell the container to call onMessage(Message message) method when the any of the queue publishes the message.
<rabbit:listener-container
connection-factory="connectionFactory" acknowledge="auto" concurrency="10"
requeue-rejected="true">
<rabbit:listener ref="myMessageListener" queues="spring.queue" />
<rabbit:listener ref="messageQueueManager" queues="user.login.notification" />
</rabbit:listener-container>
4)Remove private SimpleMessageListenerContainer container; from MessageQueueManagerImpl class.
Now it should work.
Recently, I have been trying to implement an in-memory database based on HSQLDB for one of our applications which uses Oracle DB in the production. The application uses spring framework. However, I have to implement the data-source bean programmatically as we are using the existing SQL DDL statements(Oracle queries) and so have to programmatically remove constructs like namespaces before they can run on HSQLDB.
I initialize the database using EmbeddedDatabaseBuilder(ResourceLoader).
Now my issue is that I now want to add connection pooling using say c3p0 to this.
Normally I would be using
<bean id="c3p0DataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass" value="HSQLDB driver path" /> (this is just for representation)
<property name="jdbcUrl" value="${xxx.jdbcUrl}" />
<property name="user" value="${xxx.username}" />
<property name="password" value="${xxx.password}" />
<property name="minPoolSize" value="1" />
<property name="maxPoolSize" value="3" />
<property name="maxIdleTime" value="20" />
</bean>
However, I am confused as to how I can define this while using the Spring embedded database.
Disclaimer: I am really new to spring.
Following this link:
import com.mchange.v2.c3p0.ComboPooledDataSource;
public class C3P0Utils {
public static ComboPooledDataSource newDefaultDS() {
ComboPooledDataSource dataSource = new ComboPooledDataSource();
dataSource.setMinPoolSize(1);
dataSource.setMaxPoolSize(1);
dataSource.setMaxIdleTime(20);
return dataSource;
}
}
import java.beans.PropertyVetoException;
import java.sql.Driver;
import org.springframework.jdbc.datasource.embedded.ConnectionProperties;
import org.springframework.jdbc.datasource.embedded.DataSourceFactory;
import com.mchange.v2.c3p0.ComboPooledDataSource;
public class ComboPoolDataSourceFactory implements DataSourceFactory,
ConnectionProperties {
private final ComboPooledDataSource dataSource;
public ComboPoolDataSourceFactory() {
this(C3P0Utils.newDefaultDS());
}
public ComboPoolDataSourceFactory(ComboPooledDataSource dataSource) {
assert dataSource != null;
this.dataSource = dataSource;
}
public ConnectionProperties getConnectionProperties() {
return this;
}
public ComboPooledDataSource getDataSource() {
return dataSource;
}
public void setUsername(String username) {
dataSource.setUser(username);
}
public void setPassword(String password) {
dataSource.setPassword(password);
}
public void setUrl(String url) {
dataSource.setJdbcUrl(url);
}
public void setDriverClass(Class<? extends Driver> driverClass) {
try {
dataSource.setDriverClass(driverClass.getName());
} catch (PropertyVetoException e) {
e.printStackTrace();
}
}
}
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
public class EmbeddedC3P0DatabaseBuilder extends EmbeddedDatabaseBuilder {
public EmbeddedC3P0DatabaseBuilder() {
setDataSourceFactory(new ComboPoolDataSourceFactory());
}
}
And a short usage example:
EmbeddedC3P0DatabaseBuilder builder = new EmbeddedC3P0DatabaseBuilder();
EmbeddedDatabase db = builder
.setType(EmbeddedDatabaseType.H2)
.addScript("setup-tables.sql")
.build();
JdbcTemplate template = new JdbcTemplate(db);
....
db.shutdown();
I am new to RabbitMQ and I am trying to "receiveAndConvert" to a custom type of mine: Person.java.
Here is my producer:
public class Producer {
public static void main(String[] args) {
ApplicationContext context = new GenericXmlApplicationContext("classpath:/applicationContext.xml");
AmqpTemplate template = context.getBean(AmqpTemplate.class);
Person person = new Person();
person.setAge(37);
person.setName("Julien");
template.convertAndSend("myqueue", person);
}
}
and here is my consumer:
public class Consumer {
public static void main(String[] args) {
ApplicationContext context = new GenericXmlApplicationContext("classpath:/applicationContext.xml");
AmqpTemplate template = context.getBean(AmqpTemplate.class);
Person me = (Person) template.receiveAndConvert("myqueue");
System.out.println("Me: " + me.getName() + ":" + me.getAge());
}
}
My Person.java is just a POJO with a name and an age instance variables.
I get a ClassCastException as follows:
Exception in thread "main" java.lang.ClassCastException: [B cannot be cast to trc.suivi.amqp.Person
at trc.suivi.amqp.Consumer.main(Consumer.java:14)
Note that the Producer and Consumer classes are located in two different projects and I have copied/pasted the Person.java class over to the Consumer project.
My config is as follows:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation="http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit-1.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<rabbit:connection-factory id="connectionFactory" />
<rabbit:template id="amqpTemplate" connection-factory="connectionFactory" />
<rabbit:admin connection-factory="connectionFactory" />
<rabbit:queue name="myqueue" />
</beans>
It just occurred to me that my Person.java class needs to implement Serializable. It now works.
I want to test a JMS-worker included in my glassfish-application using arquillian (to have container-services). My Worker looks the following:
package queue.worker;
import javax.ejb.ActivationConfigProperty;
import javax.ejb.MessageDriven;
import javax.jms.MessageListener;
#MessageDriven(mappedName = "java:app/jms/MailQueue", activationConfig = {
#ActivationConfigProperty(propertyName = "acknowledgeMode", propertyValue = "Auto-acknowledge"),
#ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue") })
public class MailWorker implements MessageListener {
public MailWorker() {
}
#Override
public void onMessage(javax.jms.Message inMessage) {
}
}
This is the test:
package queueTest.worker;
import java.io.File;
import javax.inject.Inject;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.junit.Arquillian;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import queue.worker.MailWorker;
#RunWith(Arquillian.class)
public class MailWorkerTest {
#Deployment
public static WebArchive createDeployment() {
WebArchive archive = ShrinkWrap
.create(WebArchive.class)
.addClasses(MailWorker.class)
.addAsWebInfResource(new File("src/test/resources/WEB-INF/glassfish-resources.xml"),
"glassfish-resources.xml")
.addAsWebInfResource(new File("src/main/webapp/WEB-INF/beans.xml"), "beans.xml");
return archive;
}
#Inject
protected MailWorker mailWorker;
#Test
public void sendRegisterMail() {
Assert.assertTrue(true);
}
}
Executing this test, the Glassfish-JSM-Queue is started[1], but I get the following error:
org.jboss.weld.exceptions.DeploymentException: WELD-001408 Unsatisfied dependencies for type [MailWorker] with qualifiers [#Default] at injection point [[field] #Inject protected queueTest.worker.MailWorkerTest.mailWorker]
When I remove "#MessageDrivern[...]" at Mailworker.class and replace it with "#ApplicationScoped", e.g., everything works fine - so there seems to be not a problem with Arquillian in general, but JMS-related.
How can I test the JMS/Queue-Worker?
[1]
Dez 23, 2012 12:42:08 AM com.sun.messaging.jms.ra.ResourceAdapter start
Information: MQJMSRA_RA1101: GlassFish MQ JMS Resource Adapter starting: broker is EMBEDDED, connection mode is Direct
Dez 23, 2012 12:42:10 AM com.sun.messaging.jms.ra.ResourceAdapter start
Information: MQJMSRA_RA1101: GlassFish MQ JMS Resource Adapter Started:EMBEDDED
Testing MDBs is harder than testing usual EJBs and CDI beans as they are executed asynchronously. Even if you were able to inject them into your test, you could just test the onMessage() method by calling it synchronously.
My approach uses the MDB to only catch the message and to extract the underlying presentation (like String or Object). Then pass the extracted message to a separate CDI bean which has a test alternative.
#MessageDriven(mappedName = "jms/queue/example", activationConfig = {
#ActivationConfigProperty(propertyName = "destinationType",
propertyValue = "javax.jms.Queue"),
#ActivationConfigProperty(propertyName = "destination",
propertyValue = "jms/queue/example")
})
public class ExampleMDB implements MessageListener {
#Inject
private ExampleMessageHandler exampleMessageHandler;
#Override
public void onMessage(Message message) {
if (message instanceof TextMessage) {
TextMessage textMessage = (TextMessage) message;
try {
exampleMessageHandler.doSomething(textMessage.getText());
} catch (JMSException e) {
throw new RuntimeException("That was unexpected!", e);
}
}
}
}
The ExampleMessageHandler defines doSomething(String text).
For the test scope, we need an implementation that captures the arguments passed to doSomething() and makes them accessible to the test class. You can archieve this with the following implementation:
#Alternative
#ApplicationScoped
public class ExampleMessageHandlerTestable implements ExampleMessageHandler {
private BlockingQueue<String> queue = new LinkedBlockingQueue<String>();
public void doSomething(String text) {
queue.add(text);
}
public String poll(int secondsUntilInterrupt) throws InterruptedException {
return queue.poll(secondsUntilInterrupt, TimeUnit.SECONDS);
}
}
This is a CDI alternative to the real implementation used by the production code. Now just let the Arquillian test use this alternative. Here's the test class:
#RunWith(Arquillian.class)
public class ExampleMDBGoodTest {
#Resource(mappedName = "ConnectionFactory", name = "ConnectionFactory")
private ConnectionFactory connectionFactory;
#Resource(mappedName = "jms/queue/example", name = "jms/queue/example")
private Queue queue;
#Inject
private ExampleMessageHandler exampleMessageHandler;
#Deployment
public static WebArchive createDeployment() {
WebArchive archive = ShrinkWrap.create(WebArchive.class, "exampleMDB.war")
.addPackages(true, ExampleMDB.class.getPackage())
.addAsWebInfResource("hornetq-jms.xml", "hornetq-jms.xml")
.addAsWebInfResource("beans-alternative.xml", "beans.xml");
System.out.println(archive.toString(true));
return archive;
}
#Test
public void testOnMessage() throws Exception {
Connection connection = connectionFactory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer producer = session.createProducer(queue);
TextMessage textMessage = session.createTextMessage("Hello world!");
producer.send(textMessage);
session.close();
connection.close();
// We cast to our configured handler defined in beans.xml
ExampleMessageHandlerTestable testHandler =
(ExampleMessageHandlerTestable) exampleMessageHandler;
assertThat(testHandler.poll(10), is("Hello world!"));
}
}
Some explanations what's going on here: The test requests a JMS ConnectionFactory and the Queue on which the MDB listens. These create the JMS messages used by the MDB under test. Then we create a test deployment. The hornetq-jms.xml defines an adhoc queue for the test. By including beans-alternative.xml, we ensure that our test alternative is used by the MDB.
<beans xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/beans_1_0.xsd">
<alternatives>
<class>com.github.mcs.arquillian.mdb.example.ExampleMessageHandlerTestable</class>
</alternatives>
</beans>
The test case itself should be straight forward. A new JMS message is sent to the queue. Then we wait up to 10 seconds for a new message within our test alternative. By using a blocking queue, we can define a timeout after which the test fails. But the test itself finishes immediately as soon as the MDB calls the alternative bean.
I have uploaded a small Maven example project from where I copied the above code parts. Because I don't know much about Glassfish, it uses JBoss as managed container. Depending on the JBoss version you might use, you need to change the version of jboss-as-arquillian-container-managed.
Hope that helps someone :-)
MDBs are not eligible for injection in to other classes. You cannot inject them in to your test case.