How to implement custom parameter mapping in Spring MVC

06.08.2018 by codeboje in Spring MVC | Spring Boot

In this tutorial, we will discuss HandlerMethodArgumentResolver which is being used to resolve the arguments of the public MVC methods (handler methods). To understand this feature in depth, we will look at different out of the box method argument resolvers and how we can a write custom argument resolver. We will take an example of resolving the hostname (domain) of the server.

Introduction/Concept of HandlerMethodArgumentResolver

As its name suggests, this is a feature of resolving the arguments of public MVC methods. If you are a Spring Web MVC or Rest application developer, then knowingly or unknowingly you must have used this powerful feature.

To begin with, let’s have a look at below code snippet.

@RequestMapping("/users/{id}")
public String sayHello(HttpServletRequest httpRequest, @PathVariable("id") String userId) {

    // We will get the httpRequest object
    // userId argument will have the value passed in place of {id} in URL

    return "Hello " + userId;
}

In the above code, there are two arguments in the sayHello method.

  1. httpRequest – which is of type HttpServletRequest
  2. userId – which is of type String.

Have we done anything in order to resolve these arguments from the actual request?

No, instead these arguments are directly available in the handler method to use. Argument “userId” is auto-populated with the value of {id} passed in Request Url.

HandlerMethodArgumentResolver Interface Details

HandlerMethodArgumentResolver

  • is an interface.
  • defined under package org.springframework.web.method.support
  • packaged in spring-web-<< RELEASE>>.jar
  • has two abstract methods.
  • every implementation needs to provide the implementation of these two methods (refer below).
boolean supportsParameter(MethodParameter parameter);

Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

Abstract Method Details

  1. supportsParameters- Implementation of this method decides whether this HandlerMethodArgumentResolver should resolve the argument or not. An argument to this method is MethodParameter which could be used to check the method name, argument type, annotations etc and based on the values implementation can be coded.

    Note Spring does some caching and supportsParameters is not called for same request every time.*

  2. resolveArgument- This method is executed only if supportParameters method returns true. Implementation of this method has a responsibility to create and return the object of the requested type.

Out of the box HandlerMethodArgumentResolver Implementations

Spring provides several implementations catering to different scenarios. Below are some of the implementations which we use quite often.

  1. PathVariableMapMethodArgumentResolver - This implementation is used internally for resolving method arguments annotated with @PathVariable annotation and resolves the argument by populating the value of path variable defined as {id} pattern. (refer code snippet we discussed above)
  2. RequestHeaderMethodArgumentResolver - This implementation is used internally for resolving method arguments annotated with @RequestHeader annotation and resolves the argument by pulling the value of headers from the request.
  3. RequestParamMethodArgumentResolver - This implementation is used internally for resolving method arguments annotated with @RequestParam annotation.
  4. RequestResponseBodyMethodProcessor - This implementation is used internally for resolving method arguments annotated with @RequestBody annotation. Based on the Content-Type header, JSON or XML implementations are picked to convert the JSON or XML body to corresponding Java Objects.
  5. ServletRequestMethodArgumentResolver This implementation is used internally to resolve HttpServletRequest argument. (refer code snippet we discussed above.

Now we have got a good understanding of how the arguments of public MVC methods are resolved and different argument resolvers used by Spring out of the box. Now it is the time to discuss “When to use HandlerMethodArgumentResolver

When to use HandlerMethodArgumentResolver

Quite often we get into a situation where HandlerMethodArgumentResolver would be a perfect choice. For example, if we have a requirement to combine the data from multiple sources like request, session or database and construct an argument of a public MVC method.

You might be thinking what is big deal in it and we could construct it by pulling data from required sources and construct in handler method itself. Yes, you are correct and we could do this but this is not a correct approach and design as

  • handler methods are not the ideal place to write such conversions.
  • we could end up having the duplicate code if same logic needs in multiple handler methods
  • any change needed later do not require any modifications in handler class.
  • it helps centralized validation logic.

Let’s have a look at both the approaches we discussed above for the given example

Approach 1 - Construct custom object in the handler method itself

@RequestMapping("/users/name")
public String getUserName(HttpServletRequest httpRequest) {

    User user = new User();
    // get id from request
    user.setId(httpRequest.getParameter("id"));
    // get name from database service based on id
    user.setName(UserDataService.getName(user.getId()));
    return user.getName();
}

Approach 2 - Using Custom HandlerMethodArgumentResolver.

This will help us having clean code along with the advantages listed above.

@RequestMapping("/users/name")
public String getUserName(User user) {
    return user.getName();
}

Hint If you have a requirement which demands a custom and complex object to be constructed from the multiple sources like request, session or any backend service and should be available under handler method, then writing custom handler method argument resolver is a perfect choice.

HandlerMethodArgumentResolver in Action

Now it is the time to write some code and see the concepts we learned practically. As mentioned in the introduction, we will write an example of resolving the hostname of the server using Spring Boot 2 project. The tutorial will be linked to a GitHub repo later.

Example Overview

We are going to write an example where the requirement is to return the hostname of the server to which the request was sent. To have a better understanding, we will implement this example with and without using HandlerMethodArgumentResolver.

Hint We can get the server host name using getServerName() method of HttpServletRequest argument

Approach 1 - Implementation without custom HandletMethodArgumentResolver

@Controller
public class GetDomainController {

    @GetMapping("/domain")
    public @ResponseBody String getDomain(HttpServletRequest httpRequest) {
        String domain = httpRequest.getServerName();
        return domain;
    }

}

Code Explanation

  1. GetDomainController class is annotated with @Controller annotation so it can act as a controller.
  2. HandlerMethod getDomain is annotated with @GetMapping annotation with the path as “/domain” so any request with URL http:<< serverhost>>:<< port>>/domain will be handled with this method.
  3. Implementation of this method will get the server hostname from getServerName() method of HttpServletRequest argument and return the value.
  4. The return type is annotated with @ResponseBody so that it will not be considered as view name and would be returned to the caller directly.

Note We can use @RestController in place of @Controller and remove @ResponseBody annotation*

Approach 2 - Implementation using custom HandlerMethodArgumentResolver

Step 1 - Write Handler Mapping

@Controller
public class GetDomainController {

    @GetMapping("/domain")
    public @ResponseBody String getDomain(String domain) {
        return domain;
    }
}

Code Explanation

In this approach, the method argument has been changed to String in place of HttpServletRequest. Value of this domain argument will be populated via custom HandlerMethodArgumentResolver implementation that we will be writing next.

Step 2- Writing Custom HandlerMethodArgumentResolver implementation

public class DomainArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public Object resolveArgument(MethodParameter methodParameter,
            ModelAndViewContainer modelViewContainer, NativeWebRequest nativeWebRequest,
            WebDataBinderFactory webDataBinderFactory) throws Exception {

        String domain = ((HttpServletRequest) nativeWebRequest.getNativeRequest()).getServerName();
        return domain;

    }

    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        return methodParameter.getParameterType().equals(String.class);
    }

}

Code Explanation

  1. DomainArgumentResolver is an implementation of HandlerMethodArgumentResolver which implements both resolveArgument and supportsParameter methods.
  2. There can be multiple implementations of supportsParameter methods which we will be discussing in a little while but this is the simplest implementation. This implementation checks that if the argument of the handler method is of String type, then this implementation will be applied.
  3. As the getDomain(String domain) method of Controller class has an argument of type String, supportParameter implementation will return true.
  4. resolveArgument method will be executed if supportParameter method returns true.
  5. To get the hostname, first, we have to get the native request from nativeWebRequest argument and type cast it into HttpServletRequest. After doing so we can simply call getServerName() method on it.

Step 3 - Registering DomainArgumentResolver in order to get it activated.

@Configuration
public class WebConfig implements WebMvcConfigurer{

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(new DomainArgumentResolver());
    }

}

Code Explanation

  1. To register the DomainArgumentResolver, we need to implement WebMvcConfigurer and override addArgumentResolvers method.
  2. Add the instance of DomainArgumentResolver class in the list of argumentResolvers.

Note Prior to Spring 5, we need to extend WebMvcConfigurerAdapter class and override the addArgumentResolvers method but with Spring 5, WebMvcConfigurerAdapter class has been deprecated. If you are using Spring 5, the recommendation is to implement WebMvcConfigurer as we have done in this example.

Other implementation of supportsParameter method

We can say that implementation of supportsParameter method above is not efficient as this is going to return true for all the handler methods which have an argument of type String and value of that argument will be populated as a hostname. Like if we have another handler mapping as below, name argument will also be populated as a hostname.

@GetMapping("/name")
public String getName(String name){
    return name;
}

To overcome, this issue, we can improve the implementation by using other parameters like a method name, request Url etc available from the methodParameter argument and can create multiple argument resolver if need be.

@Override
public boolean supportsParameter(MethodParameter methodParameter) {
    if(! methodParameter.hasMethodAnnotation(GetMapping.class)){
        return false;
    }

    GetMapping annotation = (GetMapping) methodParameter.getMethodAnnotation(GetMapping.class);
    String[] paths = annotation.value();
    Predicate<String> predicate = path -> path.equals("/domain");
    return Arrays.stream(paths).anyMatch(predicate);
}

Conclusion

From the above discussed different approaches of resolve method arguments of public MVC methods using a simple example, it is clear that how using HandlerMethodArgumentResolver implementation helps us in writing maintainable, manageable and easy to debug code. Also if you see, Approach 1 is also using HandlerMethodArgumentResolver behind the scene in order to pass HttpServletRequest argument but it was done by Spring out of the box ArgumentResolvers.


Want content like this in your inbox each workday? No BS, spam or tricks... just useful content:

I understand and agree to the privacy policy