In this post we are going to look at how we can set-up a client (build using Spring) that uses the JBoss JMS provider HornetQ remotely.

Remoting

Let us first look at how we can obtain objects remotely. In the Fun with JBoss post we have created an application that contains a stateless enterprise bean. This gives us a nice playground to call this stateless enterprise bean remotely. In order to do this we can use the following program

package test;

import model.entities.Person;
import model.logic.Company;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Properties;
import java.util.Random;

public class Test {
    public static void main(String[] args) {
        Properties properties = new Properties();
        properties.put(Context.INITIAL_CONTEXT_FACTORY, "org.jboss.naming.remote.client.InitialContextFactory");
        properties.put(Context.PROVIDER_URL, "remote://192.168.1.150:4447");
        properties.put(Context.SECURITY_PRINCIPAL, "employee");
        properties.put(Context.SECURITY_CREDENTIALS, "welcome1");
        properties.put("jboss.naming.client.ejb.context", true);

        try {
            Context context = new InitialContext(properties);
            Company company = (Company) context.lookup("LoadTest6/Model/Company!model.logic.Company");
            company.insertPerson(createPerson());
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }

    private static Person createPerson() {
        Person person = new Person();
        Random generator = new Random();
        person.setNaam(Long.toString(Math.abs(generator.nextLong()), 36));
        person.setSofinummer(1);
        return person;
    }
}

There are some points worth mentioning:

  • We need jboss-client-7.1.0.Final.jar in the client’s class path. The jar file can be obtained from the $JBOSS_HOME/bin/client directory of a JBoss AS7.1 distribution.
  • An application user must be created. This can be accomplished by using add-user.sh, located in the $JBOSS_HOME/bin directory. Detailed steps can be found in the Building a Coherence Cluster with Multiple Application Servers post. This user is then used to set the principal and credentials of the naming context that will be set-up.
  • Only JNDI objects prepended by java:jboss/exported/ can be obtained remotely. In the example, in which we obtain a stateless enterprise bean remotely, the object is bound as follows: app-name/module-name/bean-name!bean-interface in which,
    • app-name: the name of the .ear (without the .ear suffix) or the application name configured via application.xml deployment descriptor. If the application is not packaged in an .ear then there will be no app-name part to the JNDI string.
    • module-name: the name of the .jar or .war (without the .jar/.war suffix) in which the bean is deployed or the module-name configured in web.xml/ejb-jar.xml of the deployment. The module name is mandatory in the JNDI string.
    • bean-name: the name of the bean which by default is the simple name of the bean implementation class. It can be overridden either by using the name attribute of the bean defining annotation (@Stateless(name = "Company") in this case) or the ejb-jar.xml deployment descriptor.
    • bean-interface: the fully qualified class name of the interface being exposed by the bean.

When this is all to confusing for comfort, the logging will help, as something like the following output will be present when an application is deployed that contains enterprise beans:

16:50:52,840 INFO  [org.jboss.as.ejb3.deployment.processors.EjbJndiBindingsDeploymentUnitProcessor] (MSC service thread 1-3) JNDI bindings for session bean named Company in deployment unit subdeployment "Model.jar" of deployment "LoadTest6.ear" are as follows:

java:global/LoadTest6/Model/Company!model.logic.Company
java:app/Model/Company!model.logic.Company
java:module/Company!model.logic.Company
java:jboss/exported/LoadTest6/Model/Company!model.logic.Company
java:global/LoadTest6/Model/Company
java:app/Model/Company
java:module/Company

Here, we see which objects are bound under the java:jboss/exported/ name-space that can be obtained remotely. In our example, we can use context.lookup("LoadTest6/Model/Company!model.logic.Company") to obtain an instance of the enterprise bean. Note that we must not include java:jboss/exported/, as the remote-naming project expects it to always be relative to the java:jboss/exported/ name-space.

Set-up a JBoss JMS Provider

In the domain configuration file, we have the following

<domain xmlns="urn:jboss:domain:1.1">

    <extensions>
		...
        <extension module="org.jboss.as.messaging"/>
		...
    </extensions>

	<profiles>
		<profile name="cluster">
			...
		    <subsystem xmlns="urn:jboss:domain:messaging:1.1">
                <hornetq-server>
                    <persistence-enabled>true</persistence-enabled>
                    <journal-file-size>102400</journal-file-size>
                    <journal-min-files>2</journal-min-files>

                    <connectors>
                        <netty-connector name="netty" socket-binding="messaging"/>
						...
                    </connectors>

                    <acceptors>
                        <netty-acceptor name="netty" socket-binding="messaging"/>
						...
                    </acceptors>

                    <security-settings>
                        <security-setting match="#">
                            <permission type="send" roles="guest"/>
                            <permission type="consume" roles="guest"/>
                            <permission type="createNonDurableQueue" roles="guest"/>
                            <permission type="deleteNonDurableQueue" roles="guest"/>
                        </security-setting>
                    </security-settings>

					...

                    <jms-connection-factories>
						...
                        <connection-factory name="RemoteConnectionFactory">
                            <connectors>
                                <connector-ref connector-name="netty"/>
                            </connectors>
                            <entries>
                                <entry name="RemoteConnectionFactory"/>
                                <entry name="java:jboss/exported/jms/RemoteConnectionFactory"/>
                            </entries>
                        </connection-factory>
						...
                    </jms-connection-factories>

                    <jms-destinations>
                        <jms-queue name="testQueue">
                            <entry name="java:/queue/test"/>
                            <entry name="java:jboss/exported/jms/queue/test"/>
                        </jms-queue>
						...
                    </jms-destinations>
                </hornetq-server>
            </subsystem>
		</profile>
	</profiles>

	<interfaces>
        <interface name="management"/>
        <interface name="public"/>
        <interface name="unsecure"/>
    </interfaces>

    <socket-binding-groups>
        <socket-binding-group name="cluster-sockets" default-interface="public">
			...
            <socket-binding name="messaging" port="5445"/>
			...
        </socket-binding-group>
    </socket-binding-groups>

    <deployments>
        <deployment name="LoadTest6.ear" runtime-name="LoadTest6.ear">
            <content sha1="161f51dde7f085c822cc4c68b306d57f1bee902d"/>
        </deployment>
    </deployments>

    <server-groups>
        <server-group name="cluster-group" profile="cluster">
            <jvm name="default"/>
            <socket-binding-group ref="cluster-sockets"/>
            <deployments>
                <deployment name="LoadTest6.ear" runtime-name="LoadTest6.ear" enabled="false"/>
            </deployments>
        </server-group>
    </server-groups>

</domain>

The host configuration is set-up as follows:

<host name="jboss" xmlns="urn:jboss:domain:1.1">

    <management>
        <security-realms>
            <security-realm name="ManagementRealm">
                <authentication>
                    <properties path="mgmt-users.properties" relative-to="jboss.domain.config.dir"/>
                </authentication>
            </security-realm>
            <security-realm name="ApplicationRealm">
                <authentication>
                    <properties path="application-users.properties" relative-to="jboss.domain.config.dir" />
                </authentication>
            </security-realm>
        </security-realms>
        <management-interfaces>
            <native-interface security-realm="ManagementRealm">
                <socket interface="management" port="${jboss.management.native.port:9999}"/>
            </native-interface>
            <http-interface security-realm="ManagementRealm">
                <socket interface="management" port="${jboss.management.http.port:9990}"/>
            </http-interface>
        </management-interfaces>
    </management>

    <domain-controller>
       <local/>
    </domain-controller>

    <interfaces>
        <interface name="management">
            <nic name="eth0"/>
        </interface>
        <interface name="public">
            <nic name="eth0"/>
        </interface>
        <interface name="unsecure">
            <inet-address value="127.0.0.1"/>
        </interface>
    </interfaces>

    <jvms>
    	<jvm name="default">
            <heap size="512m" max-size="512m"/>
            <permgen size="256m" max-size="256m"/>
            <jvm-options>
                <option value="-server"/>
                <option value="-XX:NewRatio=2"/>
                <option value="-XX:+UseParallelGC"/>
                <option value="-XX:ParallelGCThreads=2"/>
                <option value="-XX:MaxGCPauseMillis=200"/>
                <option value="-XX:GCTimeRatio=19"/>
                <option value="-XX:+UseParallelOldGC"/>
            </jvm-options>
        </jvm>
    </jvms>

    <servers>
        <server name="cluster-server1" group="cluster-group" auto-start="false">
        </server>
        <server name="cluster-server2" group="cluster-group" auto-start="false">
            <socket-bindings port-offset="1"/>
        </server>
    </servers>
</host>

Some points are worth mentioning in the above configuration:

  • In the host configuration, we have set-up two servers: cluster-server1 and cluster-server2. Note that these servers are bound to the cluster-group server group.
  • The server group is defined in the domain configuration and coupled to the cluster profile, the default JVM (defined in the host configuration), and the cluster-sockets socket binding group.
  • The default interface that cluster-sockets socket binding group uses, is the public interface. The public interface is configured to use the network interface card in this case eth0 (see the host configuration).
  • The messaging socket binding is part of the cluster-sockets socket binding group.
  • The netty connector is configured to use the messaging socket-binding.
  • The RemoteConnectionFactory is configured to use the netty connector so when a client looks up this connection factory it will receive this netty connector which will tell the client where to connect. A remark is in order: Once we get the JMS connection factory reference from the server by looking it up in JNDI and use it to create a connection, the final destination of the connection has nothing to do with the java.naming.provider.url (Context.PROVIDER_URL) that was used in the JNDI lookup. When looking up a connection factory in JNDI, the client gets a connector which is a simple stub telling the client where its connections should go. In this case the connections are bound to the network interface card and the port defined in the messaging socket binding.

Using JBoss JMS Remotely

To interact remotely with the JBoss JMS provider (HornetQ) we can use the following program to test (before we put everything into Spring)

package model.test;

import javax.jms.*;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Properties;

public class JNDITest {
    public static void main(String[] args) {
        Properties properties = new Properties();
        properties.put(Context.INITIAL_CONTEXT_FACTORY, "org.jboss.naming.remote.client.InitialContextFactory");
        properties.put(Context.PROVIDER_URL, "remote://192.168.1.150:4447");
        properties.put(Context.SECURITY_PRINCIPAL, "employee");
        properties.put(Context.SECURITY_CREDENTIALS, "welcome1");

        ConnectionFactory connectionFactory = null;
        Destination destination = null;

        try {
            Context context = new InitialContext(properties);
            connectionFactory = (ConnectionFactory) context.lookup("jms/RemoteConnectionFactory");
            destination = (Destination) context.lookup("jms/queue/test");

            System.out.println(connectionFactory);
            System.out.println(destination);

            sendMessage(connectionFactory, destination);
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }

    private static void sendMessage(ConnectionFactory connectionFactory, Destination destination) {
        Connection connection = null;
        Session session = null;
        MessageProducer messageProducer = null;

        try {
            connection = connectionFactory.createConnection("employee", "welcome1");
            session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
            messageProducer = session.createProducer(destination);

            TextMessage text = session.createTextMessage();
            text.setText("Send some useful message");
            messageProducer.send(text);
        } catch (JMSException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                try {
                    connection.close();
                } catch (JMSException f) {
                    f.printStackTrace();
                }
            }
        }

    }
}

One (or more) of the following exceptions can be encountered when running the program:

  1. Caused by: HornetQException[errorCode=2 message=Cannot connect to server(s). Tried with all available servers.]
  2. Caused by: HornetQException[errorCode=105 message=Unable to validate user: null]
  3. javax.jms.JMSSecurityException: User: employee doesn’t have permission=’SEND’ on address jms.queue.test

The first exception is usually due to a host mismatch. Note that we are using the netty connector for the RemoteConnectionFactory. The netty connector uses the following (NettyConnector.java) to obtain an address:

    remoteDestination = new InetSocketAddress(host, port);
    InetAddress inetAddress = ((InetSocketAddress) remoteDestination).getAddress();

So what address would that be? To check this we can use the following

import java.net.InetAddress;

public class WhatIsMyAddress {
    public static void main(String[] args) throws Exception {
        System.out.println(InetAddress.getLocalHost());
    }
}

When this program is run we get the following output:

[jboss@axis-into-ict temp]$ /home/jboss/jdk1.6.0_31/bin/java WhatIsMyAddress
axis-into-ict.nl/192.168.1.150

When we run the test to interact with the JBoss JMS provider remotely, we get the following

Aug 10, 2012 10:48:44 AM org.xnio.Xnio <clinit>
INFO: XNIO Version 3.0.3.GA
Aug 10, 2012 10:48:44 AM org.xnio.nio.NioXnio <clinit>
INFO: XNIO NIO Implementation Version 3.0.3.GA
Aug 10, 2012 10:48:44 AM org.jboss.remoting3.EndpointImpl <clinit>
INFO: JBoss Remoting version 3.2.2.GA
HornetQConnectionFactory [serverLocator=ServerLocatorImpl [initialConnectors=[org-hornetq-core-remoting-impl-netty-NettyConnectorFactory?port=5445&host=axis-into-ict-nl], discoveryGroupConfiguration=null], clientID=null, dupsOKBatchSize=1048576, transactionBatchSize=1048576, readOnly=false]
HornetQQueue[testQueue]

One important thing to note here, is that the host is set to axis-into-ict-nl instead of the expected axis-into-ict.nl, i.e., in the host the ‘.’ are replaced by ‘-’. In order to get the axis-into-ict-nl to be resolved to the right IP address we add this to the etc/hosts file on the client

192.168.1.150       axis-into-ict.nl axis-into-ict-nl

The second exception is because no user is provided in the creation of a connection, i.e., connection = connectionFactory.createConnection(); is used instead of connection = connectionFactory.createConnection("employee", "welcome1").

The third exception is because the provided user to create a connection does not have the right privileges. Note that in the JMS configuration the following is present

<security-settings>
    <security-setting match="#">
        <permission type="send" roles="guest"/>
        <permission type="consume" roles="guest"/>
        <permission type="createNonDurableQueue" roles="guest"/>
        <permission type="deleteNonDurableQueue" roles="guest"/>
    </security-setting>
</security-settings>

This means the user must have the guest role. To accomplish this edit the application-roles.properties file (located in the $JBOSS_HOME/domain/configuration directory) and add the guest role to the used user, for example,

employee=guest,EMPLOYEE
manager=MANAGER

Restart the server such that the changes are picked up.

Create the Spring Client

Now that we have everything in place we can set-up the Spring configuration, for example,

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="jnditemplate" class="org.springframework.jndi.JndiTemplate">
        <property name="environment">
            <props>
                <prop key="java.naming.factory.initial">org.jboss.naming.remote.client.InitialContextFactory</prop>
                <prop key="java.naming.provider.url">remote://192.168.1.150:4447</prop>
                <prop key="java.naming.security.principal">employee</prop>
                <prop key="java.naming.security.credentials">welcome1</prop>
            </props>
        </property>
    </bean>
    <bean id="connectionfactory" class="org.springframework.jndi.JndiObjectFactoryBean">
        <property name="jndiTemplate" ref="jnditemplate"/>
        <property name="jndiName" value="jms/RemoteConnectionFactory"/>
    </bean>
    <bean id="destination" class="org.springframework.jndi.JndiObjectFactoryBean">
        <property name="jndiTemplate" ref="jnditemplate"/>
        <property name="jndiName" value="jms/queue/test"/>
    </bean>
    <bean id="credentialsconnectionfactory"
          class="org.springframework.jms.connection.UserCredentialsConnectionFactoryAdapter">
        <property name="targetConnectionFactory" ref="connectionfactory"/>
        <property name="username" value="employee"/>
        <property name="password" value="welcome1"/>
    </bean>
    <bean id="jmstemplate" class="org.springframework.jms.core.JmsTemplate">
        <property name="connectionFactory" ref="credentialsconnectionfactory"/>
        <property name="defaultDestination" ref="destination"/>
    </bean>
    <bean class="org.springframework.jms.listener.SimpleMessageListenerContainer">
        <property name="connectionFactory" ref="credentialsconnectionfactory"/>
        <property name="destination" ref="destination"/>
        <property name="messageListener" ref="receiver"/>
    </bean>
    <bean id="sender" class="model.logic.JMSSender">
        <property name="jmsTemplate" ref="jmstemplate"/>
    </bean>
    <bean id="receiver" class="model.logic.JMSReceiver"/>
</beans>

in which, the referred classes JMSSender and JMSReceiver look as follows

package model.logic;

import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
import javax.jms.TextMessage;
import java.util.Random;

public class JMSSender {

    private Random generator = new Random();
    private JmsTemplate jmsTemplate;

    public JMSSender() {
    }

    public JmsTemplate getJmsTemplate() {
        return jmsTemplate;
    }

    public void setJmsTemplate(JmsTemplate jmsTemplate) {
        this.jmsTemplate = jmsTemplate;
    }

    public void sendMessage() {
        getJmsTemplate().send(new MessageCreator(){
            public Message createMessage(Session session) throws JMSException {
                TextMessage message = session.createTextMessage();
                message.setText(Long.toString(Math.abs(generator.nextLong()), 36));
                return message;
            }
        });
    }
}
package model.logic;

import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.MessageListener;
import javax.jms.TextMessage;

public class JMSReceiver implements MessageListener {
    public JMSReceiver() {
    }

    public void onMessage(Message message) {
        TextMessage text = (TextMessage)message;
        try {
            message.acknowledge();
            System.out.println("received the following message: " + text.getText());
        } catch (JMSException e) {
            e.printStackTrace();
        }
    }
}

To test the set-up we can use

package model.test;

import model.logic.JMSSender;
import model.utils.SpringUtilities;

public class JMSTest {

    public static void main(String[] args) {
        JMSSender jmsSender = SpringUtilities.getJMSSender();

        while (true) {
            jmsSender.sendMessage();
            System.out.println("done sending message");
            try {
                Thread.sleep(10000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

When the test is run the following output is observed

Aug 10, 2012 11:19:41 AM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@1ed2ae8: startup date [Fri Aug 10 11:19:41 CEST 2012]; root of context hierarchy
Aug 10, 2012 11:19:41 AM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [spring-config.xml]
Aug 10, 2012 11:19:41 AM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@ef5502: defining beans [jnditemplate,connectionfactory,destination,credentialsconnectionfactory,jmstemplate,org.springframework.jms.listener.SimpleMessageListenerContainer#0,sender,receiver]; root of factory hierarchy
Aug 10, 2012 11:19:41 AM org.xnio.Xnio <clinit>
INFO: XNIO Version 3.0.3.GA
Aug 10, 2012 11:19:41 AM org.xnio.nio.NioXnio <clinit>
INFO: XNIO NIO Implementation Version 3.0.3.GA
Aug 10, 2012 11:19:41 AM org.jboss.remoting3.EndpointImpl <clinit>
INFO: JBoss Remoting version 3.2.2.GA
Aug 10, 2012 11:19:42 AM org.jboss.naming.remote.protocol.v1.RemoteNamingStoreV1$MessageReceiver handleEnd
ERROR: Channel end notification received, closing channel Channel ID b3d15ef8 (outbound) of Remoting connection 009ffe3f to /192.168.1.150:4447
Aug 10, 2012 11:19:42 AM org.jboss.naming.remote.protocol.v1.RemoteNamingStoreV1$MessageReceiver handleEnd
ERROR: Channel end notification received, closing channel Channel ID c4cd8d30 (outbound) of Remoting connection 0032060c to /192.168.1.150:4447
Aug 10, 2012 11:19:42 AM org.springframework.context.support.DefaultLifecycleProcessor$LifecycleGroup start
INFO: Starting beans in phase 2147483647
received the following message: Send some useful message
received the following message: q9em87lq59pb
done sending message
received the following message: 9l1e70x6e2yf
done sending message
received the following message: 1e0fqxcfs0os5
done sending message
received the following message: 8bcprios0t56
done sending message
received the following message: 1elnsn5m1sxf1
done sending message
received the following message: 5cnl5bt1ztp8
done sending message
received the following message: 1tnm5yf1dn6fx
done sending message
received the following message: j8efslaqykc6
done sending message
received the following message: t10r3842i2fi
done sending message
received the following message: j845q4yhyrgn
done sending message
received the following message: 4ky8mbz8h7kl
done sending message
received the following message: y5go5h1iwcl0
done sending message
received the following message: 5j6buhmnq9up
done sending message
received the following message: 1wdojt8n3kztv
done sending message
received the following message: 1k6p9352nwcgl
done sending message
received the following message: 1povempe0r3ju
done sending message

References

[1] EJB invocations from a remote client using JNDI.
[2] Remote EJB invocations via JNDI – EJB client API or remote-naming project.
[3] EJB invocations from a remote server instance.