To start, I'm quite sure there are several implementation of this pattern already. However, this is just one of three parts. The first part introduces an implementation of the Service Locator pattern that performs remote method invocation. The second part is short and puts optimizations into the implementation. The final part involves converting it into a PicoContainer ComponentAdapter.
Context
Services are implemented as Java objects that can be run and tested outside the context of a container, but using an EJB to provide separation of the service implementation from clients.
Problem
The use of services as Java Objects rather than EJBs allows for easier testability without using a container. The problem comes when using EJBs as a transport and ending up with several EJBs that only perform transport functionality as this will yield in duplicate code and a lot of configuration done through the EJB descriptors and manifests. Using the Service Locator pattern in conjunction with Dynamic Proxies would reduce the amount of EJBs needed to one.
Forces
- Reduce the number of EJBs that perform the same function of transporting data from client to the service implementation.
- Prevent cut and paste coding from the service interface to the EJB Remote interface (that require java.rmi.RemoteException thrown).
- Remove the need of hand-coded proxies for every service.
Solution overview
Create an EJB that instantiates implementations of services and stores them in a map. The EJB will have an exposed method
Serializable
invoke(Class serviceClassInterface,
String methodName,
Serializable[] args)that would invoke the method on the service class implementation instance. This single EJB would be used for all services.
Depending on the application server. we are no longer taking full advantage of the EJB Cache provided by the as there is only one entry. Also, the application server might not provide the capability to create a pool of stateless session beans.
You will also lose access to metrics based on EJB usage since it all goes to one. This is similar to the Struts framework where everything goes into one servlet.
A service locator to performs the EJB lookup functionality and retrieve an RMI client stub for the clients that can connect to the server.
Solution Step 1: Creating the service test case
In keeping with the Test First Development methodology, a test case is made that shows how we plan to use the service. Starting simple makes things easier to understand so the first test case shows how the service would be used without a container.
public void testUpperServiceNoContainer() {
UpperService upperService = new UpperServiceImpl();
assertEquals("ABCDE", upperService.upper("Abcde"));
}The implementation class implements the method as follows:
public String upper(String s) {
return s.toUpperCase();
}
Solution Step 2: Introducing the service locator
The service locator pattern is pretty simple, you send a request for a service and it will give you an object that you can use to manipulate the service. Unlike the Core J2EE pattern the implementation discussed here avoids the Singleton antipattern.
public void testServiceLocator() {
ServiceLocator locator = new MapServiceLocator();
UpperService upperService =
(UpperService) locator.getService(UpperService.class);
assertEquals("ABCDE", upperService.upper("Abcde"));
}
At this point the application would now fail to run since we do not have ServiceLocator implemented yet. The next few sections discuss how to build the ServiceLocator class.
Solution Step 3: Creating the Service Locator class
We start the service locator using a simple map that instantiates the implementation. This will make the test case pass.
public class MapServiceLocator implements ServiceLocator {
private Map serviceMap;
public MapServiceLocator() {
serviceMap = new HashMap();
serviceMap.put(UpperService.class, new UpperServiceImpl());
}
public Object getService(Class clazz) {
return serviceMap.get(UpperService.class);
}
}
Our next step would be to remove knowledge of UpperServiceImpl on the MapServiceLocator implementation.
Solution Step 4: Introducing the Invoker
Currently the ServiceLocator has knowledge of the actual implementation classes. Separating the knowledge of the implementation would allow packaging code so interfaces and transfer objects are the only ones provided to the client and the implementation can be put elsewhere. An approach will use Java's reflection API and use it as an indirect way of invoking a method. By using this approach it is possible to invoke a method based on information obtained at runtime rather at compile time. The following test case shows how an invoker method would be used.
public void testInvoke() {
Invoker invoker = new Invoker();
assertEquals("ABCDE",
invoker.invoke(UpperService.class,
"upper", new Serializable[] { "Abcde" }));
}As shown, the client has no concept of the actual implementation. Similar to that of the ServiceLocator. The implementation of Invoker is as follows:
public class Invoker {
private Map map;
public Invoker() {
map = new HashMap();
map.put(UpperService.class, new UpperServiceImpl());
}
public Serializable invoke(Class clazz, String mName, Serializable[] args) {
Object impl = map.get(clazz);
Class[] parameterTypes = new Class[params.length];
for (int i = 0; i < args.length; ++i) {
parameterTypes[i] = args[i].getClass();
}
Method method = clazz.getMethod(mName, parameterTypes);
return (Serializable)method.invoke(impl, args);
}
}
Implementing the method above would make the test case pass.
Solution Step 5: Changing the service method to use the invoker
The java.lang.reflect.Proxy class allows us to create a custom invocation handler that would intercept method calls and perform an action based on it. In our case, it should simply invoke the implementation method. The following test case demonstrates how clients would use it. Care has to be taken to ensure that only Serializable data gets transmitted otherwise if the service is running on a remote server it would cause problems. The following service locator has been converted to use the invoker. An invocation handler acts similar to the testInvoke method. The locator creates a proxy that uses the invocation handler which invokes the actual method.
public class InvokerServiceLocator implements ServiceLocator {
private Invoker invoker = new Invoker();
public Object getService(Class clazz) {
InvocationHandler handler = new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args){
Serializable[] serializableArgs = new Serializable[args.length];
System.arraycopy(args, 0, serializableArgs, 0, args.length);
return invoker.invoke(clazz, method.getName(), args);
}
};
return Proxy.newProxyInstance(clazz.getClassLoader(),
new Class[] { clazz }, handler);
}
}
When creating a new test case using the updated implementation, it should pass as is. The Invoker itself can be separated so an interface can be put into the client and the implementation can be put into the EJB tier. The next step would be to change Invoker to an EJB.
Solution Step 6: Creating a sample client
Creating a simple client that invokes the remote service would help in testing. Initially, the local Invoker would be used rather a remote one.
protected void init() {
getServletContext().setAttribute(ServiceLocator.class.getName(),
new InvokerServiceLocator());
}
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
ServiceLocator locator =
(ServiceLocator) getServletContext().getAttribute(
ServiceLocator.class.getName());
UpperService upperService =
(UpperService) locator.getService(UpperService.class);
resp.getWriter().println(upperService.upper("Abcde"));
}
Since this eventually would use an EJB call, it this client should have to be tested on a full J2EE container rather than using something like ServletUnit. It is suggested that an HttpUnit test is created for this one to ensure that it will work in an automated fashion.
Solution Step 7: Converting to an EJB
Converting the Invoker to be an EJB is a purely mechanical process. The constructor is moved to the ejbCreate() method and the boiler plate interfaces, descriptors and manifests are created.
public class InvokerBean implements SessionBean {
private Map map;
public void ejbCreate() throws CreateException {
map = new HashMap();
map.put(UpperService.class, new UpperServiceImpl());
}
public Serializable invoke(Class clazz, String mName, Serializable[] args) {
Object impl = map.get(clazz);
Class[] parameterTypes = new Class[args.length];
for (int i = 0; i < args.length; ++i) {
parameterTypes[i] = args[i].getClass();
}
Method method = clazz.getMethod(mName, parameterTypes);
return (Serializable)method.invoke(impl, args);
}
}
The interfaces can be implemented as Local or Remote or both. Its recommended that the local interface is used rather than remote, not because of efficiency (some application servers can convert to use pass-by-reference automatically if the EJB is in the same server), but because of security. Local interfaces ensure that the invoker gets used only by the application and not remote clients. Its a choice between not needing to change the code if they want remote or security.
Solution Step 8: Converting InvokerServiceLocator to use the EJB
The final step is to create an implementation of the InvokerServiceLocator to use the EJB instead of a local version.
public Object getService(Class clazz) {
InvocationHandler handler = new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) {
Serializable[] serializableArgs = new Serializable[args.length];
System.arraycopy(args, 0, serializableArgs, 0, args.length);
Context context = new InitialContext();
InvokerLocalHome =
(InvokerLocalHome) context.lookup("java:comp/env/ejb/Invoker"));
InvokerLocal invoker = home.create();
return invoker.invoke(clazz, method.getName(), serializableArgs);
}
};
return Proxy.newProxyInstance(clazz.getClassLoader(),
new Class[] { clazz }, handler);
}
When invoking the test client it should yield the same results.
Passing in additional data
On multi-tier applications, sometimes it is required to pass some contextual data stored in a ThreadLocal or other variable from one tier to another. The pattern allows to make the invoke method pass data that is not inside the method signature and send it over the wire. The EJB implementation class can create a new ThreadLocal to store the data or call a separate interface method that would store the contextual data. Although Dependency Injection would be better, ThreadLocal is an easy way of transmitting contextual data around without having to do any of the leg-work. The invoker method on the EJB tier can wrap the setting and clearing of ThreadLocal data as follows:
public Serializable invoke(Serializable contextData,
Class clazz, String mName, Serializable[] args) {
try {
setThreadLocalData(contextData);
Object impl = map.get(clazz);
Class[] parameterTypes = new Class[args.length];
for (int i = 0; i < args.length; ++i) {
parameterTypes[i] = args[i].getClass();
}
Method method = clazz.getMethod(mName, parameterTypes);
return (Serializable)method.invoke(impl, args);
} finally {
clearThreadLocalData();
}
}
Please note that if dependency injection is used or the services contain attributes, you might not use the map to store the services but instantiate them as needed.
Further Work
The implementation provided in this article has not been optimized for performance. There are several ways of improving this such as creating a cache of methods and caching the JNDI lookup. The list of implementation classes are also hard coded into the Maps and those can be made property file driven. All these techniques would be described in the next article (which I apologize that it had never materialized).