EJB invocations from a remote client using JNDI

来源:互联网 发布:js font 赋值 编辑:程序博客网 时间:2024/05/18 18:14

This chapter explains how to invoke EJBs from a remote client by using the JNDI API to first lookup the bean proxy and then invoke on that proxy.

After you have read this article, do remember to take a look at Remote EJB invocations via JNDI - EJB client API or remote-naming project

Before getting into the details, we would like the users to know that we have introduced a new EJB client API, which is a JBoss specific API and allows invocation on remote EJBs. This client API isn't based on JNDI. So remote clients need not rely on JNDI API to invoke on EJBs. A separate document covering the EJB remote client API will be made available. For now, you can refer to the javadocs of the EJB client project at http://docs.jboss.org/ejbclient/. In this document, we'll just concentrate on the traditional JNDI based invocation on EJBs. So let's get started:

Deploying your EJBs on the server side:

Users who already have EJBs deployed on the server side can just skip to the next section.

As a first step, you'll have to deploy your application containing the EJBs on the AS7 server. If you want those EJBs to be remotely invocable, then you'll have to expose atleast one remote view for that bean. In this example, let's consider a simple Calculator stateless bean which exposes a RemoteCalculator remote business interface. We'll also have a simple stateful CounterBean which exposes a RemoteCounter remote business interface. Here's the code:

packageorg.jboss.as.quickstarts.ejb.remote.stateless;
 
/**
 * @author Jaikiran Pai
 */
publicinterfaceRemoteCalculator {
 
    intadd(inta,intb);
 
    intsubtract(inta,intb);
}
packageorg.jboss.as.quickstarts.ejb.remote.stateless;
 
importjavax.ejb.Remote;
importjavax.ejb.Stateless;
 
/**
 * @author Jaikiran Pai
 */
@Stateless
@Remote(RemoteCalculator.class)
publicclassCalculatorBean implementsRemoteCalculator {
 
    @Override
    publicintadd(inta,intb) {
        returna + b;
    }
 
    @Override
    publicintsubtract(inta,intb) {
        returna - b;
    }
}
packageorg.jboss.as.quickstarts.ejb.remote.stateful;
 
/**
 * @author Jaikiran Pai
 */
publicinterfaceRemoteCounter {
 
    voidincrement();
 
    voiddecrement();
 
    intgetCount();
}
packageorg.jboss.as.quickstarts.ejb.remote.stateful;
 
importjavax.ejb.Remote;
importjavax.ejb.Stateful;
 
/**
 * @author Jaikiran Pai
 */
@Stateful
@Remote(RemoteCounter.class)
publicclassCounterBean implementsRemoteCounter {
 
    privateintcount = 0;
 
    @Override
    publicvoidincrement() {
        this.count++;
    }
 
    @Override
    publicvoiddecrement() {
        this.count--;
    }
 
    @Override
    publicintgetCount() {
        returnthis.count;
    }
}

Let's package this in a jar (how you package it in a jar is out of scope of this chapter) named "jboss-as-ejb-remote-app.jar" and deploy it to the server. Make sure that your deployment has been processed successfully and there aren't any errors.

Writing a remote client application for accessing and invoking the EJBs deployed on the server

The next step is to write an application which will invoke the EJBs that you deployed on the server. In AS7, you can either choose to use the JBoss specific EJB client API to do the invocation or use JNDI to lookup a proxy for your bean and invoke on that returned proxy. In this chapter we will concentrate on the JNDI lookup and invocation and will leave the EJB client API for a separate chapter.

So let's take a look at what the client code looks like for looking up the JNDI proxy and invoking on it. Here's the entire client code which invokes on a stateless bean:

packageorg.jboss.as.quickstarts.ejb.remote.client;
 
importjavax.naming.Context;
importjavax.naming.InitialContext;
importjavax.naming.NamingException;
importjava.security.Security;
importjava.util.Hashtable;
 
importorg.jboss.as.quickstarts.ejb.remote.stateful.CounterBean;
importorg.jboss.as.quickstarts.ejb.remote.stateful.RemoteCounter;
importorg.jboss.as.quickstarts.ejb.remote.stateless.CalculatorBean;
importorg.jboss.as.quickstarts.ejb.remote.stateless.RemoteCalculator;
importorg.jboss.sasl.JBossSaslProvider;
 
/**
 * A sample program which acts a remote client for a EJB deployed on AS7 server.
 * This program shows how to lookup stateful and stateless beans via JNDI and then invoke on them
 *
 * @author Jaikiran Pai
 */
publicclassRemoteEJBClient {
 
    publicstaticvoid main(String[] args) throwsException {
        // Invoke a stateless bean
        invokeStatelessBean();
 
        // Invoke a stateful bean
        invokeStatefulBean();
    }
 
    /**
     * Looks up a stateless bean and invokes on it
     *
     * @throws NamingException
     */
    privatestaticvoid invokeStatelessBean() throwsNamingException {
        // Let's lookup the remote stateless calculator
        finalRemoteCalculator statelessRemoteCalculator = lookupRemoteStatelessCalculator();
        System.out.println("Obtained a remote stateless calculator for invocation");
        // invoke on the remote calculator
        inta = 204;
        intb = 340;
        System.out.println("Adding " + a + " and " + b + " via the remote stateless calculator deployed on the server");
        intsum = statelessRemoteCalculator.add(a, b);
        System.out.println("Remote calculator returned sum = " + sum);
        if(sum != a + b) {
            thrownewRuntimeException("Remote stateless calculator returned an incorrect sum " + sum + " ,expected sum was " + (a + b));
        }
        // try one more invocation, this time for subtraction
        intnum1 = 3434;
        intnum2 = 2332;
        System.out.println("Subtracting " + num2 + " from " + num1 + " via the remote stateless calculator deployed on the server");
        intdifference = statelessRemoteCalculator.subtract(num1, num2);
        System.out.println("Remote calculator returned difference = " + difference);
        if(difference != num1 - num2) {
            thrownewRuntimeException("Remote stateless calculator returned an incorrect difference " + difference + " ,expected difference was " + (num1 - num2));
        }
    }
 
    /**
     * Looks up a stateful bean and invokes on it
     *
     * @throws NamingException
     */
    privatestaticvoid invokeStatefulBean() throwsNamingException {
        // Let's lookup the remote stateful counter
        finalRemoteCounter statefulRemoteCounter = lookupRemoteStatefulCounter();
        System.out.println("Obtained a remote stateful counter for invocation");
        // invoke on the remote counter bean
        finalintNUM_TIMES = 20;
        System.out.println("Counter will now be incremented " + NUM_TIMES + " times");
        for(inti = 0; i < NUM_TIMES; i++) {
            System.out.println("Incrementing counter");
            statefulRemoteCounter.increment();
            System.out.println("Count after increment is " + statefulRemoteCounter.getCount());
        }
        // now decrementing
        System.out.println("Counter will now be decremented " + NUM_TIMES + " times");
        for(inti = NUM_TIMES; i > 0; i--) {
            System.out.println("Decrementing counter");
            statefulRemoteCounter.decrement();
            System.out.println("Count after decrement is " + statefulRemoteCounter.getCount());
        }
    }
 
    /**
     * Looks up and returns the proxy to remote stateless calculator bean
     *
     * @return
     * @throws NamingException
     */
    privatestaticRemoteCalculator lookupRemoteStatelessCalculator() throwsNamingException {
        finalHashtable jndiProperties = newHashtable();
        jndiProperties.put(Context.URL_PKG_PREFIXES,"org.jboss.ejb.client.naming");
        finalContext context = newInitialContext(jndiProperties);
        // The app name is the application name of the deployed EJBs. This is typically the ear name
        // without the .ear suffix. However, the application name could be overridden in the application.xml of the
        // EJB deployment on the server.
        // Since we haven't deployed the application as a .ear, the app name for us will be an empty string
        finalString appName = "";
        // This is the module name of the deployed EJBs on the server. This is typically the jar name of the
        // EJB deployment, without the .jar suffix, but can be overridden via the ejb-jar.xml
        // In this example, we have deployed the EJBs in a jboss-as-ejb-remote-app.jar, so the module name is
        // jboss-as-ejb-remote-app
        finalString moduleName = "jboss-as-ejb-remote-app";
        // AS7 allows each deployment to have an (optional) distinct name. We haven't specified a distinct name for
        // our EJB deployment, so this is an empty string
        finalString distinctName = "";
        // The EJB name which by default is the simple class name of the bean implementation class
        finalString beanName = CalculatorBean.class.getSimpleName();
        // the remote view fully qualified class name
        finalString viewClassName = RemoteCalculator.class.getName();
        // let's do the lookup
        return(RemoteCalculator) context.lookup("ejb:"+ appName + "/"+ moduleName + "/"+ distinctName + "/"+ beanName + "!"+ viewClassName);
    }
 
    /**
     * Looks up and returns the proxy to remote stateful counter bean
     *
     * @return
     * @throws NamingException
     */
    privatestaticRemoteCounter lookupRemoteStatefulCounter() throwsNamingException {
        finalHashtable jndiProperties = newHashtable();
        jndiProperties.put(Context.URL_PKG_PREFIXES,"org.jboss.ejb.client.naming");
        finalContext context = newInitialContext(jndiProperties);
        // The app name is the application name of the deployed EJBs. This is typically the ear name
        // without the .ear suffix. However, the application name could be overridden in the application.xml of the
        // EJB deployment on the server.
        // Since we haven't deployed the application as a .ear, the app name for us will be an empty string
        finalString appName = "";
        // This is the module name of the deployed EJBs on the server. This is typically the jar name of the
        // EJB deployment, without the .jar suffix, but can be overridden via the ejb-jar.xml
        // In this example, we have deployed the EJBs in a jboss-as-ejb-remote-app.jar, so the module name is
        // jboss-as-ejb-remote-app
        finalString moduleName = "jboss-as-ejb-remote-app";
        // AS7 allows each deployment to have an (optional) distinct name. We haven't specified a distinct name for
        // our EJB deployment, so this is an empty string
        finalString distinctName = "";
        // The EJB name which by default is the simple class name of the bean implementation class
        finalString beanName = CounterBean.class.getSimpleName();
        // the remote view fully qualified class name
        finalString viewClassName = RemoteCounter.class.getName();
        // let's do the lookup (notice the ?stateful string as the last part of the jndi name for stateful bean lookup)
        return(RemoteCounter) context.lookup("ejb:"+ appName + "/"+ moduleName + "/"+ distinctName + "/"+ beanName + "!"+ viewClassName + "?stateful");
    }
}
The entire server side and client side code is hosted at the github repo here https://github.com/jboss-jdf/jboss-as-quickstart/tree/master/ejb-remote

The code has some comments which will help you understand each of those lines. But we'll explain here in more detail what the code does. As a first step in the client code, we'll do a lookup of the EJB using a JNDI name. In AS7, for remote access to EJBs, you use the ejb: namespace with the following syntax:

For stateless beans:

ejb:<app-name>/<module-name>/<distinct-name>/<bean-name>!<fully-qualified-classname-of-the-remote-interface>

For stateful beans:

ejb:<app-name>/<module-name>/<distinct-name>/<bean-name>!<fully-qualified-classname-of-the-remote-interface>?stateful

The ejb: namespace identifies it as a EJB lookup and is a constant (i.e. doesn't change) for doing EJB lookups. The rest of the parts in the jndi name are as follows:

app-name : This is the name of the .ear (without the .ear suffix) that you have deployed on the server and contains your EJBs.

  • Java EE 6 allows you to override the application name, to a name of your choice by setting it in the application.xml. If the deployment uses uses such an override then the app-name used in the JNDI name should match that name.
  • EJBs can also be deployed in a .war or a plain .jar (like we did in step 1). In such cases where the deployment isn't an .ear file, then the app-name must be an empty string, while doing the lookup.

module-name : This is the name of the .jar (without the .jar suffix) that you have deployed on the server and the contains your EJBs. If the EJBs are deployed in a .war then the module name is the .war name (without the .war suffix).

  • Java EE 6 allows you to override the module name, by setting it in the ejb-jar.xml/web.xml of your deployment. If the deployment uses such an override then the module-name used in the JNDI name should match that name.
  • Module name part cannot be an empty string in the JNDI name

distinct-name : This is a JBoss AS7 specific name which can be optionally assigned to the deployments that are deployed on the server. More about the purpose and usage of this will be explained in a separate chapter. If a deployment doesn't use distinct-name then, use an empty string in the JNDI name, for distinct-name

bean-name : This is the name of the bean for which you are doing the lookup. The bean name is typically the unqualified classname of the bean implementation class, but can be overriden through either ejb-jar.xml or via annotations. The bean name part cannot be an empty string in the JNDI name.

fully-qualified-classname-of-the-remote-interface : This is the fully qualified class name of the interface for which you are doing the lookup. The interface should be one of the remote interfaces exposed by the bean on the server. The fully qualified class name part cannot be an empty string in the JNDI name.

For stateful beans, the JNDI name expects an additional "?stateful" to be appended after the fully qualified interface name part. This is because for stateful beans, a new session gets created on JNDI lookup and the EJB client API implementation doesn't contact the server during the JNDI lookup to know what kind of a bean the JNDI name represents (we'll come to this in a while). So the JNDI name itself is expected to indicate that the client is looking up a stateful bean, so that an appropriate session can be created.

Now that we know the syntax, let's see our code and check what JNDI name it uses. Since our stateless EJB named CalculatorBean is deployed in a jboss-as-ejb-remote-app.jar (without any ear) and since we are looking up the org.jboss.as.quickstarts.ejb.remote.stateless.RemoteCalculator remote interface, our JNDI name will be:

ejb:/jboss-as-ejb-remote-app//CalculatorBean!org.jboss.as.quickstarts.ejb.remote.stateless.RemoteCalculator

That's what the lookupRemoteStatelessCalculator() method in the above client code uses.

For the stateful EJB named CounterBean which is deployed in hte same jboss-as-ejb-remote-app.jar and which exposes the org.jboss.as.quickstarts.ejb.remote.stateful.RemoteCounter, the JNDI name will be:

ejb:/jboss-as-ejb-remote-app//CounterBean!org.jboss.as.quickstarts.ejb.remote.stateful.RemoteCounter?stateful

That's what the lookupRemoteStatefulCounter() method in the above client code uses.

Now that we know of the JNDI name, let's take a look at the following piece of code in the lookupRemoteStatelessCalculator():

finalHashtable jndiProperties = newHashtable();
jndiProperties.put(Context.URL_PKG_PREFIXES,"org.jboss.ejb.client.naming");
finalContext context = newInitialContext(jndiProperties);

Here we are creating a JNDI InitialContext object by passing it some JNDI properties. The Context.URL_PKG_PREFIXES is set to org.jboss.ejb.client.naming. This is necessary because we should let the JNDI API know what handles the ejb: namespace that we use in our JNDI names for lookup. The "org.jboss.ejb.client.naming" has a URLContextFactory implementation which will be used by the JNDI APIs to parse and return an object for ejb: namespace lookups. You can either pass these properties to the constructor of the InitialContext class or have a jndi.properites file in the classpath of the client application, which (atleast) contains the following property:

java.naming.factory.url.pkgs=org.jboss.ejb.client.naming

So at this point, we have setup the InitialContext and also have the JNDI name ready to do the lookup. You can now do the lookup and the appropriate proxy which will be castable to the remote interface that you used as the fully qualified class name in the JNDI name, will be returned. Some of you might be wondering, how the JNDI implementation knew which server address to look, for your deployed EJBs. The answer is in AS7, the proxies returned via JNDI name lookup for ejb: namespace do not connect to the server unless an invocation on those proxies is done.

Now let's get to the point where we invoke on this returned proxy:

// Let's lookup the remote stateless calculator
        finalRemoteCalculator statelessRemoteCalculator = lookupRemoteStatelessCalculator();
        System.out.println("Obtained a remote stateless calculator for invocation");
        // invoke on the remote calculator
        inta = 204;
        intb = 340;
        System.out.println("Adding " + a + " and " + b + " via the remote stateless calculator deployed on the server");
        intsum = statelessRemoteCalculator.add(a, b);

We can see here that the proxy returned after the lookup is used to invoke the add(...) method of the bean. It's at this point that the JNDI implementation (which is backed by the JBoss EJB client API) needs to know the server details. So let's now get to the important part of setting up the EJB client context properties.

Setting up EJB client context properties

A EJB client context is a context which contains contextual information for carrying out remote invocations on EJBs. This is a JBoss specific API. The EJB client context can be associated with multiple EJB receivers. Each EJB receiver is capable of handling invocations on different EJBs. For example, an EJB receiver "Foo" might be able to handle invocation on a bean identified by app-A/module-A/distinctinctName-A/Bar!RemoteBar, whereas a EJB receiver named "Blah" might be able to handle invocation on a bean identified by app-B/module-B/distinctName-B/BeanB!RemoteBean. Each such EJB receiver knows about what set of EJBs it can handle and each of the EJB receiver knows which server target to use for handling the invocations on the bean. For example, if you have a AS7 server at 10.20.30.40 IP address which has its remoting port opened at 4447 and if that's the server on which you deployed that CalculatorBean, then you can setup a EJB receiver which knows its target address is 10.20.30.40:4447. Such an EJB receiver will be capable enough to communicate to the server via the JBoss specific EJB remote client protocol (details of which will be explained in-depth in a separate chapter).

Now that we know what a EJB client context is and what a EJB receiver is, let's see how we can setup a client context with 1 EJB receiver which can connect to 10.20.30.40 IP address at port 4447. That EJB client context will then be used (internally) by the JNDI implementation to handle invocations on the bean proxy.

The client will have to place a jboss-ejb-client.properties file in the classpath of the application. The jboss-ejb-client.properties can contain the following properties:

endpoint.name=client-endpoint
remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED=false
 
remote.connections=default
 
remote.connection.default.host=10.20.30.40
remote.connection.default.port = 4447
remote.connection.default.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false
 
remote.connection.default.username=appuser
remote.connection.default.password=apppassword
The above properties file is just an example. The actual file that was used for this sample program is available here for reference https://github.com/jboss-jdf/jboss-as-quickstart/blob/master/ejb-remote/client/src/main/resources/jboss-ejb-client.properties

We'll see what each of it means.

First the endpoint.name property. We mentioned earlier that the EJB receivers will communicate with the server for EJB invocations. Internally, they use JBoss Remoting project to carry out the communication. The endpoint.name property represents the name that will be used to create the client side of the enpdoint. The endpoint.name property is optional and if not specified in the jboss-ejb-client.properties file, it will default to "config-based-ejb-client-endpoint" name.

Next is the remote.connectionprovider.create.options.<....> properties:

remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED=false

The "remote.connectionprovider.create.options." property prefix can be used to pass the options that will be used while create the connection provider which will handle the "remote:" protocol. In this example we use the "remote.connectionprovider.create.options." property prefix to pass the "org.xnio.Options.SSL_ENABLED" property value as false. That property will then be used during the connection provider creation. Similarly other properties can be passed too, just append it to the "remote.connectionprovider.create.options." prefix

Next we'll see:

remote.connections=default

This is where you define the connections that you want to setup for communication with the remote server. The "remote.connections" property uses a comma separated value of connection "names". The connection names are just logical and are used grouping together the connection configuration properties later on in the properties file. The example above sets up a single remote connection named "default". There can be more than one connections that are configured. For example:

remote.connections=one, two

Here we are listing 2 connections named "one" and "two". Ultimately, each of the connections will map to a EJB receiver. So if you have 2 connections, that will setup 2 EJB receivers that will be added to the EJB client context. Each of these connections will be configured with the connection specific properties as follows:

remote.connection.default.host=10.20.30.40
remote.connection.default.port = 4447
remote.connection.default.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false

As you can see we are using the "remote.connection.<connection-name>." prefix for specifying the connection specific property. The connection name here is "default" and we are setting the "host" property of that connection to point to 10.20.30.40. Similarly we set the "port" for that connection to 4447.

By default AS7 uses 4447 as the remoting port. The EJB client API uses the remoting port for communicating with the server for remote invocations, so that's the port we use in our client programs (unless the server is configured for some other remoting port)
remote.connection.default.username=appuser
remote.connection.default.password=apppassword

The given user/password must be set by using the command bin/add-user.sh (or.bat).
The user and password must be set because the security-realm is enabled for the subsystem remoting (see standalone*.xml or domain.xml) by default.
If you do not need the security for remoting you might remove the attribute security-realm in the configuration.

security-realm is possible since 7.1.0.FINAL and enabled by default.

We then use the "remote.connection.<connection-name>.connect.options." property prefix to setup options that will be used during the connection creation.

Here's an example of setting up multiple connections with different properties for each of those:

remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED=false
 
remote.connections=one, two
 
remote.connection.one.host=localhost
remote.connection.one.port=6999
remote.connection.one.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false
 
remote.connection.two.host=localhost
remote.connection.two.port=7999
remote.connection.two.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false

As you can see we setup 2 connections "one" and "two" which both point to "localhost" as the "host" but different ports. Each of these connections will internally be used to create the EJB receivers in the EJB client context.

So that's how the jboss-ejb-client.properties file can be setup and placed in the classpath.

Using a different file for setting up EJB client context

The EJB client code will by default look for jboss-ejb-client.properties in the classpath. However, you can specify a different file of your choice by setting the "jboss.ejb.client.properties.file.path" system property which points to a properties file on your filesystem, containing the client context configurations. An example for that would be "-Djboss.ejb.client.properties.file.path=/home/me/my-client/custom-jboss-ejb-client.properties"

Setting up the client classpath with the jars that are required to run the client application

Starting JBoss AS 7.1.0.Final, a jboss-client jar is shipped in the distribution. It's available at JBOSS_HOME/bin/client/jboss-client-7.1.0.Final.jar. Place this jar in the classpath of your client application.

If you are using Maven to build the client application, then please follow the instructions in the JBOSS_HOME/bin/client/README.txt to add this jar as a Maven dependency.

Summary

In the above examples, we saw what it takes to invoke a EJB from a remote client. To summarize:

  • On the server side you need to deploy EJBs which expose the remote views.
  • On the client side you need a client program which:
    • Has a jboss-ejb-client.properties in its classpath to setup the server connection information
    • Either has a jndi.properties to specify the java.naming.factory.url.pkgs property or passes that as a property to the InitialContext constructor
    • Setup the client classpath to include the jboss-client jar that's required for remote invocation of the EJBs. The location of the jar is mentioned above. You'll also need to have your application's bean interface jars and other jars that are required by your application, in the client classpath