In this post I will show how to run an embedded instance of ActiveMQ that uses SSL for communication. In the process I will also show how to configure a JMS client to use SSL.
The resulting project is available on GitHub.
I have omitted the keystore and truststore in this article – if you want to use the exact ones I have used in the example project, they are available in the GitHub repository.
Maven pom.xml File
The example project is a Maven project with the following pom.xml file.
<?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>se.ivankrizsan.activemq</groupId> <artifactId>embedded-activemq-ssl</artifactId> <version>1.0.0-SNAPSHOT</version> <properties> <spring.version>4.3.2.RELEASE</spring.version> <activemq.version>5.14.0</activemq.version> <junit.version>4.12</junit.version> <log4j.version>2.6.2</log4j.version> </properties> <dependencies> <dependency> <groupId>org.apache.activemq</groupId> <artifactId>activemq-spring</artifactId> <version>${activemq.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jms</artifactId> <version>${spring.version}</version> </dependency> <!-- Logging dependencies. --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>${log4j.version}</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>${log4j.version}</version> </dependency> <!-- Test dependencies. --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${spring.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build> </project>
Nothing fancy in here; Spring is used for the unit-test and for the JMS client, the unit-tests are implemented using JUnit4. Note that Java 8 is used.
Properties File
I am using a properties file to be able to easily configure the different aspects of JMS communication, such as keystore and truststore, JMS broker URL and so on. Note that there are two different JMS broker URLs; one used by the client and one by the embedded ActiveMQ broker. The reason for this is to make it easy to check whether the client can connect to the broker using a TCP connection, without SSL. I would not expect there being a need for two URL properties in a regular application. The default is for the client to use the SSL protocol.
# URL on which the (embedded) broker will expose a connector. JMS_BROKER_URL=ssl://localhost:61616 # Truststore used by the JMS broker and clients. JMS_BROKER_TRUSTSTORE=certs/truststore.jks JMS_BROKER_TRUSTSTORE_TYPE=JKS JMS_BROKER_TRUSTSTORE_PASSWORD=secret # Keystore used by JMS broker and clients. JMS_BROKER_KEYSTORE=certs/clientkeystore.jks JMS_BROKER_KEYSTORE_TYPE=JKS JMS_BROKER_KEYSTORE_PASSWORD=secret # JMS broker URL used by the client to connect to the JMS broker. CLIENT_JMS_BROKER_URL=ssl://localhost:61616
Read Properties Configuration
Since I want to be able to inject values from the property file above into my test class, as we will shortly see, I need some Spring configuration:
package se.ivankrizsan.configuration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.support.PropertySourcesPlaceholderConfigurer; import org.springframework.core.io.ClassPathResource; /** * Configuration that enables injection of configuration property values in Spring-based tests. * * @author Ivan Krizsan */ @Configuration public class ReadTestPropertiesConfiguration { /** * Bean that enables injection of configuration property values. * * @return PropertySourcesPlaceholderConfigurer bean. */ @Bean public static PropertySourcesPlaceholderConfigurer testProperties() { final PropertySourcesPlaceholderConfigurer theConfigurer = new PropertySourcesPlaceholderConfigurer(); theConfigurer.setLocation(new ClassPathResource("embedded-activemq-ssl.properties")); return theConfigurer; } }
Note that the method annotated with the @Bean annotation is static. The reason for this is to avoid the risk for lifecycle conflicts, as described in the @Bean annotation JavaDoc, under the Bootstrapping section.
JMS Configuration
The following class contains the JMS related Spring beans as well as the bean that starts the embedded ActiveMQ instance for the test.package se.ivankrizsan.configuration;
import org.apache.activemq.ActiveMQConnectionFactory; import org.apache.activemq.ActiveMQSslConnectionFactory; import org.apache.activemq.broker.BrokerService; import org.apache.activemq.broker.SslBrokerService; import org.apache.activemq.usage.MemoryUsage; import org.apache.activemq.usage.SystemUsage; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.DependsOn; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.jms.core.JmsTemplate; import javax.jms.ConnectionFactory; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import java.security.KeyStore; /** * Configuration of embedded ActiveMQ broker and related helpers used in tests. * * @author Ivan Krizsan */ @Configuration public class JmsTestConfiguration { /* Constant(s): */ /* Configuration parameter(s): */ @Value("${JMS_BROKER_URL}") protected String mJmsBrokerUrl; @Value("${JMS_BROKER_TRUSTSTORE}") protected String mJmsBrokerTruststore; @Value("${JMS_BROKER_TRUSTSTORE_TYPE}") protected String mJmsBrokerTruststoreType; @Value("${JMS_BROKER_TRUSTSTORE_PASSWORD}") protected String mJmsBrokerTruststorePassword; @Value("${JMS_BROKER_KEYSTORE}") protected String mJmsBrokerKeystore; @Value("${JMS_BROKER_KEYSTORE_TYPE}") protected String mJmsBrokerKeystoreType; @Value("${JMS_BROKER_KEYSTORE_PASSWORD}") protected String mJmsBrokerKeystorePassword; @Value(("${CLIENT_JMS_BROKER_URL}")) protected String mClientJmsBrokerUrl; /** * JMS template for tests. * * @return JMS template. */ @Bean public JmsTemplate jmsTestTemplate(final ConnectionFactory inConnectionFactory) { final JmsTemplate theJmsTestTemplate = new JmsTemplate(inConnectionFactory); theJmsTestTemplate.setReceiveTimeout(5000L); return theJmsTestTemplate; } /** * Client JMS connection factory. * Depending on the client JMS broker URL, a TCP or a SSL connection factory will be created. * * @return JMS connection factory. */ @Bean @DependsOn("embeddedBroker") public ConnectionFactory clientJMSConnectionFactory() { final ActiveMQConnectionFactory theConnectionFactory; if (mClientJmsBrokerUrl.startsWith("ssl")) { final ActiveMQSslConnectionFactory theSSLConnectionFactory = new ActiveMQSslConnectionFactory(mClientJmsBrokerUrl); try { theSSLConnectionFactory.setTrustStore(mJmsBrokerTruststore); theSSLConnectionFactory.setTrustStorePassword(mJmsBrokerTruststorePassword); theSSLConnectionFactory.setKeyStore(mJmsBrokerKeystore); theSSLConnectionFactory.setKeyStorePassword(mJmsBrokerKeystorePassword); theConnectionFactory = theSSLConnectionFactory; } catch (final Exception theException) { throw new Error(theException); } } else { theConnectionFactory = new ActiveMQConnectionFactory(mClientJmsBrokerUrl); } return theConnectionFactory; } /** * Embedded ActiveMQ broker for tests. * The embedded broker exposes only a SSL connector for clients. * * @return Embedded ActiveMQ broker. */ @Bean(initMethod = "start", destroyMethod = "stop") public BrokerService embeddedBroker() { SslBrokerService theActiveMqBroker; try { theActiveMqBroker = new SslBrokerService(); theActiveMqBroker.setUseJmx(false); theActiveMqBroker.setPersistent(false); theActiveMqBroker.setUseShutdownHook(true); /* Add ActiveMQ SSL connector using configured keystore and truststore. */ final KeyManager[] theKeystore = readKeystore(); final TrustManager[] theTruststore = readTruststore(); theActiveMqBroker.addSslConnector(mJmsBrokerUrl + "?transport.needClientAuth=true", theKeystore, theTruststore, null); /* Set memory limit in order not to use too much memory during tests. */ final MemoryUsage theActiveMqMemoryUsage = new MemoryUsage(); theActiveMqMemoryUsage.setPercentOfJvmHeap(20); final SystemUsage theActiveMqSystemUsage = new SystemUsage(); theActiveMqSystemUsage.setMemoryUsage(theActiveMqMemoryUsage); theActiveMqBroker.setSystemUsage(theActiveMqSystemUsage); } catch (final Exception theException) { throw new Error("An error occurred starting test ActiveMQ broker", theException); } return theActiveMqBroker; } private KeyManager[] readKeystore() throws Exception { final KeyManagerFactory theKeyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); final KeyStore theKeyStore = KeyStore.getInstance(mJmsBrokerKeystoreType); final Resource theKeystoreResource = new ClassPathResource(mJmsBrokerKeystore); theKeyStore.load(theKeystoreResource.getInputStream(), mJmsBrokerKeystorePassword.toCharArray()); theKeyManagerFactory.init(theKeyStore, mJmsBrokerKeystorePassword.toCharArray()); final KeyManager[] theKeystoreManagers = theKeyManagerFactory.getKeyManagers(); return theKeystoreManagers; } private TrustManager[] readTruststore() throws Exception { final KeyStore theTruststore = KeyStore.getInstance(mJmsBrokerTruststoreType); final Resource theTruststoreResource = new ClassPathResource(mJmsBrokerTruststore); theTruststore.load(theTruststoreResource.getInputStream(), null); final TrustManagerFactory theTrustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); theTrustManagerFactory.init(theTruststore); final TrustManager[] theTrustManagers = theTrustManagerFactory.getTrustManagers(); return theTrustManagers; } }
Note that the clientJMSConnectionFactory method creates different types of ActiveMQ connection factory depending on whether the client broker URL starts with “ssl” or not. For SSL communication, a keystore and truststore is configured on the connection factory. The JmsTemplate used by the client to send and receive messages does not require any special configuration to use SSL or TCP apart from the ActiveMQ connection factory – everything is encapsulated in the connection factory.
In addition, note how property values from the properties-file are injected using the @Value annotation.
Test Class
With the dependencies and all the configuration in place, we are now ready to implement the test. The test attempts to send a message to a queue on the broker and later receive the same message from the same queue.
package se.ivankrizsan.activemq; import org.apache.activemq.command.ActiveMQTextMessage; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jms.core.JmsTemplate; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import se.ivankrizsan.configuration.JmsTestConfiguration; import se.ivankrizsan.configuration.ReadTestPropertiesConfiguration; import javax.jms.Message; import javax.jms.TextMessage; /** * Tests connecting to the embedded ActiveMQ broker using SSL connections. * * @author Ivan Krizsan */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { ReadTestPropertiesConfiguration.class, JmsTestConfiguration.class }) @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_CLASS) public class EmbeddedActiveMQSSLTest { /** Class logger. */ private static final Logger LOGGER = LoggerFactory.getLogger(EmbeddedActiveMQSSLTest.class); /* Constant(s): */ protected final static String TEST_QUEUE = "testQueue"; protected final static String TEST_MESSAGE_STRING = "This is a text message!"; /* Instance variable(s): */ @Autowired protected JmsTemplate mJmsTestTemplate; /** * Tests sending and receiving a message to/from the broker. * Expected result: * It should be possible to send and receive one message to/from the JMS broker. * The payload of the received message should be the same as the payload of the sent message. * * @throws Exception If error occurs. Indicates test failure. */ @Test public void testSendAndReceiver() throws Exception { LOGGER.debug("About to send JMS message"); mJmsTestTemplate.send(TEST_QUEUE, (inSession) -> { final TextMessage theTextMessage = new ActiveMQTextMessage(); theTextMessage.setText(TEST_MESSAGE_STRING); return theTextMessage; }); LOGGER.debug("JMS message sent"); LOGGER.debug("About to receive JMS message"); final Message theMessage = mJmsTestTemplate.receive(TEST_QUEUE); LOGGER.debug("JMS message received"); Assert.assertNotNull("There should be a message on the queue", theMessage); Assert.assertTrue("The message should be a text message", theMessage instanceof TextMessage); final TextMessage theTextMessage = (TextMessage)theMessage; LOGGER.debug("Contents of received message: {}", theTextMessage.getText()); Assert.assertEquals("Message contents should match", TEST_MESSAGE_STRING, theTextMessage.getText()); } }
I would not print log in regular tests, but since this is an educational example there is some logging in this test class. The Spring JmsTemplate is used to send and receive a message.
Log4J Configuration File
For completeness, I will list the Log4J configuration file here.
<?xml version="1.0" encoding="UTF-8"?> <Configuration> <Appenders> <Console name="STDOUT" target="SYSTEM_OUT"> <PatternLayout pattern="%d %-5p %-30C - %m%n"/> </Console> </Appenders> <Loggers> <Logger name="se.ivankrizsan" level="debug"/> <Logger name="org.springframework" level="info"/> <Root level="info"> <AppenderRef ref="STDOUT"/> </Root> </Loggers> </Configuration>
Running the Example
If we now run the test, output similar to the following should be output to the console:
INFO: Loading properties file from class path resource [embedded-activemq-ssl.properties] 2016-08-15 08:26:02,280 INFO org.apache.activemq.broker.BrokerService - Using Persistence Adapter: MemoryPersistenceAdapter 2016-08-15 08:26:02,413 INFO org.apache.activemq.broker.BrokerService - Apache ActiveMQ 5.14.0 (localhost, ID:computer.lan:1) is starting 2016-08-15 08:26:02,417 INFO org.apache.activemq.transport.TransportServerThreadSupport - Listening for connections at: ssl://localhost:61616?transport.needClientAuth=true 2016-08-15 08:26:02,418 INFO org.apache.activemq.broker.TransportConnector - Connector ssl://localhost:61616?transport.needClientAuth=true started 2016-08-15 08:26:02,418 INFO org.apache.activemq.broker.BrokerService - Apache ActiveMQ 5.14.0 (localhost, ID:computer.lan:1) started 2016-08-15 08:26:02,418 INFO org.apache.activemq.broker.BrokerService - For help or more information please see: http://activemq.apache.org2016-08-15 08:26:02,529 DEBUG se.ivankrizsan.activemq.EmbeddedActiveMQSSLTest - About to send JMS message 2016-08-15 08:26:02,669 DEBUG se.ivankrizsan.activemq.EmbeddedActiveMQSSLTest - JMS message sent 2016-08-15 08:26:02,670 DEBUG se.ivankrizsan.activemq.EmbeddedActiveMQSSLTest - About to receive JMS message 2016-08-15 08:26:02,708 DEBUG se.ivankrizsan.activemq.EmbeddedActiveMQSSLTest - JMS message received 2016-08-15 08:26:02,709 DEBUG se.ivankrizsan.activemq.EmbeddedActiveMQSSLTest - Contents of received message: This is a text message! 2016-08-15 08:26:02,711 INFO org.apache.activemq.broker.BrokerService - Apache ActiveMQ 5.14.0 (localhost, ID:computer.lan:1) is shutting down 2016-08-15 08:26:02,711 INFO org.apache.activemq.broker.TransportConnector - Connector ssl://localhost:61616?transport.needClientAuth=true stopped Aug 15, 2016 8:26:02 AM org.springframework.context.support.GenericApplicationContext doClose INFO: Closing org.springframework.context.support.GenericApplicationContext@9353778: startup date [Mon Aug 15 08:26:01 CEST 2016]; root of context hierarchy 2016-08-15 08:26:02,716 INFO org.apache.activemq.broker.BrokerService - Apache ActiveMQ 5.14.0 (localhost, ID:computer.lan:1) uptime 0.444 seconds 2016-08-15 08:26:02,717 INFO org.apache.activemq.broker.BrokerService - Apache ActiveMQ 5.14.0 (localhost, ID:computer.lan:1) is shutdown Process finished with exit code 0
The test should pass and we can see from the log that a JMS message was sent and a JMS message was received, as expected.
If we now modify the property CLIENT_JMS_BROKER_URL in the properties file to use plain TCP, like this:
CLIENT_JMS_BROKER_URL=tcp://localhost:61616
Then we re-run the test, which should now fail:
2016-08-15 11:39:49,935 ERROR org.apache.activemq.broker.TransportConnector$1 - Could not accept connection from tcp://127.0.0.1:53050 : javax.net.ssl.SSLException: Unsupported record version Unknown-0.1 Aug 15, 2016 11:39:49 AM org.springframework.context.support.GenericApplicationContext doClose INFO: Closing org.springframework.context.support.GenericApplicationContext@9353778: startup date [Mon Aug 15 11:39:49 CEST 2016]; root of context hierarchy org.springframework.jms.UncategorizedJmsException: Uncategorized exception occurred during JMS processing; nested exception is javax.jms.JMSException: Cannot send, channel has already failed: tcp://127.0.0.1:61616
Thus we have created an embedded ActiveMQ broker which only accepts connections over SSL.
Happy coding!
Thanks!
This was really helpful!!