Spring custom parameter parser~

1. What is a parameter parser

Are these annotations @RequstBody, @RequstParam familiar?

We often use such parameter annotations when developing Controller interfaces. What are the functions of these annotations? Do we really understand?

In simple terms, these annotations are to help us directly parse the parameters passed by the front end into javaBean that can be directly used in code logic. For example, @RequstBody receives json parameters and converts them into java objects, as shown below:

Foreground pass parameters parameter format
{ "userId": 1, "userName": "Alex"} application/json

The normal code is written as follows:

@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestBody UserInfo userInfo){
    //***
    return userInfo.getName();
}


But if the way the service receives parameters is changed, the parameters cannot be successfully received in the following code. Why is this?

@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestBody String userName, @RequestBody Integer userId){
    //***
    return userName;
}


If the above code slightly changes the use of annotations and changes the parameter passing format in the foreground, it can be parsed normally.

Foreground pass parameters parameter format
http://***?userName=Alex&userId=1 none
@RequestMapping(value = "/getUserInfo")
public String getUserInfo(@RequestParam String userName, @RequestParam Integer userId){
    //***
    return userName;
}


Here we have to introduce the corresponding content behind these annotations—parameter parsers provided by Spring. These parameter parsers help us parse the parameters passed from the front desk, bind them to the Controller input parameters we defined, and pass parameters in different formats. , requires different parameter parsers, sometimes some special parameter formats, and even requires us to customize a parameter parser.

Whether in SpringBoot or in Spring MVC, an HTTP request will be received by the DispatcherServlet class (essentially a Servlet, inherited from HttpServlet). Spring is responsible for obtaining and parsing the request from HttpServlet, matching the request uri to the Controller class method, parsing the parameters and executing the method, and finally processing the return value and rendering the view.

The function of the parameter parser is to convert the parameters submitted by the http request into the input parameters of our controller processing unit . The way the original Servlet obtains parameters is as follows, you need to manually obtain the required information from HttpServletRequest.

@WebServlet(urlPatterns="/getResource")
public class resourceServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        /**获取参数开始*/
        String resourceId = req.getParameter("resourceId");
        String resourceType = req.getHeader("resourceType");
        /**获取参数结束*/
        resp.setContentType("text/html;charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.println("resourceId " + resourceId + " resourceType " + resourceType);
    }
}


In order to help developers liberate their productivity, Spring provides parameter parsers with specific formats (the type corresponding to the content-type in the header). We only need to add specific annotations to the interface parameters (of course, there are default parsers without annotations) ), you can directly get the parameters you want, without having to manually get the original input parameters from HttpServletRequest yourself, as shown below:

@RestController
public class resourceController {

  @RequestMapping("/resource")
  public String getResource(@RequestParam("resourceId") String resourceId,
            @RequestParam("resourceType") String resourceType,
            @RequestHeader("token") String token) {
    return "resourceId" + resourceId + " token " + token;
  }
}


Common usage methods of annotation class parameter parsers and the corresponding relationship with annotations are as follows:

Annotation naming Placement use
@PathVariable placed before the parameter Allow request parameters in the url path
@RequestParam placed before the parameter Allows the parameters of the request to be directly connected after the url address, which is also Spring's default parameter parser
@RequestHeader placed before the parameter Get parameters from the request header
@RequestBody placed before the parameter Allow the parameters of the request to be in the parameter body instead of directly following the address
Annotation naming Corresponding parser content-type
@PathVariable PathVariableMethodArgumentResolver none
@RequestParam RequestParamMethodArgumentResolver None (get request) and multipart/form-data
@RequestBody RequestResponseBodyMethodProcessor application/json
@RequestPart RequestPartMethodArgumentResolver multipart/form-data

2. Principle of parameter parser

To understand the parameter parser, we must first understand the execution process of the most primitive Spring MVC. After the client user initiates an Http request, the request will be submitted to the front controller (Dispatcher Servlet), and the front controller will request the processor mapper (step 1), and the processor mapper will return an execution chain (Handler Execution step 2 ), the interceptor we usually define is executed at this stage, and then the front controller will send the Handler information in the execution chain returned by the mapper to the adapter (Handler Adapter step 3), and the adapter will find and execute the corresponding Handler logic, that is, the Controller control unit we defined (step 4). After the Handler is executed, it will return a ModelAndView object, which can be returned to the client request response information after parsing and view rendering by the view parser.

When the container is initialized, the RequestMappingHandlerMapping mapper will store the method annotated with the @RequestMapping annotation into the cache, where the key is RequestMappingInfo and the value is HandlerMethod. How HandlerMethod performs method parameter parsing and binding, it is necessary to understand the request parameter adapter **RequestMappingHandlerAdapter, ** This adapter corresponds to the next parameter parsing and binding process. The source path is as follows:

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter

 The general parsing and binding process of RequestMappingHandlerAdapter is shown in the figure below.

RequestMappingHandlerAdapter implements the interface InitializingBean. After the Spring container initializes the Bean, call the method afterPropertiesSet ( ) to bind the default parameter resolver to the parameter argumentResolvers of the HandlerMethodArgumentResolverComposite adapter, where HandlerMethodArgumentResolverComposite is the implementation class of the interface HandlerMethodArgumentResolver. The source path is as follows:

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#afterPropertiesSet

@Override
public void afterPropertiesSet() {
   // Do this first, it may add ResponseBody advice beans
   initControllerAdviceCache();

   if (this.argumentResolvers == null) {
      /**  */
      List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
      this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
   }
   if (this.initBinderArgumentResolvers == null) {
      List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
      this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
   }
   if (this.returnValueHandlers == null) {
      List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
      this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
   }
}


Through the getDefaultArgumentResolvers ( ) method, you can see which default parameter resolvers Spring provides us, and these resolvers are all   implementation classes of the HandlerMethodArgumentResolver interface.

For different parameter types, Spring provides some basic parameter parsers, including annotation-based parsers, specific type-based parsers, and of course default parsers. If the existing parsers cannot meet the parsing requirements , Spring also provides an extension point that supports user-defined parsers, the source code is as follows:

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#getDefaultArgumentResolvers

private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
   List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();

   // Annotation-based argument resolution 基于注解
   /** @RequestPart 文件注入 */
   resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
   /** @RequestParam 名称解析参数 */
   resolvers.add(new RequestParamMapMethodArgumentResolver());
   /** @PathVariable url路径参数 */
   resolvers.add(new PathVariableMethodArgumentResolver());
   /** @PathVariable url路径参数,返回一个map */
   resolvers.add(new PathVariableMapMethodArgumentResolver());
   /** @MatrixVariable url矩阵变量参数 */
   resolvers.add(new MatrixVariableMethodArgumentResolver());
   /** @MatrixVariable url矩阵变量参数 返回一个map*/
   resolvers.add(new Matrix VariableMapMethodArgumentResolver());
   /** 兜底处理@ModelAttribute注解和无注解 */
   resolvers.add(new ServletModelAttributeMethodProcessor(false));
   /** @RequestBody body体解析参数 */
   resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
   /** @RequestPart 使用类似RequestParam */
   resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
   /** @RequestHeader 解析请求header */
   resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
   /** @RequestHeader 解析请求header,返回map */
   resolvers.add(new RequestHeaderMapMethodArgumentResolver());
   /** Cookie中取值注入 */
   resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
   /** @Value */
   resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
   /** @SessionAttribute */
   resolvers.add(new SessionAttributeMethodArgumentResolver());
   /** @RequestAttribute */
   resolvers.add(new RequestAttributeMethodArgumentResolver());

   // Type-based argument resolution 基于类型
   /** Servlet api 对象 HttpServletRequest 对象绑定值 */
   resolvers.add(new ServletRequestMethodArgumentResolver());
   /** Servlet api 对象 HttpServletResponse 对象绑定值 */
   resolvers.add(new ServletResponseMethodArgumentResolver());
   /** http请求中 HttpEntity RequestEntity数据绑定 */
   resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
   /** 请求重定向 */
   resolvers.add(new RedirectAttributesMethodArgumentResolver());
   /** 返回Model对象 */
   resolvers.add(new ModelMethodProcessor());
   /** 处理入参,返回一个map */
   resolvers.add(new MapMethodProcessor());
   /** 处理错误方法参数,返回最后一个对象 */
   resolvers.add(new ErrorsMethodArgumentResolver());
   /** SessionStatus */
   resolvers.add(new SessionStatusMethodArgumentResolver());
   /**  */
   resolvers.add(new UriComponentsBuilderMethodArgumentResolver());

   // Custom arguments 用户自定义
   if (getCustomArgumentResolvers() != null) {
      resolvers.addAll(getCustomArgumentResolvers());
   }

   // Catch-all 兜底默认
   resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
   resolvers.add(new ServletModelAttributeMethodProcessor(true));

   return resolvers;
}


Only two methods are defined in the HandlerMethodArgumentResolver  interface, which are the resolver applicability determination method supportsParameter ( ) and the parameter resolution method resolveArgument(). The differences in the use of parameter resolvers for different purposes are reflected in these two methods, which are not discussed here. Specifically expand the parsing and binding process of parameters.

3. Design of custom parameter parser

The design of Spring implements the principle of opening and closing very well. It not only integrates many very powerful capabilities in the package, but also reserves the ability for users to customize and expand. The same is true for the parameter parser. The parameter parser provided by Spring can basically satisfy Commonly used parameter analysis capabilities, but the parameter transmission of many systems is not standardized. For example, the business parameters transmitted by the JD color gateway are encapsulated in the body. It is necessary to extract the business parameters from the body first, and then analyze them in a targeted manner. At this time, Spring provides The parser can't help us, we need to extend the custom adaptation parameter parser.

Spring provides two ways to customize the parameter resolver, one is to implement the adapter interface  HandlerMethodArgumentResolver , and the other is to inherit the existing parameter resolver (  the existing implementation class of  the HandlerMethodArgumentResolver interface) such as AbstractNamedValueMethodArgumentResolver  for enhanced optimization. If it is a deeply customized custom parameter parser, it is recommended to implement its own interface for development. Taking the implementation of an interface adapter interface custom development parser as an example, it introduces how to customize a parameter parser.

By looking at the source code, I found that there are two methods for the parameter parsing adapter interface to be extended, namely supportsParameter () and resolveArgument (), the first method is the scene where the custom parameter parser is applicable, that is, how to hit the parameter parser , and the second one is the implementation of specific parsing parameters.

public interface HandlerMethodArgumentResolver {

   /**
    * 识别到哪些参数特征,才使用当前自定义解析器
    */
   boolean supportsParameter(MethodParameter parameter);

   /**
    * 具体参数解析方法
    */
   Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
         NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception;

}



Now start to implement an annotation-based custom parameter parser. This is the parameter parser used in the actual use of the code. It obtains the body business parameters of the color gateway, and then parses them for the Controller method to use directly.

public class ActMethodArgumentResolver implements HandlerMethodArgumentResolver {
    private static final String DEFAULT_VALUE = "body";

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        /** 只有指定注解注释的参数才会走当前自定义参数解析器 */
        return parameter.hasParameterAnnotation(RequestJsonParam.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        /** 获取参数注解 */
        RequestJsonParam attribute = parameter.getParameterAnnotation(RequestJsonParam.class);
        
        /** 获取参数名 */
        String name = attribute.value();
        /** 获取指定名字参数的值 */
        String value = webRequest.getParameter(StringUtils.isEmpty(name) ? DEFAULT_VALUE : name);
        /** 获取注解设定参数类型 */
        Class<?> targetParamType = attribute.recordClass();
        /** 获取实际参数类型 */
        Class<?> webParamType = parameter.getParameterType()
        /** 以自定义参数类型为准 */
        Class<?> paramType = targetParamType != null ? targetParamType : parameter.getParameterType();
        if (ObjectUtils.equals(paramType, String.class) 
            || ObjectUtils.equals(paramType, Integer.class)
            || ObjectUtils.equals(paramType, Long.class) 
            || ObjectUtils.equals(paramType, Boolean.class)) {
                JSONObject object = JSON.parseObject(value);
                log.error("ActMethodArgumentResolver resolveArgument,paramName:{}, object:{}", paramName, JSON.toJSONString(object));
                if (object.get(paramName) instanceof Integer && ObjectUtils.equals(paramType, Long.class)) {
                    //入参:Integer  目标类型:Long
                    result = paramType.cast(((Integer) object.get(paramName)).longValue());
                }else if (object.get(paramName) instanceof Integer && ObjectUtils.equals(paramType, String.class)) {
                    //入参:Integer  目标类型:String
                    result = String.valueOf(object.get(paramName));
                }else if (object.get(paramName) instanceof Long && ObjectUtils.equals(paramType, Integer.class)) {
                    //入参:Long  目标类型:Integer(精度丢失)
                    result = paramType.cast(((Long) object.get(paramName)).intValue());
                }else if (object.get(paramName) instanceof Long && ObjectUtils.equals(paramType, String.class)) {
                    //入参:Long  目标类型:String
                    result = String.valueOf(object.get(paramName));
                }else if (object.get(paramName) instanceof String && ObjectUtils.equals(paramType, Long.class)) {
                    //入参:String  目标类型:Long
                    result = Long.valueOf((String) object.get(paramName));
                } else if (object.get(paramName) instanceof String && ObjectUtils.equals(paramType, Integer.class)) {
                    //入参:String  目标类型:Integer
                    result = Integer.valueOf((String) object.get(paramName));
                } else {
                    result = paramType.cast(object.get(paramName));
                }
        }else if (paramType.isArray()) {
            /** 入参是数组 */
            result = JsonHelper.fromJson(value, paramType);
            if (result != null) {
                Object[] targets = (Object[]) result;
                for (int i = 0; i < targets.length; i++) {
                   WebDataBinder binder = binderFactory.createBinder(webRequest, targets[i], name + "[" + i + "]");
                   validateIfApplicable(binder, parameter, annotations);
                }
             }
       } else if (Collection.class.isAssignableFrom(paramType)) {
            /** 这里要特别注意!!!,集合参数由于范型获取不到集合元素类型,所以指定类型就非常关键了 */
            Class recordClass = attribute.recordClass() == null ? LinkedHashMap.class : attribute.recordClass();
            result = JsonHelper.fromJsonArrayBy(value, recordClass, paramType);
            if (result != null) {
               Collection<Object> targets = (Collection<Object>) result;
               int index = 0;
               for (Object targetObj : targets) {
                   WebDataBinder binder = binderFactory.createBinder(webRequest, targetObj, name + "[" + (index++) + "]");
                   validateIfApplicable(binder, parameter, annotations);
               }
            }
        } else{
              result = JSON.parseObject(value, paramType);
        }
    
        if (result != null) {
            /** 参数绑定 */
            WebDataBinder binder = binderFactory.createBinder(webRequest, result, name);
            result = binder.convertIfNecessary(result, paramType, parameter);
            validateIfApplicable(binder, parameter, annotations);
            mavContainer.addAttribute(name, result);
        }
    }


The definition of the custom parameter parser annotation is as follows. Here, a special attribute recordClass is defined, and what problem is solved will be discussed later.

/**
 * 请求json参数处理注解
 * @author wangpengchao01
 * @date 2022-11-07 14:18
 */
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestJsonParam {
    /**
     * 绑定的请求参数名
     */
    String value() default "body";

    /**
     * 参数是否必须
     */
    boolean required() default false;

    /**
     * 默认值
     */
    String defaultValue() default ValueConstants.DEFAULT_NONE;

    /**
     * 集合json反序列化后记录的类型
     */
    Class recordClass() default null;
}


Register the custom parser into the Spring container through the configuration class

@Configuration
public class WebConfig extends WebMvcConfigurerAdapter {

    @Bean
    public static ActMethodArgumentResolver actMethodArgumentResolverConfigurer() {
        return new ActMethodArgumentResolver();
    }

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


At this point, a complete annotation-based custom parameter parser is completed.

4. Summary

Understanding the principle of Spring's parameter parser is helpful for the correct use of Spring's parameter parser, and it also allows us to design a parameter parser suitable for our own system. For the parsing of some common parameter types, the writing of repeated codes is reduced, but there is a premise here The input parameters of complex types in our project must be unified , and the format of the parameters passed by the front end must also be unified . Otherwise, designing a custom parameter parser will be a disaster, and various complicated compatibility work needs to be done. The design of the parameter parser should be placed in the initial stage of project development as much as possible. If there is no unified specification for interface development of a system with a complex history, it is not recommended to customize the design of the parameter parser.

Guess you like

Origin blog.csdn.net/crg18438610577/article/details/130152573