Monday, November 19, 2007

Method Name in the URL File Name with Spring MVC's MultiActionController

Spring's MultiActionController is great for allowing a single controller to be able to handle multiple types of requests. What method to dispatch to is determined based on the method name resolver. You have several options here. E.g. with the ParameterMethodNameResolver you specify what method to call in the request parameters. You can specify that a certain request parameter, e.g. action, is going to contain the method name to dispatch to:

http://localhost:8080/app/someController.do?action=someMethod

The InternalPathMethodNameResolver resolves the controller from the path and the method from the file name without extension, e.g. the following URL would cause someMethod to be called on the controller. It basically strips the file name from the path (the part after the last slash) and strips the extension (.do):

http://localhost:8080/app/someController/someMethod.do

The PropertiesMethodNameResolver allows you to specify the method in a "value of the page" kind of way. E.g. you have someController.do and you add =someMethod to the URL in order for it to resolve to the controller and the method that is to be called:

http://localhost:8080/app/someController.do=someMethod

Besides the above strategies, one could also imagine a strategy where both the controller and the method are passed in the file name part of the path. The construct would contain the name of the controller, e.g. someController, suffixed with what method to call, e.g. .someMethod, suffixed with the extension, in this case .do:

http://localhost:8080/app/someController.someMethod.do

The above can also be extended to support dispatching to a default method in case no method name was given, e.g. one could specify someMethod to be the default and then call:

http://localhost:8080/app/someController.do

Spring does not have support for this scheme, but it can be easily extended to do so and this article will show how. The first thing to do is setup the web.xml file of your web application (I have created a dynamic web project in Eclipse 3.3 called SpringTests, so replace any references to this project with references to your own). First register the DispatcherServlet. In the example below the DispatcherServlet is registered under servlet name mvc:

<servlet>
  <servlet-name>mvc<servlet-name>
  <servlet-class>
  org.springframework.web.servlet.DispatcherServlet
  <servlet-class>
  <init-param>
    <param-name>contextConfigLocation<param-name>
    <param-value>
    classpath:com/acme/mvc.xml
    <param-value>
  <init-param>
  <load-on-startup>1<load-on-startup>
<servlet>


Please note in the above snippet the location of the Spring configuration file. It is on the classpath in com/acme/mvc.xml, so unless you are making an exact copy of the setup described here, refer to your Spring configuration file that is going to have the URL mappings of the DispatcherServlet. Next step is to add the servlet mapping. To stay inline with the example scenarios described we will map *.do to our mvc servlet:

<servlet-mapping>
  <servlet-name>mvc<servlet-name>
  <url-pattern>*.do<url-pattern>
<servlet-mapping>


The next thing we will need to do is create and register a controller. For this example, we will use a very simple controller called MyController that will have just two methods and that writes what method was called to the output stream. In our source folder we create a new package, com.acme, and add the MyController class. We will also be using the com.acme package to contain our Spring configuration file, mvc.xml:

package com.acme;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.mvc.multiaction.MultiActionController;

public class MyController extends MultiActionController {

  public void defaultMethod(HttpServletRequest request,
    HttpServletResponse response) throws IOException
  {
    PrintWriter pw = new PrintWriter(response.getOutputStream());
    pw.print("Default method was called");
    pw.flush();
  }

  public void nonDefaultMethod(HttpServletRequest request,
    HttpServletResponse response) throws IOException
  {
    PrintWriter pw = new PrintWriter(response.getOutputStream());
    pw.print("Non default method was called");
    pw.flush();
  }
}


We will now create the mvc.xml file, which will register our controller, contain the DispatcherServlet URL mapping to our controller, but also the necessary configuration for our custom method name resolver class. First we will register our controller. Please note that we specify a methodNameResolver as being the defaultUrlMethodNameResolver. This will be a reference to our custom method name resolver which we will need to implement once we are done configuring. It is injected in our controller's methodNameResolver property (which is a property of MultiActionController), and any attempt to resolve which method name is to be used will be sent to our own implementation:

<bean id="myController" class="com.acme.MyController">
  <property name="methodNameResolver"
            
ref="defaultUrlMethodNameResolver" />
<bean>


The next step is to configure the method name resolver to point to our DefaultUrlMethodNameResolver class, which we will create later on in the com.acme package. We inject a default method into our method name resolver. This property will be used to return a default method name in case the URL does not contain one (e.g. someController.do). In our example we want the default method name to be defaultMethod:

<bean id="defaultUrlMethodNameResolver"
      class="com.acme.DefaultUrlMethodNameResolver">
  <property name="defaultMethod"
            
value="defaultMethod" />
<bean>


Now we add the URL mappings for the DispatcherServlet and map myController URL's to the myController bean. Note that we map to myController.* and not myController.*.do as the latter will force us to always specify a method name as it forces two dots in the file name. E.g. myController.do will fail as it does not match the latter pattern. The former pattern matches with both myController.do and myController.someMethod.do. Not adding the .do part in the pattern is not a big deal, as *.do is already configured in the servlet mapping in the web.xml above, meaning that not supplying the .do on the URL would not have been handled by the DispatcherServlet anyways.

<bean id="urlMapping"
  
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
  <property name="mappings">
    <props>
      <prop key="/myController.*">myController<prop>
    <props>
  <property>
<bean>


We now get to the actual implementation of the method name resolver. We create the DefaultUrlMethodNameResolver in the com.acme package. As we want to inject a default method name we will need to have the defaultMethod property and a setter for this property. We will also need to implement the MethodNameResolver interface and implement the getHandlerMethodName method:

package com.acme;

import javax.servlet.http.HttpServletRequest;

import org.springframework.web.servlet.mvc.multiaction.MethodNameResolver;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;

public class DefaultUrlMethodNameResolver implements MethodNameResolver {

  private String defaultMethod = null;

  public String getHandlerMethodName(HttpServletRequest request)
    throws NoSuchRequestHandlingMethodException
  {
    String path = request.getServletPath();
    String[] pathParts = path.split("\\.");
    if (pathParts.length > 2) {
      if (!"".equals(pathParts[1])) {
        return pathParts[1];
      } else {
        return defaultMethod;
      }
    } else {
      return defaultMethod;
    }
  }

  public void setDefaultMethod(String defaultMethod) {
    this.defaultMethod = defaultMethod;
  }
}


The above code splits the file name on the dot. The path parts array contains the different segments of the file name. If the size of the array is greater than two, we know that we have at least three parts in the file name, the first part being the controller name, the second the method name and the third being .do. We therefore return the second element of the array as the method name (pathParts[1]), unless this element were to be empty, in which case we return the default method (this is in order to handle e.g. myController..do). If we had only two elements in the array, we have something like myController.do and thus return the default method. We can now run this web application on a server and try out a couple of URL's to see how it works.

http://localhost:8080/SpringTests/myController.do results in "Default method was called".

http://localhost:8080/SpringTests/myController..do results in "Default method was called".

http://localhost:8080/SpringTests/myController.defaultMethod.do results in "Default method was called".

http://localhost:8080/SpringTests/myController.nonDefaultMethod.do results in "Non default method was called".

So here we have a strategy that allows both the controller and the method name to dispatch to in the file name part of the URL.

1 comment:

Unknown said...

A very detailed explanation. Cleared my doubts instantly