01 July 2015

Fun with Struts 2

My team is building its first application in the Apache Struts 2 framework (it's actually a migration from a Struts 1 application). So we already have a substantial suite of actions already coded, organized into modules. The Struts 2 package concept handles our modules well.

We have standard logic that we want to execute for the invocation of any action (authentication, logging, timing, etc.), so I've recoded our Struts 1 filters as interceptors.

Then there is exception handling. It took me several readings of the documentation and various forum postings and some trial and error to get this working the way we need it, including being able to catch exceptions thrown by interceptors. I ended up deciding to pass the exception info directly to a view JSP, rather than an action class, but we might revisit that decision and do some refactoring.

The docs explain how to configure a handler to be applied globally to the entire application, and how to configure an action-specific handler. But in our case, we have one module that consists of actions that respond to Ajax requests, and hence render JSON, while the other modules render complete web pages with JSPs. The problem statement: how can you configure a Struts 2 global exception handler with the <global-exception-mapping> tag that will apply to a specific package, but will share the same interceptor stack with the rest of the application? For actions in our JSON module, we want error info returned as JSON payload, not HTML.

Well, it turns out that you can do it, but you have to be a little creative with the package inheritance hierarchy. What follows is a simplified (and slightly redacted) version of our actual application, so I can't vouch that this code is complete and accurate, but it should give you the general idea.

The key is to make a package just for the interceptors. Then every other package extends that package. The core package defines exception handlers and global results that all packages use, except the json package, which defines its own.



Here's the struts.xml (again, in our production solution, every package is defined in a separate file that is brought in with <include>, but it's easier to see what's going on with everything in one place):

<!DOCTYPE struts PUBLIC
          "-//Apache Software Foundation//DTD Struts Configuration 2.0//EN"
          "http://struts.apache.org/dtds/struts-2.0.dtd">
<struts>
    <constant name="struts.convention.default.parent.package" value="interceptors"/>
    <package name="interceptors" namespace="/" extends="struts-default">
        <interceptors>
            <interceptor name="appTracer" class="org.CLIENT.cmsui.interceptor.TracerInterceptor" />
            <interceptor name="appAuthentication" class="org.CLIENT.cmsui.interceptor.AuthenticationInterceptor" />
            <interceptor-stack name="appStack">
                <interceptor-ref name="exception">
                     <param name="logEnabled">true</param>
                     <param name="logLevel">ERROR</param>
                </interceptor-ref>
                <interceptor-ref name="appTracer">
                    <param name="message">===begin interceptor stack===</param>
                </interceptor-ref>
                <interceptor-ref name="appAuthentication" />
                <interceptor-ref name="defaultStack" >
                     <param name="exception.logEnabled">true</param>
                     <param name="exception.logLevel">ERROR</param>
                </interceptor-ref>
            </interceptor-stack>
        </interceptors>
        <default-interceptor-ref name="appStack" />
    </package>
    <package name="core" namespace="/" extends="interceptors">
        <global-results>
            <result name="generalExceptionResult">/jsp/GeneralException.jsp</result>
            <!-- results returned by authentication interceptor -->
            <result name="passwordWrongResult">/jsp/user/UserLogin.jsp?passwordWrong=true</result>
        </global-results>        
        <global-exception-mappings>
            <exception-mapping exception="java.lang.Exception" result="generalExceptionResult"/>
        </global-exception-mappings>
    </package>
    <package name="alpha" namespace="/alpha" extends="core" >
        <action name="EditAlpha" class="org.CLIENT.cmsui.alpha.action.EditAlphaAction" >
            <result name="Success">/jsp/alpha/EditAlpha.jsp</result>
        </action>
    </package>
    <!-- and similarly for packages bravo, charlie -->
    <package name="json" namespace="/json" extends="interceptors" >
        <global-results>
             <result name="generalExceptionResult">/jsp/GeneralException.jsp</result>
             <result name="jsonExceptionResult">/jsp/JsonException.jsp</result>
            <!-- results returned by authentication interceptor -->
            <result name="passwordWrongResult">/jsp/user/UserLogin.jsp?passwordWrong=true</result>
        </global-results>
        <global-exception-mappings>
            <exception-mapping exception="java.lang.Exception" result="jsonExceptionResult"/>
        </global-exception-mappings>
        <action name="GetLookupInfo"
            class="org.CLIENT.cmsui.json.action.GetLookupInfoAction" >                
            <result name="Success">/jsp/json/GetLookupInfo.jsp</result>
        </action>
 </struts>



A couple of things to point out:

  • Notice that we use <global-results> to account for results returned by interceptors, as well as the exception handling mechanism.
  • I consider it a blot that you have to define <global-results> in two places, in both the core and json packages, but that's apparently what you have to do.
  • I recommend creating a tracer interceptor as part of your tool kit. All that it does log a message (here, set as a configurable parameter). Writing the code is a good tutorial for creating your first interceptor from scratch, and once you've got it, you can use it to debug your interceptor stacks. I've only shown one stack in this example, but when your requirements call for alternative stacks, it's good to know that you're executing the one you want.

No comments: