Programming Thoughts
J2EE - Miscellaneous
Sharing Session Data between Tomcat Apps

Sometimes, presentation data must be shared between Tomcat applications

In a microservice architecture, communication between services should happen in the business layer, not between Tomcat applications in the presentation layer. Thus, that Tomcat makes sharing data directly between applications difficult is normally a non-problem. Tomcat has Single Sign-On but this applies to shared authentication and authorisation roles, not other conversation state. Alas, occasionally, sharing conversation state, which is a presentation layer concern, can be useful.

For example, different public facing services can share back-end services, such as virtual currency transactions or phone line credit rates, but each has their own configuration on each back-end service, and each back-end service has its own front-end config pages. When an administrator wishes to configure one of the public facing services, he can enter that service, such as from the popup below.

Figure 1: Example public facing service selection

As he switches between config applications, he is presented with the config for the selected public facing service, not all of them, such as below.

Figure 2: Example configuration page for selected public facing service

The user's currently selected service can be stored in user data in business and persistence layer but this is inserting a presentation layer only concern into other layers. It also restricts the user to seeing only one service at a time.

How Tomcat gets in the way

Other applications' sessions can normally be accessed via ServletContext.getContext but Tomcat is security conscious and the app's Context in context.xml must have crosscontext set to true. See The Context Container.

Worse, each application uses its own classloader and can't recognise the same application classes found in other applications. See Class Loader How-To. Standard classes, such as Map and String, avoid this but bespoke classes are more useful. Such bespoke classes can be placed in Tomcat's lib directory, making them common to all applications, but that's best avoided.

Also, as each application is separate, other app's sessions may time out, losing its data.

Dealing without timeouts

A user can switch between from one application to another he hasn't used since logging on or its session data expired. Assuming such applications are integrated with Single Sign On, Tomcat will seamlessly copy authentication and authorisation roles but nothing else. The target application must find it without knowing the application previously in use. Instead of hunting each application, potentially missing new ones it doesn't know about, it's easiest to designate one as the common repository. Thus, the common application must be repeatedly refreshed by setting an attribute. Using a timer for refreshing would defeat the purpose of session timeouts, of course. Rather, it should happen when the user accesses shared data.

This leads to the following functions:-

/** * Returns whether shared session data can be set. If false, the client application is probably not cross context. */ private static boolean canSetSharedContext(HttpServletRequest request) { ServletContext sharedServletContext; sharedServletContext = request.getSession().getServletContext().getContext(SHARED_CONTEXT_PATH); return sharedServletContext != null; } /** * Returns shared session data, or null if not found. */ public static MySharedData getMySharedData(HttpServletRequest request) { MySharedData sharedMySharedData, appMySharedData, result; boolean canSetSharedContext; appMySharedData = (MySharedData)getDataFromAppSession(request, ATTR_NAME_MY_SHARED_DATA); canSetSharedContext = canSetSharedContext(request); if (canSetSharedContext) { sharedMySharedData = (MySharedData)getDataFromSharedContext(request, SHARED_CONTEXT_PATH, ATTR_NAME_MY_SHARED_DATA); } else { sharedMySharedData = null; } if (sharedMySharedData != null) { result = sharedMySharedData; } else if (appMySharedData != null) { result = appMySharedData; } else { result = null; } setDataForAppSession(request, ATTR_NAME_MY_SHARED_DATA, result); if (canSetSharedContext) { setDataForSharedContext(request, SHARED_CONTEXT_PATH, ATTR_NAME_MY_SHARED_DATA, result); } return result; }

Classloader incompatibility

The way to copy objects from one classloader to another is the same as from one JVM to another, namely serialization. This requires shared session data to be serializable but that's good practice anyway. The appropriate functions are:-

private static Serializable deserializeData(byte[] bytes) { if (bytes != null) { try (ByteArrayInputStream b = new ByteArrayInputStream(bytes)) { try (ObjectInputStream o = new ObjectInputStream(b)){ return (Serializable)o.readObject(); } catch (IOException e) { return null; } catch (ClassNotFoundException e) { return null; } } catch (IOException e) { return null; } } else { return null; } } private static byte[] serializeData(Serializable data) { if (data != null) { try (ByteArrayOutputStream b = new ByteArrayOutputStream()) { try (ObjectOutputStream o = new ObjectOutputStream(b)) { o.writeObject(data); } catch (IOException e) { return null; } return b.toByteArray(); } catch (IOException e) { return null; } } else { return null; } } private static Serializable getDataFromAppSession(HttpServletRequest request, String attributeName) { HttpSession session; session = request.getSession(false); if (session != null) { return deserializeData((byte[])session.getAttribute(attributeName)); } else { return null; } } private static Serializable getDataFromSharedContext(HttpServletRequest request, String sharedContextPath, String attributeName) { ServletContext sharedServletContext; SharedDataItem sharedDataItem; byte[] data; sharedServletContext = request.getSession().getServletContext().getContext(sharedContextPath); if (sharedServletContext != null) { data = (byte[])sharedServletContext.getAttribute(attributeName); return data; } else { return null; } } private static void setDataForAppSession(HttpServletRequest request, String attributeName, Serializable data) { request.getSession().setAttribute(attributeName, serializeData(data)); } private static void setDataForSharedContext(HttpServletRequest request, String sharedContextPath, String attributeName, Serializable data) { ServletContext sharedServletContext; byte[] data; sharedServletContext = request.getSession().getServletContext().getContext(sharedContextPath); if (sharedServletContext == null) { throw new IllegalException("Current context is not cross context context path=" + request.getContextPath()); } sharedServletContext.setAttribute(attributeName, serializeData(data)); }