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:
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 [email protected]
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 [email protected]
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!