ALTERthought Blogs

29 June 2006

Integrating Spring and the Struts Validator (transparently)

A client recently approached my company looking for specialized training/mentoring on how to introduce the Spring framework into their Struts fronted J2EE applications. One of the topics of particular interest was a desire to use Spring to (somehow) apply the same validation rules that are implemented in the Struts Validator for their UI to the back-end business services (defensive programming and all that bit, or perhaps the business services are fronted by some other form of interface, like a web service.) Oh yeah, in addition it would be nice if the integration was a transparent as possible– and by that we mean that the underlying business service implementations should not have to explicitly call any validation routines. Well it has been a bit since I had done any Struts stuff, as I now favor WebWorks for my MVC, but what the heck, lets give it a shot.

OK. Lets take a simple example to further illustrate the point at hand. Lets say we have a simple web based form with the following basic fields: name, address, phone, email, and age.

This form will submit to an action which then immediately delegates to a trivial business service.

Lets first define the Business Service in question. It looks like this:


import com.alterthought.train.ui.forms.SimpleForm;

public interface SimpleBizIfc
{
    public void doSomeBusiness(SimpleForm a_form);
}

Thats it. We have a single method that takes a single argument. One thing to note is that for brevity, general laziness and simplification, our business service takes a SimpleForm object, which in turn is derived from a Struts Form bean. THIS IS NOT RECOMMENED FOR REAL-WORLD STUFF. It too closely ties your business interfaces to Struts. I am doing it here for convienence. It should also be noted that many people now favor DynaForms, but once again, I am doing an example here…

Here is the Struts form bean in question:

public class SimpleForm extends ValidatorForm
{
    private String name;
    private String address;
    private String phone;
    private String email;
    private Integer age;

  // Accessors here... snipped for space saving...
    ...
}

The Old Crusty Way

A standard (non-spring) strutsy implementation where this form is validated using the Struts Validator might look as follows:
struts-config.xml file:


<form-beans>
  ...
  <form-bean name="simpleForm" type="com.alterthought.train.ui.forms.SimpleForm"/>
  ...
</form-beans>
...
<action-mappings>
  ...
  <!--  Basic Struts action. No Spring awareness whatsoever. -->
  <action path="/simpleConventionalAction"
      type="com.alterthought.train.ui.actions.SimpleConventionalAction"
      name="simpleForm"
      scope="request"
      validate="true"
      input="/validator.do" >
    <forward name="success" path="/success.jsp" />
  </action>
  ...

Note that we request that our form be validated by setting validate to true. I’m not going to include it here, but we would obviously need the appropriate entry in our validation.xml file that maps to our form name (simpleForm) that indicates what sort of field level validation we wish to perform.

OK, and (finally) now the SimpleConventionalAction class that we are going to submit our form to:


public class SimpleConventionalAction extends Action
{

    public ActionForward execute(
        ActionMapping mapping,
        ActionForm form,
        HttpServletRequest request,
        HttpServletResponse response) throws Exception
    {

      /*
       * Conventional (old-fashioned) way of obtaining handle to business service.
       */
      SimpleBizIfc svc = new SimpleBizImpl();

      SimpleForm sf = (SimpleForm)form;

      svc.doSomeBusiness(sf);
      return mapping.findForward ("success");
    }
}

The above should be pretty digestible; as we are not doing anything out of the ordinary. We instantiate a new Business service by directly constructing it, and then we pass the form bean to the business method in question. No Spring or any fanciness.

The Shiny New Way

OK. Now that we have the old-fangled approach, lets first Spring-ify struts and get to business. There are a bunch of good articles on how to get Struts and Spring running together, and as such I am not going spend alot of time aspect of this effort. For more info try here or here.

Our solution will use Spring to wire up our Struts Action implementation to a ProxyBean which can transparently intercept our method calls and perform validation. The Validator integration itself is courtesy of the SpringModules project, and this is what our interceptor will use to perform the service side validation.

First add the following to the appropriate section in your web.xml file:


...
<!-- Spring params -->
<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>classpath:/applicationContext.xml </param-value>
</context-param>

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
...

And then we need the applicationContext.xml file that is referenced in the contextConfigLocation parameter (this will live in your wars WEB-INF/classes dir). Rather then give you the whole file at once, I will present excerpts of it inline, so we can talk about the various sections.

Lets start with the Struts action itself. We will add a new entry to the struts-config.xml file that delegate the Action implemention to Spring.


<action path="/spring_betterSpringAction"
    type="org.springframework.web.struts.DelegatingActionProxy"
    name="simpleForm"
    scope="request"
    validate="true"
    input="/validator2.do" >
  <forward name="success" path="/success.jsp" />
</action>

And now lets look at the relevant entries to our applicationContext.xml file:



<bean name="/spring_betterSpringAction"
    class="com.alterthought.train.ui.actions.BetterSpringAction">
  <property name="svc">
      <ref bean="validatingBizSvc"/>
  </property>
</bean>

There are a couple of interesting things about the above entrys:

  1. We set our action type to be a Spring DelegatingActionProxy. This is the glue between the Struts controller and Spring.
  2. We use the simple (and aribtrary) naming convention of preappending ‘spring_‘ to the action path to reenforce in our example that this Action is delegated to Spring.
  3. The path defined for the action in the struts-config.xml file needs to exactly match the corresponding Spring bean name entry in the applicationContext.xml file. Notice I said name and not the more typical bean id. This is because the slash character (/) is illegal in the id field.
  4. The Spring bean itself can be seen to use a validatingBizSvc bean, which we will talk about shortly.

And here is the straight forward implementation of our business service (the only difference between this one and the initial ConventionalSpringAction is that we use Spring/IoC to wire a handle to our service delegate):


public class BetterSpringAction extends Action
{
    private AnotherSimpleIfc svc;

    public ActionForward execute(
        ActionMapping mapping,
        ActionForm form,
        HttpServletRequest request,
        HttpServletResponse response) throws Exception
    {

        SimpleIBizIfc svc = getSvc();

        SimpleForm sf = (SimpleForm)form;
        svc.doSomeBusiness(sf);
        return mapping.findForward ("success");
    }

    public AnotherSimpleIfc getSvc()  {   return svc;}

    public void setSvc(AnotherSimpleIfc svc) { this.svc = svc;}
 }

Now lets take a look at the validatingBizSvc bean entry from the applicationContext.xml file:


<bean id="validatingBizSvc" class="org.springframework.aop.framework.ProxyFactoryBean">
  <property name="target">
    <ref local="validatingBizSvcTarget" />
  </property>
  <property name="proxyInterfaces">
    <value>com.alterthought.train.biz.SimpleBizIfc</value>
  </property>
  <property name="interceptorNames">
    <list>
      <value>validationInterceptor</value>
    </list>
  </property>
</bean>

<bean id="validatingBizSvcTarget" class="com.alterthought.train.biz.ValidatingBizImpl"/>

<bean id="validationInterceptor" class="com.alterthought.train.sys.ValidationInterceptor">
  <property name="validator" ref="beanValidator"/>
</bean>

The validatingBizSvc bean is in fact a Proxy that targets the validatingBizSvcTarget bean and implements the underlying SimpleBizIfc interface, and uses the bean validationInterceptor as its sole interceptor. Huh? Say what? Come again? What we are doing is using Spring to dynamically introduce a Proxy that masquerades as our Business Service. We can then set up an interceptor stack (among other things) that allows us to introspect the method invocation before (and after) we ever even call the real business service implementation. Remember the client asked for transparent validation. Using a method interceptor we can inject our own logic with the business service being none the wiser. We will scutinize the implementation of the ValidatingInterceptor in a bit.

The last remaining bits are the beans used by our interceptor for the validation itself. Once again, back to the applicationContext.xml file:


<bean id="validatorFactory"
    class="org.springmodules.validation.commons.DefaultValidatorFactory">
  <property name="validationConfigLocations">
    <list>
      <value>/WEB-INF/spring-validator-rules.xml</value>
      <value>/WEB-INF/validation.xml</value>
    </list>
  </property>
</bean>

<bean id="beanValidator"
    class="org.springmodules.validation.commons.DefaultBeanValidator">
  <property name="validatorFactory" ref="validatorFactory"/>
</bean>

Some further explaination on the above:

  1. The beanValidator bean uses the SpringModules project to integrate with the Commons Validator.
  2. We tell the validatorFactory where to find its configuration data and we feed it two files
  3. MUY IMPORTANTE: The first file contains the validator rules themselves and is NOT the same as the standard Struts validator-rules.xml file. It has been altered to use the SpringModules stuff. You can get this file here
  4. The second file has the form validation mappings and IS the exact same file that the Struts Validator uses. This is how we reuse our validation rules between the presentation and the business tiers.

The only thing left is to check out the implementation of our ValidationInterceptor:


import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.struts.action.ActionForm;
import org.springframework.validation.BindException;
import org.springmodules.validation.commons.AbstractBeanValidator;

public class ValidationInterceptor implements MethodInterceptor
{

    private AbstractBeanValidator validator;

    /**
     * Interceptor to validate our form. This is a quick and dirty impl.. there
     * are numerous issues with it, not the least of which is the tight coupling
     * between Struts ActionForms and a Business Interceptor.
     *
     */
    public Object invoke(MethodInvocation invocation) throws Throwable
    {
        Object args[] = invocation.getArguments();

        /**
         * OK. This is obviously fairly brittle and niave, for example
         * the form name is hardcoded here... etc.
         */
        for (int i = 0; i < args.length; i++) {
            Object object = args[i];

            if ( object instanceof ActionForm ) {
                ActionForm form = (ActionForm)object;
                BindException errors = new BindException(form, "simpleForm");
                validator.validate(form, errors);
                if(errors.hasErrors()) {
                    System.err.println("Validation failed");
                    throw new RuntimeException(errors);
                }
            }
        }
        return invocation.proceed();
    }

    public AbstractBeanValidator getValidator() { return validator;}
    public void setValidator(AbstractBeanValidator validator) { this.validator = validator;}

}

The ValidationInterceptor.invoke() is called before our actual business service method… thereby allowing us to perform some additional logic as we see fit. In this case we simply iterate over the passed arguments looking for an ActionForm. If it finds one it submits it to the validator. Assuming there are no errors, we allow the method invocation to continue by calling proceed()

Wrap Up

So to recap, we use delegate our Struts Action to a Spring enabled implementation, which in turn uses a Proxy to intercept the business method calls. Our interceptor transparently performs Validation on the passed data using the same Validation configuration that was used by the Presentation tier. Viola!

Technorati Tags: , , ,

    del.icio.us

Comments are locked.