sábado 10 de febrero de 2007

AOP / AJAX Enabled Controllers in Spring MVC

There are two reasons that make me select Spring MVC when I have to start a project, the first one being robustness and the second being seamless integration with the rest of the components. There are two reasons aswell that I sometimes miss, the first one being (annotated) AOP in my controllers and the second a better handling of AJAX requests.

As is known, AOP in the Spring framework is based on dynamic proxies. Spring will create a proxy to an object when needed and then intercept the calls applying the declared advices. As a side effect, Spring cannot intercept anything that it didn’t wire from the start. When working with Spring MVC the main interface to use is Controller which provides a method handleRequest to perform actions. Any controller that gets declared can be wired later to a DispatcherServlet. So far so good, you can intercept the execution of the handleRequest method an apply whatever aspects you need (in my case Security and Logging are a must). Unfortunately working with Controller directly is sometimes not enough as it provides little functionality. The framework can offer more power (MultiActionController, SimpleFormController) but it won’t come without price as the handleRequest method is lost (implemented and declared final) and with it the possibility of annotating it with your aspects.

On the other hand there’s just no meaningful AJAX integration. Fortunately DWR solves this quite nicely (and even better since 2.0). When using DWR in conjunction with Spring there’s one thing that still bothers me and it is, no other, than not complying with the DRY principle, having to declare both Controllers for the MVC and DWR beans for AJAX doing the same actual work.

From now on I’ll try to offer a solution that can combine a little bit of everything. Full sources and a working example are available at Internna - Google Code.

To start, some interfaces need to be defined. They will come handy later to wire everything together:

public interface RequestHandler {
   public void addMapping(String urlMapping);
   public void addMappings(Collection urlMappings);
   public void setMappings(Set urlMappings);
   public Set getUrlMappings();
   public ModelAndView handleRequest(HttpServletRequest request,
            HttpServletResponse response) throws Exception;
}

public interface AdvisableController extends Controller {
   public void addRequestHandler(RequestHandler requestHandler);
   public void addAllRequestHandlers(Collection requestHandlers);
   public void setRequestHandlers(Set requestHandlers);
}

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface UrlMapping {
   String value();
}

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface AutowireToController {
   String controllerBean() default "";
   Class controllerClass() default AutowireToController.class;
}

As you can see the first two define the new logic and the second are just annotations (they will be used later to simplify the XML definitions). AdvisableController (which extends Controller) will be the root of our Controller hierarchy. It will provide beans compatible with the rest of the MVC and usable by the DispatcherServlet. RequestHandler defines the characteristics needed to actually execute the business logic.

Each of the interfaces come with an abstract class that implements the core functionality. AbstractRequestHandler basically manages the URLs this bean handles. The business logic should be coded in a subclass. AbstractAdvisableController role is to determine which of the available handlers should process a request. If no suitable handler is found it returns a 404 error. This class should be subclassed to add bindings or mappings for example. The important code is reduced to the following:

public final ModelAndView handleRequest(HttpServletRequest httpServletRequest,
  HttpServletResponse httpServletResponse) throws Exception {

   String controllerPath = httpServletRequest.getPathInfo();
   for (RequestHandler handler : this.requestHandlers) {
      if (canHandle(controllerPath, handler.getUrlMappings()))
         return handler.handleRequest(httpServletRequest, httpServletResponse);
   }
   httpServletResponse.sendError(HttpServletResponse.SC_NOT_FOUND);
   return null;
}

We are half way to achieve our first goal. Simply by defining two classes that extend the abstract ones we can have an advised MultiActionController working. Let's see an example:

//Just subclassed to add a mapping
@UrlMapping("/internna/*")
public class MultiActionAdvisableController extends AbstractAdvisableController {}

@UrlMapping("/nocalc.mvc")
@AutowireToController(controllerBean="multiaction")
public class NoCalculatorRequestHandler extends AbstractRequestHandler {

   @LogDebug(loggerClass=SampleRequestHandler.class)
   @PermitAll
   public ModelAndView handleRequest(HttpServletRequest request,
                     HttpServletResponse response) throws Exception {
       Map m = new HashMap();
       m.put("calculator", 0);
       return new ModelAndView("reached", m);
   }
}

// New declarations in the bean definition XML
<bean id="noCalc" class="samples.NoCalculatorRequestHandler" />
<bean id="multiaction" class="samples.MultiActionAdvisableController" />

Since release 2 DWR works even more nicely with Spring and Spring MVC. The configuration has changed so it is possible to define everything within Spring, getting rid of changes in web.xml or dwr.xml. See Bram Smeets Blog for a detailed explanation. With this in mind our task now is to integrate our MVC controllers with the DWR one. This is done creating a very simple wrapper class:

public class DWRWrapper {
   private RequestHandler requestHandler;

   public void setRequestHandler(RequestHandler requestHandler) {
      this.requestHandler = requestHandler;
   }

   public Map getResult(HttpServletRequest request) throws Exception {
      return requestHandler.handleRequest(request, null).getModel();
   }
}

At this point with an adecuate configuration our controllers can be both used in Spring MVC and in DWR without changing anything.

<dwr:controller id="dwrController" debug="true" />

<bean id="calculatorWrapper" class="es.internna.spring.mvc.DWRWrapper">
   <dwr:remote javascript="CalculatorWrapper" />
   <property name="requestHandler" ref="noCalc" />
</bean>

Don't forget to add the needed schemas and map correctly the paths needed by the DWR controller in the dispatcher servlet. In my case I had to map /engine.js, /util.js, /interface/CalculatorWrapper.js and /call/**

Once done, your controllers can be easily annotated with aspects and used directly in DWR, and in an easy way to boot! Remember to download full source code and examples at Internna - Google Code.


2 comentarios:

Sylwester dijo...

hello,
I downloaded the war file but it does not work from the beginnnig.
Beans can not be initialized

Jose Noheda dijo...

The war was tested in glassfish if I recall correctly. Please post a detailed message (stack trace, server, JVM) in http://groups.google.com/group/internna so I can try to trace where the problem is.