Standalone JBoss EJB Client application - how to g

2019-02-21 01:40发布

问题:

There is a plethora of frustratingly incorrect (better description - "close but no cigar") information concerning remote access to JBoss EJBs from a standalone application. I've been beating my head against this wall for over a day with no success.

I'm trying to port an EJB from WebLogic to JBoss, which is called by a standalone application running on another server.

I've been here, here, and several other places chasing down various "solutions" to my problem without success. I've tried reading the official documentation which wants me to install a "quickstart" based on Maven, which may or may not fit my situation, and which so far I have decided not to pursue. (My project is not built with Maven, it uses Gradle, but I am reasonably certain that I've managed to get all the right dependencies deployed).

I have a stateful EJB deployed in a WAR inside an EAR (previous implementation of deploying it simply in a WAR did not help matters).

I configure the client thusly:

        public InitialContext createInitialContext() throws NamingException {
            Properties prop = new Properties();
            prop.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming");

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

            prop.put(Context.PROVIDER_URL, purl);
            prop.put(Context.SECURITY_PRINCIPAL, "myusername");
            prop.put(Context.SECURITY_CREDENTIALS, "mypassword");
            prop.put("jboss.naming.client.ejb.context", false);

            return new InitialContext(prop);            
        }

        public void closeContext(Context context) throws NamingException {
            if (context != null) {
                context.close();
            }
        }

        private String getJndiName(
                String prefix, 
                String appName,
                String moduleName,
                String distinctName,
                String beanName,
                Class viewClass, 
                boolean stateful) 
        {
            StringBuilder builder = new StringBuilder();
            if (prefix != null && prefix.length() > 0) {
                builder.append(prefix).append(':');
            }
            builder.append(appName)
                    .append('/')
                    .append(moduleName)
                    .append('/')
                    .append(distinctName)
                    .append('/')
                    .append(beanName).append('!')
                    .append(viewClass.getName());
            if (stateful) {
                builder.append("?stateful");
            }
            return builder.toString();
        }
        public Object lookup(Context context) throws NamingException {

            final String prefix = "ejb"; 
            final String appName = "myearname";
            final String moduleName = "mywarname";
            final String distinctName = "";
            final String beanName = "MyBean";
            final Class viewClass = MyBeanInterface.class;


            String jndi = getJndiName(prefix, appName, moduleName, distinctName, beanName, viewClass, true);


            return context.lookup(jndi);
        }

Note that no "distinct name" is provided as none is needed. "distinct name" is supposed to be optional: All of this gets invoked by:

                MyBeanInterface sstatus = null;
                try {
                    ctx = createInitialContext();
                    sstatus = (MyBeanInterface) lookup(ctx);

                } catch (Exception ex) {
                     ...
                }

When this code is invoked, the following error message is produced:

Caused by: java.lang.IllegalStateException: EJBCLIENT000024: No EJB receiver available for handling [appName:SockTransport, moduleName:SockTransport, distinctName:] combination
        at org.jboss.ejb.client.EJBClientContext.requireEJBReceiver(EJBClientContext.java:873) ~[ttjd.jar:?]
        at org.jboss.ejb.client.EJBClient.createSessionWithPossibleRetries(EJBClient.java:222) ~[ttjd.jar:?]
        at org.jboss.ejb.client.EJBClient.createSession(EJBClient.java:202) ~[ttjd.jar:?]
        at org.jboss.ejb.client.naming.ejb.EjbNamingContext.doCreateProxy(EjbNamingContext.java:227) ~[ttjd.jar:?]
        at org.jboss.ejb.client.naming.ejb.EjbNamingContext.createEjbProxy(EjbNamingContext.java:204) ~[ttjd.jar:?]

Using the above code, the JNDI name I am supplying is ejb:myearname/mywarname//MyBean!com.whatever.my.package.MyBeanInterface. Note the double slash caused by the missing distinctName. I can and have rejiggered this code to produce instead ejb:myearname/mywarname/MyBean!com.whatever.my.package.MyBeanInterface and this makes no difference.

Frankly, I think this error message is a red herring. I suspect that there is some other problem with my setup that is not being caught and breaking on this interface. I don't think the distinct name or lack thereof has anything to do with the problem. I think that's simply how they log the object that can't be looked up.

Before I go down the path of figuring out how to add a useless "distinct name" in a probably vain attempt to keep JBOSS happy, can someone venture a guess as to what the real problem may be?

UPDATE:

The suggestions of @Steve_C are quite illuminating but I still have not gotten them to work. He left a few points out of the initial context creation:

  • Context.URL_PKG_PREFIXES
  • Context.INITIAL_CONTEXT_FACTORY
  • "jboss.naming.client.ejb.context"

but these were mentioned in the resource he cited - very handy by the way.

So I added these and my createInitialContext method now looks like this:

    public InitialContext createInitialContext() throws NamingException {
        Properties prop = new Properties();
        prop.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming");
        prop.put(Context.INITIAL_CONTEXT_FACTORY,
                "org.jboss.naming.remote.client.InitialContextFactory");
        prop.put(Context.PROVIDER_URL, "http-remoting://{server-ip}:{server-port});
        prop.put("jboss.naming.client.ejb.context", true);
        return new InitialContext(prop);            
    }

Why PROVIDER_URL is necessary when I've already supplied server-ip and server-port in the jboss-ejb-client.properties file remains mysterious, but it makes a difference.

With these three items added to my initial context environment, now I get a different error message (EJBCLIENT000025 instead of EJBCLIENT000024):

java.lang.IllegalStateException: EJBCLIENT000025: No EJB receiver available for handling [appName:SockTransport, moduleName:SockTransport, distinctName:] combination for invocation context org.jboss.ejb.client.EJBClientInvocationContext@67f639d3
        at org.jboss.ejb.client.EJBClientContext.requireEJBReceiver(EJBClientContext.java:798) ~[ttjd.jar:?]
        at org.jboss.ejb.client.ReceiverInterceptor.handleInvocation(ReceiverInterceptor.java:128) ~[ttjd.jar:?]
        at org.jboss.ejb.client.EJBClientInvocationContext.sendRequest(EJBClientInvocationContext.java:186) ~[ttjd.jar:?]
        at org.jboss.ejb.client.EJBInvocationHandler.sendRequestWithPossibleRetries(EJBInvocationHandler.java:255) ~[ttjd.jar:?]
        at org.jboss.ejb.client.EJBInvocationHandler.doInvoke(EJBInvocationHandler.java:200) ~[ttjd.jar:?]
        at org.jboss.ejb.client.EJBInvocationHandler.doInvoke(EJBInvocationHandler.java:183) ~[ttjd.jar:?]
    at org.jboss.ejb.client.EJBInvocationHandler.invoke(EJBInvocationHandler.java:146) ~[ttjd.jar:?]
    at com.sun.proxy.$Proxy20.create(Unknown Source) ~[?:?]

I suppose this counts as progress, but I'm finding this more difficult than it needs to be. I wonder if these new properties need to be in the properties file, but the official documentation pretty clearly says they don't.

回答1:

The most flexible WildFly/JBossEAP remote EJB lookup and invocation can be done as follows:

Create a jboss-ejb-client.properties file which must be on the client classpath:

remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED=false
remote.connections=default
remote.connection.default.host=<ip of jboss eap host>
remote.connection.default.port = 8080
remote.connection.default.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false

The EJBCLIENT000024: No EJB receiver available for handling error message is a symptom of a missing jboss-ejb-client.properties file.

Create an InitialContext:

    Properties jndiProps = new Properties();
    jndiProps.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming");
    Context ctx = new InitialContext(jndiProps);

Note that no other properties are required.

Lookup the bean and call it:

    ServiceLogic beanRemoteInterface = (ServiceLogic) ctx.lookup("ejb:/WhizBangSessionEJB/WhizBangSessionEJB!com.whatever.hostinterface.ServiceLogic?stateful");
    String bar = beanRemoteInterface.sayHello();
    System.out.println("Remote Foo bean returned " + bar);

Note the ?stateful on the end of the JNDI name that is required for stateful EJBs.

Output:

Jan 11, 2017 11:07:46 PM org.jboss.ejb.client.EJBClient <clinit>
INFO: JBoss EJB Client version 2.1.4.Final
Jan 11, 2017 11:07:46 PM org.xnio.Xnio <clinit>
INFO: XNIO version 3.4.0.Final
Jan 11, 2017 11:07:46 PM org.xnio.nio.NioXnio <clinit>
INFO: XNIO NIO Implementation Version 3.4.0.Final
Jan 11, 2017 11:07:46 PM org.jboss.remoting3.EndpointImpl <clinit>
INFO: JBoss Remoting version 4.0.21.Final
Jan 11, 2017 11:07:46 PM org.jboss.ejb.client.remoting.VersionReceiver handleMessage
INFO: EJBCLIENT000017: Received server version 2 and marshalling strategies [river]
Jan 11, 2017 11:07:46 PM org.jboss.ejb.client.remoting.RemotingConnectionEJBReceiver associate
INFO: EJBCLIENT000013: Successful version handshake completed for receiver context EJBReceiverContext{clientContext=org.jboss.ejb.client.EJBClientContext@29ca901e, receiver=Remoting connection EJB receiver [connection=org.jboss.ejb.client.remoting.ConnectionPool$PooledConnection@5649fd9b,channel=jboss.ejb,nodename=steves-mbp]} on channel Channel ID ecac0ca6 (outbound) of Remoting connection 6536e911 to /192.168.12.6:8080 of endpoint "config-based-ejb-client-endpoint" <520a3426>
Remote Foo bean returned hello

More information can be found in Remote EJB invocations via JNDI - EJB client API or remote-naming project.

More sample code can be found in the QuickStart repo at wildfly/quickstart/ejb-remote

PS. If you really want to set the distinct-name then you need to add a jboss-ejb3.xml file to your EJB jar containing:

<jboss:ejb-jar xmlns:jboss="http://www.jboss.com/xml/ns/javaee"
               xmlns="http://www.jboss.com/xml/ns/javaee"
               xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
               xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-ejb3-2_0.xsd"
               version="3.1"
               impl-version="2.0">

    <distinct-name>something-distinct</distinct-name>
</jboss:ejb-jar>

Dynamic EJB Client Properties

If you need to be able to provision the jboss-ejb-client.properties dynamically then the simplest solution would be generate this file on the fly, possibly at client initialisation time.

  1. Set the jboss.ejb.client.properties.file.path system property to point at a secure writable file system location. An insecure example might be something like

    -Djboss.ejb.client.properties.file.path=/tmp/whizbang-ejb.properties

    or

    System.setProperty("jboss.ejb.client.properties.file.path", "/tmp/whizbang-ejb.properties");

  2. Generate a properties file named with the String defined by jboss.ejb.client.properties.file.path according to the format described for jboss-ejb-client.properties files.

  3. Proceed with InitialContext creation

There are other alternatives that involve hacking the provided jboss-ejb-client code. However you need to remember that this is LGPL code and you and your company would need to make your hacks publicly available.



回答2:

Before sharing what I learned I want to give a big shout-out to @Steve_C who went way beyond the call of duty in helping me, including a lengthy chat session. In case anyone wonders, he is not ME, by the way.

Meaning no disrespect to @Steve_C or his answer (which I have upvoted as useful), there is more to be said here, as I've learned from very painful experience.

Here are some things I have learned:

1) It is necessary to have a jboss-ejb-client.properties file.
2) This file can be located either on the classpath or specified in a location specified by the following System property, which I set just prior to invoking the InitialContext constructor:

    System.setProperty("jboss.ejb.client.properties.file.path", "/path/to/properties/file");
    return new InitialContext(prop);    

3) This file must name the connections:

remote.connections=conn1,conn2

4) For each connection named in the above property, host and port entries must be stored in the properties file

remote.connection.conn1.host=10.0.0.1
remote.connection.conn1.port=8080
remote.connection.conn2.host=10.0.0.2
remote.connection.conn2.port=8080

5) For each connection named, there must also be some method of authentication specified, either a)

remote.connection.conn1.username=user1
remote.connection.conn1.password=topSecret
remote.connection.conn2.username=user2
remote.connection.conn2.password=open_sesame

or b)

remote.connection.conn1.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false
remote.connection.conn2.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false

which literally indicates that anonymous no-password invocations are not disallowed. (Let's hear it for double negatives!) I suppose it would be theoretically possible to have one connection using a password and another allowing anonymous login but I can't imagine why. However this is done, it must be specified connection by connection. There is an incorrect example on the web that includes both

remote.connection.conn2.connect.options.org.xnio.Options.SASL_POLICY_NOANONYMOUS=false

and username/password properties for the connection. The net effect of this is that the credentials can be incorrect and you still get login. I tried this and found it to be the case.

6) In spite of appearances, the specifying of java.naming.provider.url is necessary. It would be nice if JBoss could figure this out from the connection host and port properties mentioned above, but it can't! There may or may not be a good reason for this, I simply do not know.

Annoyingly, this CANNOT be specified in the Properties file. This seems to be a bug in the JBoss client. Since ":" is equivalent to "=" in the Java properties file specification, it is impossible to store URLs there with the http-remote:// notation or any url with the colon slash slash. The colon must be escaped with a backslash but evidently the JBoss client code is not calling Properties.load() to resolve the escaping correctly, but rather attempting to read it line for line? So this one must be specified in the Properties passed to the InitialContext creation. I have tried both ways and found that specify it in code works, whereas specifying it in a properties file doesn't.

So we have the unfortunate situation that there are two methods of supplying data to the InitialContext, some by properties file and some in the initial environment Hashtable passed to the InitialContext constructor. Some things must be done in one place, and other things must be done in the other. And none of this is properly documented.