zoukankan      html  css  js  c++  java
  • 自定义spring参数注解

    本文主要描述怎样自定义类似@RequestBody这样的参数注解来打破@RequestBody的单体限制。

    目录
    1 @RequestBody的单体限制
    2 自定义spring的参数注解
    3 编写spring的参数注解解析器
    4 将自定义参数注解解析器设置到spring的参数解析器集合中
    5 指定参数解析器的优先级

    一、@RequestBody的单体限制
    @RequestBody的作用:将请求体中的整体数据转化为对象。

    1     @RequestMapping(value = "/body", method = RequestMethod.POST)
    2     public Book testCommon(@RequestBody Book book) {
    3         return book;
    4     }

    springmvc具有一个参数解析器容器RequestMappingHandlerAdapter.argumentResolvers,该参数的初始化在RequestMappingHandlerAdapter#afterPropertiesSet()

     1     public void afterPropertiesSet() {
     2         ......
     3         if (this.argumentResolvers == null) {
     4             List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
     5             this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
     6         }
     7         ......
     8     }
     9 
    10     /**
    11      * Return the list of argument resolvers to use including built-in resolvers
    12      * and custom resolvers provided via {@link #setCustomArgumentResolvers}.
    13      */
    14     private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
    15         List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();
    16 
    17         // Annotation-based argument resolution
    18         resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
    19         resolvers.add(new RequestParamMapMethodArgumentResolver());
    20         resolvers.add(new PathVariableMethodArgumentResolver());
    21         resolvers.add(new PathVariableMapMethodArgumentResolver());
    22         resolvers.add(new MatrixVariableMethodArgumentResolver());
    23         resolvers.add(new MatrixVariableMapMethodArgumentResolver());
    24         resolvers.add(new ServletModelAttributeMethodProcessor(false));
    25         resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
    26         resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
    27         resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
    28         resolvers.add(new RequestHeaderMapMethodArgumentResolver());
    29         resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
    30         resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
    31         resolvers.add(new SessionAttributeMethodArgumentResolver());
    32         resolvers.add(new RequestAttributeMethodArgumentResolver());
    33 
    34         // Type-based argument resolution
    35         resolvers.add(new ServletRequestMethodArgumentResolver());
    36         resolvers.add(new ServletResponseMethodArgumentResolver());
    37         resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
    38         resolvers.add(new RedirectAttributesMethodArgumentResolver());
    39         resolvers.add(new ModelMethodProcessor());
    40         resolvers.add(new MapMethodProcessor());
    41         resolvers.add(new ErrorsMethodArgumentResolver());
    42         resolvers.add(new SessionStatusMethodArgumentResolver());
    43         resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
    44 
    45         // Custom arguments
    46         if (getCustomArgumentResolvers() != null) {
    47             resolvers.addAll(getCustomArgumentResolvers());
    48         }
    49 
    50         // Catch-all
    51         resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
    52         resolvers.add(new ServletModelAttributeMethodProcessor(true));
    53 
    54         return resolvers;
    55     }

    可以看出springmvc的参数解析器容器中存放着内置的参数解析器 + 自定义解析器,这里边就包括@RequestBody的解析器RequestResponseBodyMethodProcessor,来看一下这个解析器的主要方法:

     1     @Override
     2     public boolean supportsParameter(MethodParameter parameter) {
     3         return parameter.hasParameterAnnotation(RequestBody.class);
     4     }
     5 
     6     @Override
     7     public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
     8             NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
     9                  // 这里使用MappingJackson2HttpMessageConverter将输入流body体中的转化为Book对象
    10     }

    这里注意两点:

    1、一个参数解析器最重要的方法有两个:
    (1)supportsParameter 指定哪些参数使用该解析器进行解析
    (2)resolveArgument 对参数进行真正的解析操作

    这也是自定义参数解析器需要去实现的两个方法(见“三”)

    2、在解析器容器中,自定义解析器是位于内置解析器之后,这个顺序也是解析器的优先级,也就是说假设有一个参数同时满足两个解析器,只有第一个解析器会生效,那么怎么去调整这个解析器的顺序呢?(见“五”)

    好,现在,我们已经大致了解了springmvc的参数解析器,以及@RequestBody的解析过程。那么来看一下这个例子:

    1     @RequestMapping(value = "/two-body", method = RequestMethod.POST)
    2     public Book testCommon(@RequestBody Book book1, @RequestBody Book book2) {
    3         Book book = new Book();
    4         book.setId(Optional.ofNullable(book1).orElse(book2).getId());
    5         book.setName(Optional.ofNullable(book1).orElse(book2).getName());
    6         return book;
    7     }

    有两个@RequestBody,一执行,结果抛错:

    1 {
    2   "status": 400,
    3   "error": "Bad Request",
    4   "exception": "org.springframework.http.converter.HttpMessageNotReadableException",
    5   "message": "I/O error while reading input message; nested exception is java.io.IOException: Stream closed",
    6 }

    400通常是输入参数错误,错误原因:从上文对@RequestBody的解析过程的分析来看,这个参数实际上是将输入流的body体作为一个整体进行转换,而body整体只有一份,解析完成之后会关闭输入流,所以第二个参数book2的解析就会抛错。

    当前,解决此类的方案有两种:

    1、@RequestBody List<Book> books

    2、@RequestBody MultiObject books

    不管是哪一种,其实都是将众多的对象组成一个,因为在springmvc的一个方法中只能有一个@RequestBody,这被称为单体限制。其实在有些场景下,我就是想实现多个@RequestBody这样的功能,该怎么办?(我在实现kspringfox框架的时候,就遇到了这样的诉求:kspringfox是一个扩展了springfox的框架,主要实现了对dubbo接口的文档化,以及将dubbo接口透明的转为rest接口供我们调用的功能)

    下面我们就来实现这样一个功能。

    二、自定义spring的参数注解
    首先自定义一个类似于@RequestBody的注解:@RequestModel

    1 @Target(ElementType.PARAMETER)
    2 @Retention(RetentionPolicy.RUNTIME)
    3 public @interface RequestModel {
    4     String value() default "";
    5     boolean required() default false;
    6 }

    自定义注解很简单:@Target指明注解应用于参数上;@Retention指明注解应用于运行时。

    三、编写spring的参数注解解析器

     1 public class RequestModelArgumentResolver implements HandlerMethodArgumentResolver {
     2     @Override
     3     public boolean supportsParameter(MethodParameter parameter) {
     4         return parameter.hasParameterAnnotation(RequestModel.class);
     5     }
     6 
     7     @Override
     8     public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
     9                                   NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
    10         final String parameterJson = webRequest.getParameter(parameter.getParameterName());
    11 
    12         //parameter.getGenericParameterType() 返回参数的完整类型(带泛型) 
    13         final Type type = parameter.getGenericParameterType();
    14         final Object o = JSON.parseObject(parameterJson, type);
    15         return o;
    16     }
    17 }

    注意:
    1 supportsParameter方法指明RequestModelArgumentResolver只处理带有@RequestModel注解的参数;
    2 resolveArgument方法对入参进行解析:首先获取参数值(json串),然后获取参数的完整类型(带泛型),最后使用fastjson解析器将json格式的参数值转化为具体类型的对象。

    四、将自定义参数解析器设置到spring的参数解析器集合中

    1 @Configuration
    2 public class WebConfig extends WebMvcConfigurerAdapter {
    3     @Override
    4     public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
    5         argumentResolvers.add(new RequestModelArgumentResolver());
    6     }
    7 }

    通过上述这种方式,我们就将自定义的RequestModelArgumentResolver解析器添加到了spring的自定义参数解析器集合中。

    此时,一个自定义的参数注解就可以基本使用在我们的项目中了。简单的做个测试:

    1     @RequestMapping(value = "/two-model", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    2     public Book testModel(@RequestModel(value = "book1") Book book1, @RequestModel(value = "book2") Book book2) {
    3         Book book = new Book();
    4         book.setId(book1.getId());
    5         book.setName(book2.getName());
    6         return book;
    7     }

    前端调用:(有错误跳过)

    1 const params = new URLSearchParams()
    2 params.append('book1', '{"id": 1,"name": "11"}')
    3 params.append('book2', '{"id": 2,"name": "22"}')
    4 return axios.post('http://localhost:8080/dubbo-api/two-model', params)
    5         .then(res => {
    6           ...
    7         }).catch(
    8           err => ...
    9         )

    五、指定参数解析器的优先级
    通过前边的步骤,一个自定义的参数注解就“基本”可以使用了,但是还有一个问题。看这个例子,

    1     @RequestMapping(value = "/map", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
    2     public Map<String, Book> testMap(@RequestModel(value = "title2Book") Map<String, Book> title2Book) {
    3         return title2Book;
    4     }

    我们在“三”中的RequestModelArgumentResolver#supportsParameter方法中打断点来debug一下,发现上边这个例子根本不会走进去,也就是说此时我们自定义的RequestModelArgumentResolver不再起作用了。

    原因:在springmvc的解析器容器中,自定义解析器是放在内置解析器之后的,这个顺序也是解析器的优先级,也就是说假设有一个参数同时满足两个解析器,只有第一个解析器会生效。而springmvc对Map是专门有一个内置解析器的,这个解析器位于我们的RequestModelArgumentResolver之前,所以springmvc会使用Map解析器进行解析,而不再使用RequestModelArgumentResolver。

    具体源码我们再翻回头看一下“一”中的getDefaultArgumentResolvers:

     1     /**
     2      * Return the list of argument resolvers to use including built-in resolvers
     3      * and custom resolvers provided via {@link #setCustomArgumentResolvers}.
     4      */
     5     private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
     6         List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();
     7         ...
     8                //Map解析器
     9         resolvers.add(new MapMethodProcessor());
    10         ...
    11         // 自定义解析器
    12         if (getCustomArgumentResolvers() != null) {
    13             resolvers.addAll(getCustomArgumentResolvers());
    14         }
    15         return resolvers;
    16     }

    看一下MapMethodProcessor#supportsParameter

    1     @Override
    2     public boolean supportsParameter(MethodParameter parameter) {
    3         return Map.class.isAssignableFrom(parameter.getParameterType());
    4     }

    原因明了了以后,就要去想解决方案。(如果spring可以提供为参数解析器设置order的能力,那么就好了,但是spring没有提供)

    第一种方案
    在服务启动时,动态替换掉MapMethodProcessor#supportsParameter的字节码。

    1     @Override
    2     public boolean supportsParameter(MethodParameter parameter) {
    3                 if(parameter.hasParameterAnnotation(RequestModel.class)){
    4                          return false;
    5                 }
    6         return Map.class.isAssignableFrom(parameter.getParameterType());
    7     }

    使用javassist可以实现这一点,但是这样去做,代码复杂性较高。“任何一个功能的实现,都要想办法降低代码复杂性

    第二种方案
    首先删除"四"中的WebConfig,让spring不再自动的将自定义解析器加到RequestMappingHandlerAdapter的解析器容器中;然后我们通过下面的方式手动的将RequestModelArgumentResolver加载到RequestMappingHandlerAdapter的解析容器中。(通过这样的方式,我们可以任意的指定解析器的顺序)

     1 @Configuration
     2 public class MethodArgumentResolver {
     3     @Autowired
     4     private RequestMappingHandlerAdapter adapter;
     5 
     6     @PostConstruct
     7     public void injectSelfMethodArgumentResolver() {
     8         List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();
     9         argumentResolvers.add(new RequestModelArgumentResolver());
    10         argumentResolvers.addAll(adapter.getArgumentResolvers());
    11         adapter.setArgumentResolvers(argumentResolvers);
    12     }
    13 }
  • 相关阅读:
    c# Action,Func,Predicate委托
    c# 匿名方法
    IIS网站无法启动,提示 另一个程序正在使用此文件
    c# Http下载
    mciSendString详解(转)
    【NOIP2006PJ】数列(sequence)题解
    2020.04.29【NOIP普及组】模拟赛C组30总结
    【USACO 2019 December Silver】Milk Visits题解
    【USACO 2019 February Bronze】Measuring Traffic 题解
    【USACO 2019 February Bronze】Measuring Traffic 题解
  • 原文地址:https://www.cnblogs.com/java-zhao/p/9119258.html
Copyright © 2011-2022 走看看