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).
- XA transaction support.
I need to be a bit more specific with this one, as most people think that XA only involves databases. XA can support different resource types such as message queues and enterprise connectors. For the most part, in order to communicate with some legacy systems, a JMS type system may be needed. The message transport tends to be part of a larger transaction which may involve database activity as well.
- Attribute oriented web page development (like Tapestry).
The only way that I have seen HTML customization done in the demonstrations I've seen are through RHTML. RHTML is basically HTML with Ruby code (shades of model-1 JSPs and old ASP pages). RHTML, JSP, ASP or ASP.Net pages are usually not viewable by the browser. I alluded to when I talked about my ideal web project. Although I hear there is an add-on for this.
- I18n support.
Being in Canada we have two official languages English and French. So if you have to develop applications for the government or companies that deal with Quebec, you bet i18n support is important. Although I hear there is an add on for this too.
- Single sign-on.
This is more the JAAS thing. If you've been in the industry long enough, you would have heard "How many times must we do the logon use case?" quite often. Although I have an extenstion to that which is "How many times must we do the user management use case?" Personally having to do that again isn't my idea of fun, and I'd rather delegate that to a product like Tivoli Identity Manager which I just get someone to customize the screens and leave all the backend user management stuff to the product. If you have a large enough project, you would probably save more money buying the product rather than waiting for it to be reimplemented again for your application.
- Database migration.
From what I saw with Rails, updates to the database tables are done manually by the Ruby developer. This would be quite a chore if there is a large number of table updates that need to be done. Hibernate provides Java developers with "schema-update" which generates the SQL needed to migrate existing database tables as needed (though you should still get your DBA to review it).
So with the exception of Tapestry and Hibernate, the JEE environment already provides us with the necessary infrastructure out of the box. One key thing that Rails has over Java language solutions is the on the hot code change support. That may ween a lot of people over, myself included. But, I like to have as much of the infrastructure done for me when I do work and I would like to delegate work out to experts as needed. Much ado about Grails ... So far my experience with Grails has been limited to just some well done screencasts. However, most of it was quite a bore (not because of the presenter, he did a really good job), but because it looks the same just replace Rails with Grails and Ruby with Groovy. So as far as innovation goes it does not really help much. As I was perusing through the screencasts, I found some things that I didn't really like:- Batch file/ant based application generation.
After using Maven for a while, I haven't found a better way of structuring your source files than they did. It may be overkill, but at least it works well (with the exception of web content). An older version of Trails did this as well, thankfully they outgrew that and moved to Maven.
- Groovy Server Pages (GSP)
It looks a lot like JSP files, which I would avoid because it cannot be sent back to the web development team once the Java guys put in all their tags and what not.
- Groovy
Yet another programming language.
- Batch file to create new domain object.
Rather than just creating a new class.
- Ant based build targets.
Rather than leveraging more standard targets we have a new set of targets to deal with. This could easily be fixed by moving the development to Maven.
- I have to enter the domain class twice?
Not sure why.
Like Rails, Grails support the on the hot code change support. Since Grails runs on top of a JEE stack, that may ween a lot more people over, myself included. However, it is not without its warts which I have listed above. So why Trails? Trails, although it is still in its infancy, is better geared for application development with a larger team and multiple roles (i.e., web developer, application developer) and can more easily leverage existing skills of Java developers. Here are the things that Trails has it going for: