16 | How to customize HandlerMethodArgumentResolvers

In the previous lecture, we introduced the usage of the SpringDataWebConfiguration class, so this time we take a look at how this class is loaded, how PageableHandlerMethodArgumentResolver and SortHandlerMethodArgumentResolver take effect, and how to define your own HandlerMethodArgumentResolvers class. Are there any other web scenarios that require it? What about our customization?

Regarding the above categories, you need to have some impressions in your mind first. We will explain them in detail one by one next.

Page and Sort parameter principles

If you want to know the loading principle of paging and sorting parameters, we can find through the source code that @EnableSpringDataWebSupport loads this class. The key code is as shown in the figure below:

Drawing 0.png

Among them, the @EnableSpringDataWebSupport annotation is the core of the previous explanation, that is, the entrance that Spring Data JPA needs to open for Web support. Since we are using Spring Boot, @EnableSpringDataWebSupport does not require us to specify it manually.

This is because Spring Boot has an automatic loading mechanism. We will find that the org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration class references the @EnableSpringDataWebSupport annotation, so we do not need to reference it manually. The key code here is shown below:

Drawing 1.png

The core file of Spring Boot’s automatic loading is the spring.factories file. Then we open the spring-boot-autoconfigure-2.3.3.jar package and look at the contents of the spring.factories file. We can find the SpringDataWebAutoConfiguration configuration class, as follows:

Drawing 2.png

So we can conclude: As long as it is a Spring Boot project, we don’t need to do anything, it will naturally allow Spring Data JPA to support Web-related operations.

Drawing 3.png

The PageableHandlerMethodArgumentResolver and SortHandlerMethodArgumentResolver classes are loaded through SpringDataWebConfiguration, so we can basically know that the Page and Sort parameters of Spring Data JPA take effect because of the injection of @Bean in SpringDataWebConfiguration.

Drawing 4.png

Through the source code of the two classes PageableHandlerMethodArgumentResolver and SortHandlerMethodArgumentResolver, we can analyze that they respectively implement the org.springframework.web.method.support.HandlerMethodArgumentResolver interface in the Spring MVC Web framework, so as to do the Page and Sort parameters in the Request. Processing logic and parsing logic.

So in actual work, there may be special situations that need to be extended. For example, Page parameters may need to support multiple Keys. So what should we do? Let’s learn how to use HandlerMethodArgumentResolver.

HandlerMethodArgumentResolver usage

Detailed explanation of HandlerMethodArgumentResolvers

Anyone familiar with MVC knows that the main role of HandlerMethodArgumentResolvers in Spring MVC is to parse the method parameters in the Controller, that is, it can map the values in the Request to the parameters of the method. When we open the source code of this class, we will find that there are only two methods, as shown below:

Copy code

public interface HandlerMethodArgumentResolver {
   //Check whether the parameters of the method support processing and conversion
   boolean supportsParameter(MethodParameter parameter);
   //According to the request context, parse the parameters of the method
   Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
         NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}

The application scenarios of this interface are very wide, and we can see that there are many subclasses, as shown in the following figure:

Drawing 5.png

The functions of several of these classes are as follows:

  • PathVariableMapMethodArgumentResolver specializes in parsing the value in @PathVariable;
  • RequestResponseBodyMethodProcessor specializes in parsing the value of method parameters annotated with @RequestBody;
  • RequestParamMethodArgumentResolver specializes in parsing the value of the @RequestParam annotation parameter. When there is no annotation in the method parameter, the default is @RequestParam;
  • As well as the PageableHandlerMethodArgumentResolver and SortHandlerMethodArgumentResolver we mentioned in the previous lecture.

At this point you will find that we also explained HttpMessageConverter in the previous lecture, so what is its relationship with HandlerMethodArgumentResolvers? Let’s see.

The relationship between HandlerMethodArgumentResolvers and HttpMessageConverter

When we open the RequestResponseBodyMethodProcessor, we will find that this class mainly handles the parameters annotated with @RequestBody in the method, as shown in the following figure:

Drawing 6.png

As for the readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()) method, if we click in and continue to observe, we will find that different HttpMessageConverters will be selected for conversion based on the MediaType of the Http request.

So here you can clearly understand the relationship between HandlerMethodArgumentResolvers and HttpMessageConverter, that is, different HttpMessageConverter are called by RequestResponseBodyMethodProcessor.

So we know the calling relationship. In what order are so many HttpMessageConverters executed?

Execution sequence of HttpMessageConverter

When we customize HandlerMethodArgumentResolver, load it in through the following method.

Copy code

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
   resolvers.add(myPageableHandlerMethodArgumentResolver);
}

The custom resolver in the List has the highest priority, that is, the HandlerMethodArgumentResolver will be executed first, and then the batch of HttpMessageConverters that come with the system will be executed in order, one by one in the loop order of the List.

There is an execution efficiency problem in Spring, that is, once the required HandlerMethodArgumentResolver is found in one execution, using the caching mechanism in Spring, the List will no longer be traversed during the execution process, but the HandlerMethodArgumentResolver found last time will be used directly. This improves execution efficiency.

If you want to know more about Resolver, you can look at this class in the picture below. I won’t go into details one by one.

Drawing 7.png

So after knowing so much, can you give me a practical example?

Customized HandlerMethodArgumentResolver in practice

In actual work, you may encounter the work of revising old projects. If we want to transform the old API interface into the technical implementation of JPA, then there may be problems that require new and old parameters. Assume that in the actual scenario, the parameter of our Page is page[number], and the parameter of page size is page[size], let’s see what should be done.

Step one: Create a new MyPageableHandlerMethodArgumentResolver.

This class has two functions:

  1. Used to be compatible with the parameters of ?page[size]=2 & page[number]=0;
  2. Support JPA’s new parameter form ?size=2 & page=0.

We implement this requirement through a customized MyPageableHandlerMethodArgumentResolver, please see the following code.

Copy code

/**
 * Load this class into the Spring container through @Component
 */
@Component
public class MyPageableHandlerMethodArgumentResolver extends PageableHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
   //We assume that the parameters of sort have not changed, using the writing method in PageableHandlerMethodArgumentResolver
   private static final SortHandlerMethodArgumentResolver DEFAULT_SORT_RESOLVER = new SortHandlerMethodArgumentResolver();
   //Given two default values
   private static final Integer DEFAULT_PAGE = 0;
   private static final Integer DEFAULT_SIZE = 10;
   //Compatible with the new version, introduce JPA paging parameters
   private static final String JPA_PAGE_PARAMETER = "page";
   private static final String JPA_SIZE_PARAMETER = "size";
   //Compatible with the original old paging parameters
   private static final String DEFAULT_PAGE_PARAMETER = "page[number]";
   private static final String DEFAULT_SIZE_PARAMETER = "page[size]";
   private SortArgumentResolver sortResolver;
   //Imitate the constructor in PageableHandlerMethodArgumentResolver
   public MyPageableHandlerMethodArgumentResolver(@Nullable SortArgumentResolver sortResolver) {
      this.sortResolver = sortResolver == null ? DEFAULT_SORT_RESOLVER : sortResolver;
   }
   
   @Override
   public boolean supportsParameter(MethodParameter parameter) {
// Assume that we use our own class MyPageRequest to receive parameters
      return MyPageRequest.class.equals(parameter.getParameterType());
      //At the same time, we can also support receiving through the Pageable parameter in Spring Data JPA. The two effects are the same.
// return Pageable.class.equals(parameter.getParameterType());
   }
   /**
    * Parameters encapsulate logical page and sort. JPA parameters have higher priority than page[number] and page[size] parameters.
    */
    //public Pageable resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) { //This is the Pageable method
   @Override
   public MyPageRequest resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
      String jpaPageString = webRequest.getParameter(JPA_PAGE_PARAMETER);
      String jpaSizeString = webRequest.getParameter(JPA_SIZE_PARAMETER);
      //We take the values of page, sort and page[number], page[size] in the parameters respectively
      String pageString = webRequest.getParameter(DEFAULT_PAGE_PARAMETER);
      String sizeString = webRequest.getParameter(DEFAULT_SIZE_PARAMETER);
      //The priority when both have values, and the logic of their default values
      Integer page = jpaPageString != null ? Integer.valueOf(jpaPageString) : pageString != null ? Integer.valueOf(pageString) : DEFAULT_PAGE;
      //The logic of page + 1 can be calculated here at the same time; such as: page=page + 1;
      Integer size = jpaSizeString != null ? Integer.valueOf(jpaSizeString) : sizeString != null ? Integer.valueOf(sizeString) : DEFAULT_SIZE;
       //We assume that the value method of sort sorting will not change first.
      Sort sort = sortResolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
// If you use the Pageable parameter to receive the value, we can also directly return the PageRequest without customizing the MyPageRequest object;
// return PageRequest.of(page,size,sort);
      //Encapsulate the demerits calculated by page and size into our custom MyPageRequest class
      MyPageRequest myPageRequest = new MyPageRequest(page, size,sort);
      //Return the object required by the parameters in the controller;
      return myPageRequest;
   }
}

You can take a closer look at the logic through the comments in the code. In fact, this class is not complicated. It just takes the Page-related parameters of the Request and encapsulates them into the method parameters returned to the Controller in the object. The MyPageRequest is not required, I just want to show you how to do it differently.

Step 2: Create a new MyPageRequest.

Copy code

/**
 * Inheriting the parent class can save a lot of logic for calculating page and index.
 */
public class MyPageRequest extends PageRequest {
   protected MyPageRequest(int page, int size, Sort sort) {
      super(page, size, sort);
   }
}

This class, which we use to receive Page related parameter values, is not required either.

Step 3: implements WebMvcConfigurer to load myPageableHandlerMethodArgumentResolver.

Copy code

/**
 * Implement WebMvcConfigurer
 */
@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
   @Autowired
   private MyPageableHandlerMethodArgumentResolver myPageableHandlerMethodArgumentResolver;
   /**
    * Override this method and load our custom myPageableHandlerMethodArgumentResolver into the original mvc resolvers.
    * @param resolvers
    */
   @Override
   public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
      resolvers.add(myPageableHandlerMethodArgumentResolver);
   }
}

Here I use the Spring MVC mechanism to load our customized myPageableHandlerMethodArgumentResolver. Since the customized priority is the highest, I use MyPageRequest.class

and Pageable.class are both acceptable.

Step 4: Let’s take a look at the writing in the Controller.

Copy code

//It is also possible to use Pageable.
@GetMapping("/users")
public Page<UserInfo> queryByPage(Pageable pageable, UserInfo userInfo) {
   return userInfoRepository.findAll(Example.of(userInfo),pageable);
}
//Use MyPageRequest to receive
@GetMapping("/users/mypage")
public Page<UserInfo> queryByMyPage(MyPageRequest pageable, UserInfo userInfo) {
   return userInfoRepository.findAll(Example.of(userInfo),pageable);
}

As you can see, both Pageable and MyPageRequest methods are available here.
Step 5: Start the project and test it.

We can test the following two situations in turn and find that both can work normally.

Copy code

GET http://127.0.0.1:8089/users?page[size]=2 & page[number]=0 & ages=10 & sort=id,desc
###
GET http://127.0.0.1:8089/users?size=2 & page=0 & ages=10 & sort=id,desc
###
GET http://127.0.0.1:8089/users/mypage?page[size]=2 & page[number]=0 & ages=10 & sort=id,desc
###
GET http://127.0.0.1:8089/users/mypage?size=2 & page=0 & ages=10 & sort=id,desc

Among them, you should be able to notice that the Controller method I demonstrated has multiple parameters. Each parameter performs its own role and finds its corresponding HandlerMethodArgumentResolver. This is the elegance of the Spring MVC framework.

So in addition to the above Demo, what other suggestions does the custom HandlerMethodArgumentResolver have for our actual work?

Suggestions for practical work

What role does custom HandlerMethodArgumentResolver play in our actual work? Divided into the following scenarios.

Scene 1

When we process certain parameters in the Controller, there are many repeated steps, so we can consider writing our own framework to process the parameters in the request, and the code in the Controller will become very elegant, no need to care For other framework codes, you only need to know that the parameters of the method have values.

Scenario 2

For another example, what needs to be noted in actual work is that by default, the Page in JPA starts from 0, and we may have some old code that needs to be maintained, because most Pages in the old code will start from 1. If we do not customize HandlerMethodArgumentResolver, then when using paging, each Controller method needs to care about this logic. Then at this time you should think of the implementation of the resolveArgument method of the custom MyPageableHandlerMethodArgumentResolver listed above. Using this method, we only need to modify the calculation logic of Page.

Scene 3

For another example, in actual work, we often encounter the application scenario of “getting the current user”. At this time, the common practice is that when using the current user’s UserInfo, the user information needs to be obtained according to the token of the request header every time. The pseudo code is as follows:

Copy code

@PostMapping("user/info")
public UserInfo getUserInfo(@RequestHeader String token) {
    // pseudocode
    Long userId = redisTemplate.get(token);
    UserInfo useInfo = userInfoRepository.getById(userId);
    return userInfo;
}

If we use the HandlerMethodArgumentResolver interface to implement it, the code will become much more elegant. The pseudo code is as follows:

Copy code

// 1. Implement the HandlerMethodArgumentResolver interface
@Component
public class UserInfoArgumentResolver implements HandlerMethodArgumentResolver {
   private final RedisTemplate redisTemplate;//Pseudo code, assuming that our token is placed in redis
   private final UserInfoRepository userInfoRepository;
   public UserInfoArgumentResolver(RedisTemplate redisTemplate, UserInfoRepository userInfoRepository) {
      this.redisTemplate = redisTemplate;//Pseudo code, assuming that our token is placed in redis
      this.userInfoRepository = userInfoRepository;
   }
   @Override
   public boolean supportsParameter(MethodParameter parameter) {
      return UserInfo.class.isAssignableFrom(parameter.getParameterType());
   }
   @Override
   public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
                          NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
      HttpServletRequest nativeRequest = (HttpServletRequest) webRequest.getNativeRequest();
      String token = nativeRequest.getHeader("token");
      Long userId = (Long) redisTemplate.opsForValue().get(token);//Pseudo code, assuming that our token is placed in redis
      UserInfo useInfo = userInfoRepository.getOne(userId);
      return useInfo;
   }
}
//2. We only need to add userInfoArgumentResolver to MyWebMvcConfigurer. The key code is as follows:
@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
   @Autowired
   private MyPageableHandlerMethodArgumentResolver myPageableHandlerMethodArgumentResolver;
@Autowired
private UserInfoArgumentResolver userInfoArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
   resolvers.add(myPageableHandlerMethodArgumentResolver);
   //We only need to add userInfoArgumentResolver to resolvers
   resolvers.add(userInfoArgumentResolver);
}
}
// 3. Used in Controller
@RestController
public class UserInfoController {
  //Get the current user's information
  @GetMapping("user/info")
  public UserInfo getUserInfo(UserInfo userInfo) {
     return userInfo;
  }
  //Say hello to the current user
  @PostMapping("sayHello")
  public String sayHello(UserInfo userInfo) {
    return "hello " + userInfo.getTelephone();
  }
}

As you can see from the above code, the process of getting the current user information from redis based on the token can be completely omitted in the Controller, which optimizes the operation process.

Scene 4

Sometimes we will also change the default value and parameter name of Pageable. We can also configure the customization through the following Key value in the application.properties file, as shown in the figure below:

Drawing 8.png

Regarding parameter processing related to Spring MVC and Spring Data, you can basically master it by understanding the above content and doing some hands-on operations. But the actual work will definitely not be that simple, and you will also encounter the needs of other methods in WebMvcConfigurer. I will introduce them to you by the way.

Expand ideas

WebMvcConfigurer Introduction

When we do Spring MVC development, we may implement WebMvcConfigurer to do some common business logic. I will list a few common methods below for your convenience.

Copy code

 /* Interceptor configuration */
void addInterceptors(InterceptorRegistry var1);
/* View jump controller */
void addViewControllers(ViewControllerRegistry registry);
/**
  *Static resource processing
**/
void addResourceHandlers(ResourceHandlerRegistry registry);
/*Default static resource processor */
void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer);
/**
  *Configure the view resolver here
 **/
void configureViewResolvers(ViewResolverRegistry registry);
/* Configure some options for content adjudication */
void configureContentNegotiation(ContentNegotiationConfigurer configurer);
/** Solve cross-domain issues **/
void addCorsMappings(CorsRegistry registry);
/** Add the processing of the return result of the controller **/
void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers);

When we implement a Restful-style API protocol, we often see that it uniformly encapsulates the json response results. We can also use HandlerMethodReturnValueHandler to implement it. Let’s look at an example.

Use Result to uniformly encapsulate JSON return results

The following is an example of using custom annotations to implement JSON result encapsulation using HandlerMethodReturnValueHandler through five steps.

Step 1: We define a custom annotation @WarpWithData, which means that the return result of this annotation package is packaged with Data. The code is as follows:

Copy code

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
/**
 * Customize an annotation to wrap the returned results
 */
public @interface WarpWithData {
}

Step 2: Customize MyWarpWithDataHandlerMethodReturnValueHandler and inherit RequestResponseBodyMethodProcessor to implement the HandlerMethodReturnValueHandler interface to process the results of Data packaging. The code is as follows:

Copy code

//Customize your own return processing class. We directly inherit RequestResponseBodyMethodProcessor, so that we can use the methods in the parent class directly.
@Component
public class MyWarpWithDataHandlerMethodReturnValueHandler extends RequestResponseBodyMethodProcessor implements HandlerMethodReturnValueHandler {
   //Refer to the parent class RequestResponseBodyMethodProcessor.
   @Autowired
   public MyWarpWithDataHandlerMethodReturnValueHandler(List<HttpMessageConverter<?>> converters) {
      super(converters);
   }
   //Method that only handles annotations that need to be packaged
   @Override
   public boolean supportsReturnType(MethodParameter returnType) {
      return returnType.hasMethodAnnotation(WarpWithData.class);
   }
   //Wrap the returned result with a layer of Data
   @Override
   public void handleReturnValue(Object returnValue, MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest) throws IOException, HttpMediaTypeNotAcceptableException {
      Map<String,Object> res = new HashMap<>();
      res.put("data",returnValue);
      super.handleReturnValue(res,methodParameter,modelAndViewContainer,nativeWebRequest);
   }
}

Step 3: Directly add myWarpWithDataHandlerMethodReturnValueHandler to handlers in MyWebMvcConfigurer This is also done by overriding the addReturnValueHandlers method in the parent class WebMvcConfigurer. The key code is as follows:

Copy code

@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
   @Autowired
   private MyWarpWithDataHandlerMethodReturnValueHandler myWarpWithDataHandlerMethodReturnValueHandler;
   //Add our custom myWarpWithDataHandlerMethodReturnValueHandler to handlers
   @Override
   public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> handlers) {
      handlers.add(myWarpWithDataHandlerMethodReturnValueHandler);
   }
   
  @Autowired
  private RequestMappingHandlerAdapter requestMappingHandlerAdapter;
  //Due to the priority issue handled by HandlerMethodReturnValueHandler, we put our customized myWarpWithDataHandlerMethodReturnValueHandler first through the following method;
  @PostConstruct
  public void init() {
     List<HandlerMethodReturnValueHandler> returnValueHandlers = Lists.newArrayList(myWarpWithDataHandlerMethodReturnValueHandler);
//Take out the original list and overwrite it again;
        returnValueHandlers.addAll(requestMappingHandlerAdapter.getReturnValueHandlers());
     requestMappingHandlerAdapter.setReturnValueHandlers(returnValueHandlers);
  }
}

What needs to be noted here is that we used @PostConstruct to adjust the loading priority of HandlerMethodReturnValueHandler to make it effective.

Step 4: Add the @WarpWithData annotation directly to the Controller method. The key code is as follows:

Copy code

@GetMapping("/user/{id}")
@WarpWithData
public UserInfo getUserInfoFromPath(@PathVariable("id") Long id) {
   return userInfoRepository.getOne(id);
}

Step 5: Let’s test it.

Copy code

GET http://127.0.0.1:8089/user/1

You will get the following results. You will find that our JSON results have an additional Data wrapper.

Copy code

{
  "data": {
    "id": 1,
    "version": 0,
    "createUserId": null,
    "createTime": "2020-10-23T00:23:10.185Z",
    "lastModifiedUserId": null,
    "lastModifiedTime": "2020-10-23T00:23:10.185Z",
    "ages": 10,
    "telephone": null,
    "hibernateLazyInitializer": {}
  }
}

Through five steps, we used the extension mechanism of Spring MVC to achieve unified processing of the format of the returned results. I don’t know if you have mastered this method. I hope you can practice it more and use it better.