The Problem Scenario:
In one of our assignments, we faced a problem in making communication between two different spring based web applications which are in the same web container. Let us explain it with an example; say there are two web applications ‘Foo’ and ‘Bar’ deployed in the same web container and ‘Foo’ depends on ‘Bar’ to access a method say ‘barMethod()’ of ‘BarService’ ( a Java Class present inside the ‘Bar’ application), then
- The ‘Bar’ application servlet must provide a method to access its Java classes
- The ‘Foo’ application must get the response from Bar application through HTTP.
Problem#1: Though both the applications reside inside the same container and ‘Foo’ application knows what method of the Java class of ‘Bar’ application it has to access, it has to go through the network using HTTP protocol as if ‘Foo’ application request is considered as a client request to ‘Bar’ application as shown in the figure. Is there an optimal way to avoid the HTTP protocol overhead?
- Foo-Bar communication with out Adapter
Problem#2: Say a client (web, mobile etc..) invokes ‘fooMethod()’ of ‘FooService’ which internally calls ‘barMethod()’ of ‘BarService’ through ‘FooServlet’. And say the transaction involved in ‘Foo’ succeeds and the transactions involved in ‘Bar’ fails, then the first transaction gets committed and the second gets rolled-back as the participating threads are different. It means that according to the client, the participating transactions are not atomic as they get partially committed. Is there a way to make them atomic?
The Solution:
Yes..The solution what we found is to use an ‘adapter’ code in a cross-context enabled web container as shown in the figure, which would help in solving both the problems discussed.
- Foo-Bar communication With Adapter
Enabling cross-context:
Cross-Context is a feature provided by web containers and enabling configuration varies from container to container. For example, in Tomcat, the ‘crossContext’ attribute in ‘Context’ element of ‘TOMCAT_HOME\conf\context.xml’ must be set to true, to enable cross-context communication as shown below. By enabling this, one web application can get access to other web application.
________________________________________________________________
<Context crossContext="true">
<!-- Default set of monitored resources -->
<WatchedResource>WEB-INF/web.xml</WatchedResource>
</Context>
________________________________________________________________
The Adapter:
Once the ‘Bar’ context is obtainable in ‘Foo’, we can easily get the ‘Bar’ application spring beans using which we can access the ‘barMethod’. Yes!. That’s exactly is what the adapter does. Let us see how to invoke the ‘barMethod’ from ‘Foo’ context in two different sections. The first section explains the adapter functionality if the method invocation involves only simple data types (like int, float, String) as the method arguments/ return type and the second section explains the adapter functionality if the method invocation involves custom data types like (Person, Address etc…).
Handling Simple Data Types
The adapter does the following, before accessing any of the ‘Bar’ application Java classes (‘BarService’),
- Saves the current class loader (which loaded the ‘Foo’ application) in a temporary variable
- Sets the current class loader to the one which loaded the ‘Bar’ web application.
- Resets the current class loader with the one we saved in the temporary variable once it has finished accessing the Java Class of ‘Bar’ application.
- Let us see the adapter code.
___________________________________________________________________________________________
String methodName = request.getParameter(WEBConstants.PARAM_METHOD); ServletContext srcServletContext = request.getSession().getServletContext(); ServletContext targetServletContext = srcServletContext.getContext("/Bar"); //save the class loader which loaded the 'Foo' application in a variable ClassLoader currentClassLoader = Thread.currentThread(). getContextClassLoader(); try { Object object = targetServletContext.getAttribute ("org.springframework.web.servlet.FrameworkServlet.CONTEXT.bar"); // get the class loader which loaded the 'Bar' application ClassLoader targetServiceClassLoader = object.getClass().getClassLoader(); // and set it as the current context class loader. Thread.currentThread().setContextClassLoader(targetServiceClassLoader); Class<?> classBarService = (Class<?>) targetServiceClassLoader.loadClass ("com.pramati.crosscontext.service.BarService"); Method getBeanMethod = object.getClass().getMethod ("getBean", String.class); // Get the barService defined in the 'Bar' application context. Object barService = getBeanMethod.invoke(object, "barService"); // Get the method of the 'barService' Method targetMethod = classBarService.getMethod(methodName, (Class[]) null); if (targetMethod == null){ response.getWriter().println( "Error: The method['" + methodName + "' does not exist" ); return null; } // Invoke the method on 'barService' Object responseFromBarMethod = targetMethod.invoke(barService, (Object[]) null); } catch (Exception e) { e.printStackTrace(); } finally { Thread.currentThread().setContextClassLoader(currentClassLoader); }
_______________________________________________________________________
If we do not have these steps done, then we end up in getting ‘ClassCastException’ though we cast the right object with right type. Why? It is simple, the class loaders which loaded the ‘Foo’ application and ‘Bar’ application are different.
Handling Custom Data Types
Are there any changes if we need to pass custom type (Person/ Address etc..) parameters while invoking the method on the other context?. Absolutely Yes; we have changes to be made. There are two ways to fix this scenario.
Solution-1: If we could externalize the custom data type classes which are accessed by multiple web applications (here accessed by ‘Foo’ and ‘Bar’) to a different library and place it inside the commons library location of the web container (In case of tomcat, it is <TOMCAT_HOME>\lib), then we can pass the parameters as we did for simple data types without any source code modification.
Solution-2: If we can not externalize the common custom types then the custom data type objects which are going to be used as parameters must be serialized and deserialized say using JSON JavaScript Object Notation (or XML etc..). By including the below snippet to the adapter, we can do it.
___________________________________________________________________________________________
Person person = new Person(1, "Kayal"); ObjectMapper jacksonMapper = new ObjectMapper(); // serialize the param 'Person' with the 'Foo' application jacksonMapper String serializedPerson = jacksonMapper.writeValueAsString(person); Class<?> classPerson = (Class<?>) targetServiceClassLoader.loadClass ("com.pramati.crosscontext.model.Person"); // Get the jacksonMapper defined in the 'Bar' application context. Object targetJacksonMapper = getBeanMethod.invoke(object, "jacksonMapper"); //Get the 'readValue' method of the 'Bar' application's jacksonMapper Method readValueMethod = targetJacksonMapper.getClass().getMethod ("readValue", String.class, Class.class); //Deserialize Person using 'readValue()' of the 'Bar' application's jacksonMapper Object deserializedPerson = readValueMethod.invoke(targetJacksonMapper, serializedPerson, classPerson); // invoke 'barMethodWithParam' of 'BarService' with the deserialized Person responseFromBarMethod = targetMethod.invoke(barService, new Object[] { deserializedPerson}); // Get the 'writeValueAsString' method of the 'Bar' application's jacksonMapper Method writeValueAsStringMethod = targetJacksonMapper.getClass(). getMethod("writeValueAsString", Object.class); //serialize response using 'writeValueAsString' of 'Bar' appln's jacksonMapper responseFromBarMethod = (String) writeValueAsStringMethod.invoke (targetJacksonMapper, responseFromBarMethod); ___________________________________________________________________________________________
With this,
- Able to improve performance by avoiding the HTTP protocol overhead.
- Able to achieve atomicity in transactions.
How? The transactions are cached by the Spring Tx. Manager using ‘Thread Local’ concept which are specific to threads. If we have communication between applications using the adapter, the threads and the participating transactions are same; hence is atomic.