Saturday, April 5, 2008

Struts 2 session replication


Very often the following task occuring: saving and distributing session between web applications. In this post, implemented one of the strategies of saving and distributing session for web framework Struts 2.


Used technologies : EJB3, Struts-2, Tomcat, Jboss


So we have task for linking several web applications with Session Manager. One of the restrictions in this implementation is usage of subdomains of domain e.g. *.domain.com. So we can use domain cookies (this restriction can be taken off with URL rewriting, but in this implementation we use cookies ). Because existed system was build on Struts 2 web framework, we decided to use internal capabilities of this framework. As we will see further, Struts 2 has very exciting futures called Interceptors, and they help us to get rid of crosscutting concernes.
Session Manager is a EJB3 component with simple interface, for simplicity only subset of methods will be shown with descriptive methods signature :



@Remote
public interface SessionManagerRemote {
void extendActivity(String sessionId) throws SessionExpiredException;
String registerActivity();
void unregister(String sessionId);
Long getSessionDuration(String sessionId);
Map getSession (String sessionId) throws SessionExpiredException;
void putSession (String sessionId,Map sessionObjects)throws SessionExpiredException;
}

This component will be deployed on J2EE Application Server, e.g. JBOSS. Jboss implementation following:



@org.jboss.annotation.ejb.Service(objectName = "jboss:managementService=SessionManager")
@Remote(SessionManagerRemote.class)
@org.jboss.annotation.ejb.RemoteBinding(jndiBinding = "com/project/management/session/remote")
public class SessionManager implements SessionManagerRemote {
}

Service annotation is used for Singleton implementation in Jboss runtime.


So, back to main task. We need to replace Struts 2 session implementation with our distributed session, controlled by session manager. But for make such serious session replacement we must understand architecture of Struts 2 framework. First we must detect how we use session in Struts 2 application :




    • OGNL expressions with #session identifier e.g. <s:property:value="#session.PARAMETER_IN_SESSION"/>

    • Actions that implement SessionAware interface :




public class TempAction implements SessionAware {
private Map session;
public void setSession(Map map) {
this.session = map;
}
}

So we must create our interceptor that



  1. initialize OGNL value stack with our distributed session

  2. inject our distributed session in Actions that implements SessionAware interface


According to Struts 2 source code, Dispatcher initialize OGNL value stack, and session in particular:



extraContext.put("session", sessionMap);

and ServletConfigInterceptor injects session in Action classes.So we override such behaviour as following :



//initializing session in OGNL value stack
actionInvocation.getInvocationContext().getContextMap().put("session",distributedSession);
//injecting our session
if (action instanceof SessionAware) {
((SessionAware) action).setSession(distributedSession);
}

So final configuration is following. struts.xml :


<struts>
<package name="default" extends="struts-default">
<interceptors>
<interceptor name="distributedSession" class="com.interceptors.SessionCheckerInterceptor"/>
<interceptor-stack name="distributedSessionStack">
<interceptor-ref name="basicStack"/>
<interceptor-ref name="distributedSession"/>
</interceptor-stack>
</interceptors>
<default-interceptor-ref name="distributedSessionStack"/>
</package>
<package name="test" extends="default" namespace="/actions">
<action name="logon" class="com.actions.SomeTemporaryAction" method="login">
<result name="success" type="redirectAction">myaccount</result>
<result name="input" type="redirect">/</result>
</action>
</package>
</struts>



We call interceptor distributedSession, and it must be in list after basicStack in order to work consistently.
Final version of our Interceptor :



public class SessionCheckerInterceptor extends AbstractInterceptor {
public String intercept(ActionInvocation actionInvocation) throws Exception {
HttpServletRequest request = ServletActionContext.getRequest();
String distributedSession = processCookie(request);
String result = null;
Map sessionBeforeInvokation = JNDICacheableFactory.getSessionManager().getSession(distributedSession);
Object action = actionInvocation.getAction();
if (action instanceof SessionAware) {
((SessionAware) action).setSession(sessionBeforeInvokation);
}
actionInvocation.getInvocationContext().setSession(sessionBeforeInvokation);
actionInvocation.getInvocationContext().getContextMap().put("session", sessionBeforeInvokation);
int beforeInvHash = sessionBeforeInvokation.hashCode();

result = actionInvocation.invoke();

Map sessionAfterInvocation = actionInvocation.getInvocationContext().getSession();

int afterInvHash = sessionAfterInvocation.hashCode();

if (afterInvHash != beforeInvHash) {
JNDICacheableFactory.getSessionManager().putSession(distributedSession, sessionAfterInvocation);
}
return result;
}
}

Note: method processCookie omited, in this method cookie processing logic implemented, but if we’d want to realize cross-domain logic we might use URL rewriting for example (in our case we use cookies because we use subdomains of similar domain).


Note: for checking updates in session we compare their hashes, but other implementations are possible. If session changed it is updated.