How to code a Remote EJB Client with WildFly

This tutorial explains how to invoke EJBs running on WildFly from a remote client by using the JNDI API. We will learn how to first lookup the EJB proxy and then invoke methods on that from a remote standalone Java client.

Firstly, let’s describe our Use case: a remote Java clients attempts to look up an EJB running on WildFly application server:

wildfly remote ejb client tutorial

On the other hand, if your EJB Client is running inside WildFly application server (ex. another EJB Application), then we recommend checking this tutorial: WildFly: How to call an EJB from an EJB located in another application

In order to demonstrate how to invoke EJBs from a remote EJB Client, we need to complete the following steps:

  • Create the EJB Server Project which contains the interfaces and the implementation for the EJBs
  • Create the EJB Client Project which looks up the remote EJBs
  • Configure Security in the Client Project in order to authorize the remote call

Create the EJB Server Project

Firstly, we will set up the server project. This project includes the following Stateful and Stateless Session EJBs:

package com.itbuzzpress.chapter4.ejb;

import jakarta.ejb.Remote;
import jakarta.ejb.Stateful;
import com.itbuzzpress.chapter4.exception.InsufficientFundsException;

@Stateful
@Remote(Account.class)
public class AccountEJB implements Account {

    long money;

    @Override
    public long getMoney() {
        return money;
    }

    public void createAccount(long amount) {
        this.money = amount;
    }

    @Override
    public void deposit(long amount) {
        this.money += amount;
        System.out.println("Money deposit. total is " + money);
    }

    @Override
    public void withdraw(long amount) throws InsufficientFundsException {

        long newAmount = money - amount;
        if (newAmount < 0) {
            throw new InsufficientFundsException("Unsufficient funds for account! ");
        }

        money = newAmount;
        System.out.println("Money withdrawal. total is " + money);
    }
}


package com.itbuzzpress.chapter4.ejb;

import jakarta.ejb.Remote;
import jakarta.ejb.Stateless;

@Stateless
@Remote(Calculator.class)
public class CalculatorEJB implements Calculator {
   
	float interest=5;
 
	@Override
	public float calculateInterest(long money) {	 
	    return money * (1+ (interest/100));	       
       }
	
}

As you can see, this project uses the “jakarta” namespace for the EJBs. At the end of this section you will find the source code also for the legacy (javax) server.

Next, let’s code the interface contracts for the EJBs:

package com.itbuzzpress.chapter4.ejb;

import com.itbuzzpress.chapter4.exception.InsufficientFundsException;

public interface Account {

	public void deposit(long amount);
	public void withdraw(long amount) throws InsufficientFundsException;
	public long getMoney();
	public void createAccount(long amount);
}

package com.itbuzzpress.chapter4.ejb;

public interface Calculator {
	public float calculateInterest(long money);	 
}

Configuring Server dependencies

In order to build and deploy the server project on a Jakarta EE compliant application server, just include the jakarta.jakartaee-api artifact in pom.xml:

<dependency>
         <groupId>jakarta.platform</groupId>
         <artifactId>jakarta.jakartaee-api</artifactId>
         <version>${version.jakartaee}</version>
         <scope>provided</scope>
</dependency>

Obviously, you will need to include the version.jakartaee in your properties. For example:

<version.jakartaee>9.1.0</version.jakartaee>

Deploy the EJB Server Project

As our pom.xml includes WildFly’s maven plugin, you can simply build and deploy the EJB Server project as follows:

mvn install wildfly:deploy

Check from the server logs that the EJB project has been deployed and that JNDI bindings are available:

17:54:04,147 INFO  [org.jboss.as.ejb3.deployment] (MSC service thread 1-8) WFLYEJB0473: JNDI bindings for session bean named 'AccountEJB' in deployment unit 'deployment "ejb-server-basic.jar"' are as follows:

	java:global/ejb-server-basic/AccountEJB!com.itbuzzpress.chapter4.ejb.Account
	java:app/ejb-server-basic/AccountEJB!com.itbuzzpress.chapter4.ejb.Account
	java:module/AccountEJB!com.itbuzzpress.chapter4.ejb.Account
	java:jboss/exported/ejb-server-basic/AccountEJB!com.itbuzzpress.chapter4.ejb.Account
	ejb:/ejb-server-basic/AccountEJB!com.itbuzzpress.chapter4.ejb.Account?stateful
	java:global/ejb-server-basic/AccountEJB
	java:app/ejb-server-basic/AccountEJB
	java:module/AccountEJB

Create the EJB Client Project

Next, we will be building the EJB Client project. This project includes the following RemoteEJBClient Class::

package com.itbuzzpress.chapter4.client;

import java.util.Hashtable;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import com.itbuzzpress.chapter4.ejb.Account;
import com.itbuzzpress.chapter4.ejb.Calculator;
import com.itbuzzpress.chapter4.exception.InsufficientFundsException;

import javax.naming.spi.NamingManager;
public class RemoteEJBClient {

	public static void main(String[] args) throws Exception {
		Calculator calculator = lookupCalculatorEJB();
		Account account = lookupAccountEJB();
		System.out.println("Create Account with 1000$ ");

		account.createAccount(1000l);
		System.out.println("Deposit 250$ ");
		account.deposit(250);

		try {
			System.out.println("Withdraw 500$ ");
			account.withdraw(500);
		} catch (InsufficientFundsException e) {
			e.printStackTrace();
		}
		long money = account.getMoney();
		System.out.println("Money left " + money);
		float totalMoney = calculator.calculateInterest(money);
		System.out.println("Money plus interests " + totalMoney);
	}

	private static Account lookupAccountEJB() throws NamingException {
		final Hashtable jndiProperties = new Hashtable();
 
		jndiProperties.put(Context.INITIAL_CONTEXT_FACTORY,  "org.wildfly.naming.client.WildFlyInitialContextFactory");
                jndiProperties.put(Context.PROVIDER_URL,"http-remoting://localhost:8080");
		final Context ctx = new InitialContext(jndiProperties);
		return (Account) ctx
				.lookup("ejb:/ejb-server-basic/AccountEJB!com.itbuzzpress.chapter4.ejb.Account?stateful");
	}

	private static Calculator lookupCalculatorEJB() throws NamingException {
		final Hashtable jndiProperties = new Hashtable();
		jndiProperties.put(Context.INITIAL_CONTEXT_FACTORY,  "org.wildfly.naming.client.WildFlyInitialContextFactory");
                jndiProperties.put(Context.PROVIDER_URL,"http-remoting://localhost:8080");
		final Context context = new InitialContext(jndiProperties);

		return (Calculator) context
				.lookup("ejb:/ejb-server-basic/CalculatorEJB!com.itbuzzpress.chapter4.ejb.Calculator");
	}
}

As you can see, the most evident change with former WildFly ejb client is the new Initial Context which requires “org.wildfly.naming.client.WildFlyInitialContextFactory”. If you set the system property java.naming.factory.initial to org.wildfly.naming.client.WildFlyInitialContextFactory, then your Java code to access an initial context is just:

InitialContext ctx = new InitialContext();
Account account = (Account) ctx.lookup("ejb:/ejb-server-basic/CalculatorEJB!com.itbuzzpress.chapter4.ejb.Calculator");

Configuring EJB Client dependencies

The server project requires the following dependencies:

<dependency>
     <groupId>com.itbuzzpress.chapter4</groupId>
     <artifactId>ejb-server-basic</artifactId>
     <type>ejb-client</type>
     <version>${project.version}</version>
</dependency>

<dependency>
     <groupId>org.wildfly</groupId>
     <artifactId>wildfly-ejb-client-bom</artifactId>
     <type>pom</type>
     <scope>compile</scope>
     <version>${version.wildfly}</version>
</dependency>

In your project properties, set the WildFly version where the EJB is available. For example:

<version.wildfly>26.1.1.Final</version.wildfly>

Configuring Security in the EJB Client Project

Another change impacting your clients is that the former jboss-ejb-client.properties file is deprecated so you are encouraged to migrate to the Elytron wildfly-config.xml file which unifies all client configuration in a single place.

In our example, we will set the property wildfly.sasl.local-user.quiet-auth which enables silent authentication for a local user:

<configuration>
    <authentication-client xmlns="urn:elytron:1.0">
        <authentication-rules>
                    <rule use-configuration="default" />
        </authentication-rules>
        <authentication-configurations>
            <configuration name="default">
                <sasl-mechanism-selector selector="#ALL" />
                <set-mechanism-properties>
                    <property key="wildfly.sasl.local-user.quiet-auth" value="true" />
                 </set-mechanism-properties>
                <providers>
                    <use-service-loader/>
                </providers>
             </configuration>
        </authentication-configurations>
    </authentication-client>
</configuration>

On the other hand, if you need to provide credentials, then you can add them in the wildfly-config.xml file as in the following example:

<configuration>
    <authentication-client xmlns="urn:elytron:1.0">
        <authentication-rules>
            <rule use-configuration="default"/>
        </authentication-rules>
        <authentication-configurations>
            <configuration name="default">
                <sasl-mechanism-selector selector="DIGEST-MD5"/>
                <set-user-name name="ejbuser"/>
                <credentials>
                    <clear-password password="password1!"/>
                </credentials>
            </configuration>
        </authentication-configurations>
    </authentication-client>	
</configuration>

To create the user on the server, just execute the add-user.sh script as follows:

$ ./add-user.sh -a -u ejbuser -p password1!

Running the EJB Client Project

In order to run the Client application, we have included in the pom.xml of our Client Project the exec-maven-plugin which contains as argument the Class name to be executed:

<plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>exec-maven-plugin</artifactId>
            <version>${version.exec.plugin}</version>
            <executions>
               <execution>
                  <goals>
                     <goal>exec</goal>
                  </goals>
               </execution>
            </executions>
            <configuration>
               <executable>java</executable>
               <workingDirectory>${project.build.directory}/exec-working-directory</workingDirectory>
               <arguments>
                  <argument>-classpath</argument>
                  <classpath />                 
                  <argument>com.itbuzzpress.chapter4.client.RemoteEJBClient</argument>
               </arguments>
            </configuration>
</plugin>

You can run it the application with:

mvn exec:exec

Source code

The source code for the Jakarta EE Remote EJB Client is available at: https://github.com/fmarchioni/mastertheboss/tree/master/ejb/remote-ejb-latest

The source code for the Java EE Remote EJB Client is available at: https://github.com/fmarchioni/mastertheboss/tree/master/ejb/remote-ejb-javaee

Switching to HTTP transport

To allow for balancing at the individual invocation level, a new “pure” HTTP protocol is available sinceWildFly 11. Clients can use “http://” URLs as opposed to “remoting+http://” to leverage this feature. The advantage of it, is that this protocol utilizes standard HTTP behaviour, it can be efficiently balanced by any load-balancer, not just the one built into EAP.

In order to use the pure HTTP Transport, you have to include the following dependencies in your project:

<dependency>
        <groupId>org.wildfly.wildfly-http-client</groupId>
        <artifactId>wildfly-http-client-common</artifactId>                  
</dependency>
<dependency>
        <groupId>org.wildfly.wildfly-http-client</groupId>
        <artifactId>wildfly-http-naming-client</artifactId>                 
 </dependency>

Then in your code you need to replace the Context.PROVIDER_URL from this:

jndiProperties.put(Context.PROVIDER_URL,"http-remoting://localhost:8080");

to this:

jndiProperties.put(Context.PROVIDER_URL,"http://localhost:8080/wildfly-services");

EJB Client Hack: How to retrieve the EJB Client’s remote IP address

The dependency jboss-ejb-client includes the EJBClient class that contains an useful property named SOURCE_ADDRESS_KEY that you can use to retrieve the remote EJB Client IP address. Here is an example of it:

import java.net.InetSocketAddress;
import javax.annotation.Resource;
import javax.ejb.SessionContext;
import javax.ejb.Stateless;
import org.jboss.ejb.client.EJBClient;

@Stateless
public class CalculatorEJB implements Calculator {
    @Resource
    private SessionContext ctx;

    @Override
    public String getClientAddress() {

        InetSocketAddress clientAddress = (InetSocketAddress) ctx.getContextData().get(EJBClient.SOURCE_ADDRESS_KEY);
        if(clientAddress != null) return clientAddress.getHostString();
        return null;
    }
}

Legacy Remote EJB Clients (WildFly 8,9,10 and JBoss EAP 6)

If you are still running an old WildFly (8,9,10) or JBoss EAP 6 application server, there are some differences in the EJB Client lookup.

These EJB clients rely on the the jboss-remote-naming project (https://github.com/jbossas/jboss-remote-naming) to lookup remote EJBs and perform security checks.

The recommended steps to configure an EJB client are the following:

  • Include a jboss-ejb-client.properties and place it on the client classpath:
endpoint.name=client-endpoint
remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED=false
remote.connections=server1
remote.connection.server1.host=192.168.10.1
remote.connection.server1.port = 4447
remote.connection.server1.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false
remote.connection.server1.username=user
remote.connection.server1.password=password
  • Then, in order to look up your EJB, set the Context.URL_PKG_PREFIXES to “org.jboss.ejb.client.naming”:
final Hashtable jndiProperties = new Hashtable();
jndiProperties.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming");
Context context = new InitialContext(jndiProperties);
context.lookup("ejb:ejb-server-basic/CalculatorEJB!com.itbuzzpress.chapter4.ejb.Calculator");

You can find a full example with source code at: https://github.com/fmarchioni/mastertheboss/tree/master/ejb/remote-ejb


Troubleshooting EJB 3 remote clients

The expected outcome is a dummy echo print on the Console when running the client. However the things to checkout are many and so the number of possible errors. A quite common error in the forum of developers is:

Exception in thread “main” javax.naming.NoInitialContextException: Need to specify class name in environment or system property, or as an applet parameter, or in an application resource file: java.naming.factory.initial

The checklist for this error includes:

  • Have you added JBoss’s client libraries to your project ?
  • Maybe you are using a wrong JNDI root context. For example, instead of the correct

ejb:/as7project//SampleBeanRemoteImpl!com.sample.ejb.SampleBeanRemote
you might be using:
java:/as7project//SampleBeanRemoteImpl!com.sample.ejb.SampleBeanRemote

Another common issue is:

No EJB receiver available for handling [appName:,modulename:,distinctname:as7project] combination for invocation context org.jboss.ejb.client.EJBClientInvocationContext@10f6d3

In this case, there’s a mismatch between module name and distinct name. (Look at the underlined section). Check your JNDI building String

On the other hand, here the error looks the same but the module name is correct:

No EJB receiver available for handling [appName:,modulename:as7project,distinctname:] combination for invocation context org.jboss.ejb.client.EJBClientInvocationContext@7b7072

Here, you should check your JNDI again, maybe you have uncorrectly stated your remote EJB name or your remote EJB implementation class. For example, instead of the correct
ejb:/as7project//SampleBeanRemoteImpl!com.sample.ejb.SampleBeanRemote
you might be using:
java:/as7project//SampleBeanRemoteImpl!com.sample.ejb.SampleBeanRemoteTypo

Here’s one more issue:

WARN: Could not register a EJB receiver for connection to remote://192.168.1.1:4447
java.lang.RuntimeException: Operation failed with status WAITING

This is commonly caused by a wrong combination of IP or port in the file jboss-ejb-client.properties.

  • You might be unable to reach that host address
  • Maybe you are using a port-offset on that server, so instead of port 4447 you should use 4447 + offset

One more issue:

Exception in thread “main” javax.naming.NamingException: No provider URL configured for connection

Chances are that you are trying to initialize older JBoss InitialContextFactory.
For example, maybe you have got:

jndiProperties.put(Context.INITIAL_CONTEXT_FACTORY, "org.jboss.naming.remote.client.InitialContextFactory");

If so, remove that line from your code!