Announcement Announcement Module
Collapse
No announcement yet.
PathVariable and ViewResolver prevent Controller-less config Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • PathVariable and ViewResolver prevent Controller-less config

    Hi there.

    With Spring 3 MVC, we're almost so very close to attaining the Grails/Rails/Play ease of configuration for quick MVC... but the problems with combining @PathVariable with, say, InternalResourceViewResolver prevent me from finally getting rid of having to code a boilerplate configuration Controller class.


    Here's my original Controller:

    Code:
    @Controller
    @RequestMapping("/logs")
    public class LogFileSummaryController {
    
    	private final LogFileSummaryManager logFileSummaryManager;
    
    	@Autowired
    	public LogFileSummaryController(LogFileSummaryManager logFileSummaryManager) {
    		this.logFileSummaryManager = logFileSummaryManager;
    	}
    
    	@RequestMapping(method = RequestMethod.GET)
    	public ModelAndView getAllLogFileSummaries() {
    		List<LogFileSummary> logFileSummaries = logFileSummaryManager.getAllLogFiles();
    
    		ModelAndView modelAndView = new ModelAndView("/logs/list");
    		modelAndView.addObject("logFileSummaries", logFileSummaries);
    		return modelAndView;
    	}
    
    	@RequestMapping(value="/{logFileId}", method = RequestMethod.GET)
    	public ModelAndView getLogFileSummary(@PathVariable String logFileId) {
    		LogFileSummary logFileSummary = logFileSummaryManager.getById(logFileId);
    
    		ModelAndView modelAndView = new ModelAndView("/logs/logFileSummary");
    		modelAndView.addObject("logFileSummary", logFileSummary);
    		return modelAndView;
    	}
    }
    The most frustrating thing about this code is how little it does. The entire reason this class exists is to map a url to making a method call - exactly what grails/rails/play do, and exactly what our controllers should be doing.

    The only reason this class has to exist, is because of the view name. When using a normal view resolver, the problem is that the view is based on the url requested. In this example, "/logs/1234" is interpreted to look for "/WEB-INF/jsp/logs/1234.jsp", which is so obviously wrong that it only took seconds to realize this whole problem.

    So, I did what any frustrated hacker without a Guiness on St. Patrick's Day would do: start hacking away at the stupid Spring source code:

    Code:
    org/springframework/web/bind/annotation/support/HandlerMethodInvoker.java
    166a167,168
    >             webRequest.setAttribute(HandlerMethodInvoker.class.getName() + ".controllerClass", handlerMethodToInvoke.getDeclaringClass().getName(), WebRequest.SCOPE_REQUEST);
    >             webRequest.setAttribute(HandlerMethodInvoker.class.getName() + ".controllerMethod", handlerMethodToInvoke.getName(), WebRequest.SCOPE_REQUEST);
    And, a little custom ViewNameTranslator implementation (season to taste):

    Code:
    public class HandlerMethodInvokerViewNameTranslator implements RequestToViewNameTranslator {
    
    	public String getViewName(HttpServletRequest request) {
    
    		String controllerClassName = (String) request.getAttribute("org.springframework.web.bind.annotation.support.HandlerMethodInvoker.controllerClass");
    		String controllerMethodName = (String) request.getAttribute("org.springframework.web.bind.annotation.support.HandlerMethodInvoker.controllerMethod");
    
    		controllerClassName = sanitizeControllerName(controllerClassName);
    		controllerMethodName = sanitizeControllerMethodName(controllerMethodName);
    
    		return controllerClassName + "/" + controllerMethodName;
    	}
    
    	private String sanitizeControllerMethodName(String controllerMethodName) {
    		controllerMethodName = lowercaseFirstLetter(controllerMethodName);
    		controllerMethodName = removeGetPrefix(controllerMethodName);
    
    		return controllerMethodName;
    	}
    
    	private String removeGetPrefix(String controllerMethodName) {
    		if (controllerMethodName.startsWith("get"))
    		{
    			controllerMethodName = controllerMethodName.substring(3);
    			controllerMethodName = lowercaseFirstLetter(controllerMethodName);
    		}
    
    		return controllerMethodName;
    	}
    
    	private String sanitizeControllerName(String controllerClassName) {
    		controllerClassName = controllerClassName.substring(controllerClassName.lastIndexOf(".") + 1);
    		controllerClassName = stripController(controllerClassName);
    		controllerClassName = lowercaseFirstLetter(controllerClassName);
    		return controllerClassName;
    	}
    
    	private String lowercaseFirstLetter(String controllerClassName) {
    		char lower = Character.toLowerCase(controllerClassName.charAt(0));
    		return lower + controllerClassName.substring(1);
    	}
    
    	private String stripController(String controllerClassName) {
    		if (controllerClassName.endsWith("Controller")) {
    			controllerClassName = controllerClassName.substring(0, controllerClassName.indexOf("Controller"));
    		}
    
    		return controllerClassName;
    	}
    }
    The controllers xml file:
    Code:
    	<context:component-scan base-package="com.daveclay.web"/>
    
    	<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    		<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
    		<property name="prefix" value="/WEB-INF/jsp/"/>
    		<property name="suffix" value=".jsp"/>
    	</bean>
    
    	<bean id="viewNameTranslator" class="com.daveclay.spring.web.HandlerMethodInvokerViewNameTranslator"/>
    And now my "Controller" looks like this:

    Code:
    @Controller
    @RequestMapping("/logs")
    public class LogsController {
    
    	private final LogFileSummaryManager logFileSummaryManager;
    
    	@Autowired
    	public LogsController(LogFileSummaryManager logFileSummaryManager) {
    		this.logFileSummaryManager = logFileSummaryManager;
    	}
    
    	@RequestMapping(method = RequestMethod.GET)
    	public List<LogFileSummary> getAllLogFileSummaries() {
    		List<LogFileSummary> logFileSummaries = logFileSummaryManager.getAllLogFiles();
    		return logFileSummaries;
    	}
    
    	@RequestMapping(value="/{logFileId}", method = RequestMethod.GET)
    	public LogFileSummary getLogFileSummary(@PathVariable String logFileId) {
    		LogFileSummary logFileSummary = logFileSummaryManager.getById(logFileId);
    		return logFileSummary;
    	}
    }
    And now, even IDEA is telling me that all these methods do is call another method... the one directly on the service interface. Now if that isn't indication that a Controller is nothing but configuration, I just don't know what does. HTTP and html are nothing more than a binding to our application's services and rendering out the returned object data. Instead of xml or json, we're producing html. Separation and delegation of responsibilities.

    It would just be another little step to move this into an extended-xml configuration file mapping url path variables and request parameters to method names on actual service beans, and I'd never have to write the word "Controller" ever again. And really, after 12 years of bloated Java MVC in the mainstream, isn't it about time?

  • #2
    Hi Dave,

    I just stumbled across your post having had exactly the same issues you did with the RequestToViewNameResolver and path variables, so went about implementing something similar.

    However, it doesn't look like those attributes are in the request any longer (so presumably have been removed at some point along the way to Spring 3.0.5).

    I'll probably end up creating my own subclass of one of the annotation classes to add the request attributes back in - but wondered if you'd worked around it in the interim?

    Regards,

    James Frost

    Comment


    • #3
      There is a JIRA on this here: https://jira.springframework.org/browse/SPR-5391

      Vote it up if you'd like to see it in the next release, or even better, attach a patch.

      Keith

      Comment

      Working...
      X