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.

3 comments:

Anonymous said...

It was extremely interesting for me to read this post. Thanks for it. I like such topics and anything connected to them. I definitely want to read more soon.
Alex
Cell jammers

Hazard said...

If this shit works I'll fedex you some original easter hoookers... Now where the fuck did you find the JNDICacheableFactory?? It doesn't seem to exist outside your blog.. :'-(

erandi said...

HI,

Is there any location to findout JNDICacheableFactory ??

Appriciate your quick response on this