views:

277

answers:

3

I'm trying to find an optimal way to use the latest Spring 3.0. I really like the @RequestMapping annotation with all features applied to it. However, what I don't like, is that the URL bound to the action should be fully specified in the java file.

It would be the best to somehow send the whole url-binding configuration to the context xml file. However, it would also do if that url-binding could be moved to xml at least partially.

This is what I mean:

Current code:

@Controller
@RequestMapping("myController")
class MyController {
    @RequestMapping("**/someMethod")
    String someMethod(...) {
    }
}

This code binds the myController/someMethod to MyController::someMethod. What I don't like here is that "myController" part binding is also in this java file. I want to make it as modular as possible, and this part plays very bad for me.

What I'd like to see is something like this, to achieve the same result:

context.xml

<mapping>
    <url>myController</url>
    <controller>MyController</controller>
</mapping>    

java

@Controller
//-- No request mapping here --// @RequestMapping("myController")
class MyController {
    @RequestMapping("**/someMethod")
    String someMethod(...) {
    }
}

Is something like this possible on annotated controllers in Spring 3?

A: 

You can combine XML and annotation-style mappings with a considerable amount of flexibility. Both can use Ant-style wildcard matching, so you can do things like this (not tested, but gives you the general idea):

<bean class="SimpleUrlHandlerMapping">
   <property name="mappings">
      <map>
         <entry key="myController/**" value-ref="myController"/>
      </map>
   </property>
</bean>

<bean id="myController" class="MyController"/>

And then

@Controller
class MyController {
    @RequestMapping("**/someMethod")
    String someMethod(...) {
    }
}

The URL /myController/someMethod should then match that method.

You might need to play with a bit to get it to work, but that's the gist of it.

skaffman
@skaffman: Will it work for annotated controllers too? I've just google examples like yours but they were all using `extends SomeSpringController` model and not `@Controller`.
Max
@skaffman: Ok, I've just tested it and I am not sure if it works or not. Problem is, `@RequestMapping("**/someMethod")` works even without any xml at all, because of that `**` part. And also it works for any given path, not only `myController/**`.
Max
A: 

However, what I do not like, is that the URL bound to the action should be fully specified in the java file

So rely on ControllerClassNameHandlerMapping

<bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>

Keep in mind ControllerNameHandlerMapping remove Controller suffix if it exists and return the remaining text, lower-cased If you just want The first letter lower-cased, set caseSensitive property as true

Suppose here goes your controller

package br.com.ar.view.resources;

@Controller
public class UserController {

    /**
      * mapped To /user/form
      */
    @RequestMapping(method=RequesMethod.GET)
    public void form(Model model) {
        model.add(categoryRepository().getCategoryList());
    }

    /**
      * mapped To user/form
      */
    @RequestMapping(method=RequesMethod.POST)
    public void form(User user) {
        userRepository.add(user);
    }

}

There is more: if you use a modularized app, you can rely on basePackage property. Suppose you have financial and human resources module like

br.com.ar.view.financial.AccountController;
br.com.ar.view.resources.ManagementController;

You define your base package

<bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping">
    <property name="basePackage" value="br.com.ar.view"/>
</bean>

You can call your AccountController form method as

/financial/account/form

And you can call your ManagementController form method as

/resources/management/form

As i am pretty sure you use default TranslateToViewName convention over configuration, your directory structure should looks like

/WEB-INF
    /view
        /financial
            /user
                form.jsp

        /resources
            /management
                form.jsp

Do not forget define your InternalResourceViewResolver

<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/view/"/>
    <property name="suffix" value=".jsp"/>
</bean>

And finally, If your request does not need a controller. No problem, define your defaultHandler property

<bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping">
    <property name="basePackage" value="br.com.ar.view"/>
    <property name="caseSensitive" value="true"/>
    <property name="defaultHandler">
        <bean class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>
    </property>
</bean>

Now if you call, for instance, /index.htm (I suppose your DispatcherServlet intercepts htm extension) and you do not have any IndexController, Spring will look for

/WEB-INF/view/index.jsp

Good, do not ???

Arthur Ronald F D Garcia
Nice, but it does not satisfy my needs. I'm migrating a big project to Spring 3, and it has to have specific (predefined) urls for specific actions as all Ajax stuff depends on it. And naming controllers the way they would resemble the URL would be crazy, some URL's are like `\main\index\get?1`, and I would need to create some `main.IndexController` with method `get()` just to achieve that URL. I agree that this option is nice if you are building a new project, but it won't work for migration as in my case.
Max
@Max **I am pretty sure** whether you have a URL like \main\index\get?1 **you need to use** @PathVariable and @RequestMapping Together. How can Spring guess **1** should be mapped To a variable called id ??? You should provide some kind of information. @RequestMapping(value="/main/index/{id}") public void show(@PathVariable Long id) { // code goes here } I suspect whether you want this kind of behavior with minimal amount of configuration, **you need to create your own HandlerMapping**
Arthur Ronald F D Garcia
Hm, interesting idea about creating my own HandlerMapping. Could you please provide the info on how to do it (maybe few nice example) as a separate answer please?
Max
@Max ok. I will show you a good insight. But keep in mind Spring **heavily use reflection behind the scenes**. I will show you Tomorrow.
Arthur Ronald F D Garcia
I would be most greatful.
Max
@Max Added as separate answer
Arthur Ronald F D Garcia
+3  A: 

As requested. You want to create your own URL pattern without Spring controllers annotations.

First of all, create a CustomController annotation To avoid to be detected by @Controller HandlerMapping

package br.com.ar.web.stereotype;

@Target(value=TYPE)
@Retention(value=RUNTIME)
@Component
public @interface CustomController {}

Here goes our AccountController

@CustomController
public class AccountController {

    public void form(Long id) {
        // do something
    }

}

Our HandlerAdapter - It takes care of calling our controller - Someting similar to Spring Validator interface approach

package br.com.ar.web.support;

public class CustomHandlerAdapter implements HandlerAdapter {

    public boolean supports(Object handler) {
        Annotation [] annotationArray = handler.getClass().getAnnotations();

        for(Annotation annotation: annotationArray) {
           /**
             * Make sure your annotation contains @SomeController
             */
        }
    }

    /**
      * Third parameter is our CustomController
      */
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Method[] methods = handler.getClass().getMethods();

        /**
          * Logic To verify whether Target method fullfil request goes here
          */            

        /**
          * It can be useful To see MultiActionController.invokeNamedMethod and MultiActionController.isHandlerMethod              
          */
        method.invoke(// parameters goes here);
    }

    public long getLastModified(HttpServletRequest request, Object handler) {
        return -1;
    }
}

And finally, our HandlerMapping. Make sure your HandlerMapping extends WebApplicationObjectSupport. It allows you To retrieve any Spring managed bean by calling

getApplicationContext().getBean(beanName);

package br.com.ar.web.servlet.handler;

public class CustomeHandlerMapping extends WebApplicationObjectSupport implements HandlerMapping, Ordered {

    private static final String CUSTOM_HANDLER_ADAPTER_NAME = "CUSTOM_HANDLER_ADAPTER_NAME";

    /**
      * Bind each URL path-CustomController bean name
      */
    private final Map handlerMap = new LinkedHashMap();

    /**
      * Ordered interface will make sure your HandlerMapping should be intercepted BEFORE or AFTER DefaultAnnotationHandlerMapping
      */
    public final void setOrder(int order) {
        this.order = order;
    }

    public final int getOrder() {
        return this.order;
    }

    /**
      * HandlerMapping interface method
      */
    public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
        String url = extractUrl(request);

        if(handlerMap.get(url) == null) {
            /**
              * Because Spring 3.0 controller is stateful
              * Let's just store CustomController class (Not an instance) in ApplicationContext
              *
              * Or use a FactoryBean to retrieve your CustomController
              */
            handlerMap.put(url, getApplicationContext().getBean(beanName));
        }

        /**
          * instantiateClass needs no-arg constructor
          */
        Object handler = BeanUtils.instantiateClass(handlerMap.get(url));

        return new HandlerExecutionChain(handler);
    }

    private String extractUrl(HttpServletRequest request) {
        /**
          * Here goes code needed To retrieve URL path from request
          *
          * Take a look at AntPathMatcher, UrlPathHelper and PathMatcher
          *
          * It can be useful To see AbstractUrlHandlerMapping.getHandlerInternal method
          */ 
    }

}

Do not forget register both HandlerAdapter and HandlerMapping

<bean id="br.com.ar.web.servlet.handler.CustomHandlerMapping"/>
<bean id="br.com.ar.web.support.CustomHandlerAdapter"/>
<!--To allow Spring 3.0 controller-->
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>
<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping"/>

I hope it can gives you a good kick off

The sequence (Behind The scenes) Spring DispatcherServlet will call our objects are

/**
  * Our HandlerMapping goes here
  */
HandlerMapping handlerMapping = getHandler(request);

HandlerExecutionChain handlerExecutionChain = handlerMapping.getHandler(request);

for(HandlerInterceptor interceptor: handlerExecutionChain.getInterceptors) {
    interceptor.preHandle(request, response, handlerExecutionChain.getHandler());
}

/**
  * Our CustomController goes here
  */
Object handler = handlerExecutionChain.getHandler();

/**
  * Our CustomHandlerAdapter goes here
  */
HandlerAdapter handlerAdapter = getHandlerAdapter(handler);

ModelAndView mav = handlerAdapter.handle(request, response, handler);

for(HandlerInterceptor interceptor: handlerExecutionChain.getInterceptors) {
    interceptor.postHandle(request, response, handlerExecutionChain.getHandler());
}
Arthur Ronald F D Garcia
@Max Just as additional comment: There is no **new** Spring 3.0 MVC. It is just a lot of Java reflection capabilities behind The scenes where The key code is stored inside AnnotationMethodHandlerAdapter and DefaultAnnotationHandlerMapping. In practice, It still use old-style MVC capabilities. Unfortunately, As far As I know, Its open-source code is not avaliable yet.
Arthur Ronald F D Garcia
@Max A nice article can be found here: http://blog.zenika.com/index.php?post/2010/04/26/Building-a-web-framework-on-top-of-Spring-MVC
Arthur Ronald F D Garcia