JAX-WS (+ WSS4J for the security) is a quite common solution used by enterprises but JAX-WS is quite slow by design (pipeline, xml serialization, depending on implementation a lot of reflection or bytecode generation in some cases…) and the question to compare it to a simpler protocol comes pretty quickly when you held both client and server sides.
This article aims to show how JAX-WS could be replaced by EJBd protocol on TomEE (>= 1.6.0 – currently in snapshot when writing these lines).
EJBd protocol is the proprietary protocol used by TomEE/OpenEJB when doing @Remote EJBs. The protocol is based on RMI and is pretty simple (if you’d need to write your own client server you’d probably do something close).
How to deploy existing @WebServices as @Remote
The first question is how do I deploy my webservices as remote EJB? You can of course update your code or simply add a flag in conf/system.properties:
openejb.jaxws.add-remote = true
It will simply add the webservice interface (means it doesn’t work with @LocalBean) as a @Remote interface.
How do i handle my parameters and returned value when not Serializable?
Since EJBd is based on RMI it needs to use Serializable parameters and returned values. Since that’s rarely the case with a JAX-WS service we need a solution to avoid NotSerializableException exceptions.
The solution on the server side is to pass to EJBd service (EjbServer/EjbDaemon if you know its little name ;)) a “serializer”. On the client side you’ll just do the same when creating a client. The serializer will implement org.apache.openejb.client.serializer.EJBDSerializer. Here is a serializer using jackson to use JSon format:
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.openejb.client.serializer.EJBDSerializer; import java.io.IOException; import java.io.Serializable; public class JacksonSerializer implements EJBDSerializer { // static since instances are created for container + for each client private static final ObjectMapper mapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); @Override public Serializable serialize(final Object o) { try { return mapper.writeValueAsString(o); } catch (final JsonProcessingException e) { throw new RuntimeException(e); } } @Override public Object deserialize(final Serializable serializable, final Class<?> aClass) { try { return mapper.readValue(String.class.cast(serializable), aClass); } catch (final IOException e) { throw new RuntimeException(e); } } }
To configure the serializer usage on server side just ensure to add the system property:
ejbd.serializer = org.foo.serializer.JacksonSerializer
To configure it on the client side just add to the client properties (passed to the initial context) openejb.ejbd.serializer property:
final Context context = new InitialContext(new Properties() {{ put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.RemoteInitialContextFactory); put(Context.PROVIDER_URL, "http://localhost:8080/tomee/ejb"); put("openejb.ejbd.serializer", JacksonSerializer.class.getName()); }});
And the security?
Then you webservice is probably secured by WSS4J so how to secure the EJBd call? First you need to use JAAS. To do so you first need to create a custom LoginModule which reproduce what the WSS4J log-in does (here is a mock one allowing foo/bar to be logged in):
import javax.security.auth.Subject; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.NameCallback; import javax.security.auth.callback.PasswordCallback; import javax.security.auth.callback.UnsupportedCallbackException; import javax.security.auth.login.LoginException; import javax.security.auth.spi.LoginModule; import java.io.IOException; import java.util.Map; public class FooBarLoginModule implements LoginModule { private CallbackHandler callbackHandler; @Override public void initialize(final Subject subject, final CallbackHandler callbackHandler, final Map<String, ?> sharedState, final Map<String, ?> options) { this.callbackHandler = callbackHandler; } @Override public boolean login() throws LoginException { final Callback[] callbacks = new Callback[] { new NameCallback("Username: "), new PasswordCallback("Password: ", false) }; try { callbackHandler.handle(callbacks); } catch (final IOException ioe) { throw new LoginException(ioe.getMessage()); } catch (final UnsupportedCallbackException uce) { throw new LoginException(uce.getMessage() + " not available to obtain information from user"); } return "foo#bar".equals(NameCallback.class.cast(callbacks[0]).getName() + "#" + new String(PasswordCallback.class.cast(callbacks[1]).getPassword())); } @Override public boolean commit() throws LoginException { return true; } @Override public boolean abort() throws LoginException { return true; } @Override public boolean logout() throws LoginException { return true; } }
Then configure your LoginModule creating a file conf/login.config:
FooBar { org.superbiz.FooBarLoginModule required; };
Then activate this JAAS config file adding the system property:
-Djava.security.auth.login.config=${CATALINA_HOME}/conf/login.config
Finally add to your initial context properties the user, password and realm (JAAS one):
final Context context = new InitialContext(new Properties() {{ put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.RemoteInitialContextFactory); put(Context.PROVIDER_URL, "http://localhost:8080/tomee/ejb"); put("openejb.ejbd.serializer", JacksonSerializer.class.getName()); put("java.naming.security.principal", "foo"); put("java.naming.security.credentials", "bar"); put("openejb.authentication.realmName", "FooBar"); }});
Now you’ll be logged when doing the lookup of your remote bean (the name will be in startup logs if you don’t know which one will be used, by default it is [simple name of the implementation]Remote).
Compared to JAX-WS the login is done in another request than the invocation (done at lookup then you can do all the invocations you want the security context will be reassociated with the client while in EJBs (EJBContext for instance) but if your security uses ThreadLocal it is not guaranteed to work – it will surely not work very often). To avoid it you have to add another property to the client context properties:
final Context context = new InitialContext(new Properties() {{ put(Context.INITIAL_CONTEXT_FACTORY, "org.apache.openejb.client.RemoteInitialContextFactory); put(Context.PROVIDER_URL, "http://localhost:8080/tomee/ejb"); put("openejb.ejbd.serializer", JacksonSerializer.class.getName()); put("java.naming.security.principal", "foo"); put("java.naming.security.credentials", "bar"); put("openejb.authentication.realmName", "FooBar"); put("openejb.ejbd.authenticate-with-request", "true"); }});
This will ask OpenEJB to login when doing an invocation and logout after the invocation (using your custom LoginModule).
Now you just have to replace your clients to use EJBd instead of JAX-WS and that’s done!
Conclusion
On a very trivial webservice with few classes the EJBd solution is already 3 times faster than the JAXWS one (you can hope far more for real life webservices). The harder thing will be to replace all the client code to use EJB APIs instead of JAX-WS client API. With CDI it is quite easy to replace a producer but if you were creating your client deep in your code it will need some harder refactoring.