自定义spring参数注解 - 打破@RequestBody单体限制

本文主要描述怎样自定义类似@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 }

原文地址:https://www.cnblogs.com/java-zhao/p/9119258.html

时间: 2024-11-09 09:30:49

自定义spring参数注解 - 打破@RequestBody单体限制的相关文章

Spring Controller中获取输入参数注解使用

1.处理request的uri部分的参数:@PathVariable. 2.处理request header部分的参数:@RequestHeader,@CookieValue@RequestHeader 注解,可以把Request请求header部分的值绑定到方法的参数上.@CookieValue 可以把Request header中关于cookie的值绑定到方法的参数上. [email protected]注解用来接收地址中的参数,参数的格式是http://*****?uid=111111&u

Spring自定义argumentResolver参数解析器

在一个web程序中,当一个HTTP请求进来时,会被容器处理进而转换成一个servlet请求.http请求所携带的数据,虽然是格式化的但是无类型:而java作为强类型语言,同时为了健壮性考虑,必然要有完善的类型约束.当然,那么,将数据从servlet请求中转换到java中,一个很原始的方式是手动处理.幸好,Spring MVC通过以注解往函数添加额外信息的方式,使得上述的数据转换过程能够交由框架自动处理.从一个角度去看,Controller中的函数声明及注解定义了此HTTP请求的数据格式和类型,也

Springboot中使用自定义参数注解获取 token 中用户数据

使用自定义参数注解获取 token 中User数据 使用背景 在springboot项目开发中需要从token中获取用户信息时通常的方式要经历几个步骤 拦截器中截获token TokenUtil工具类解析token中的用户信息 把解析结果存入到成员变量中 controller中通过TokenUtil工具类提供的静态方法获取用户信息 下面是过程示例代码 /*--------1.拦截器中获取---------*/ String token =request.getHeader("token"

spring(6)--注解式控制器

6.1.注解式控制器简介 一.Spring2.5之前,我们都是通过实现Controller接口或其实现来定义我们的处理器类.已经@Deprecated.   二.Spring2.5引入注解式处理器支持,通过@Controller 和 @RequestMapping注解定义我们的处理器类. 并且提供了一组强大的注解: 需要通过处理器映射DefaultAnnotationHandlerMapping和处理器适配器AnnotationMethodHandlerAdapter来开启支持@Controll

Spring JSR-250注解

Spring JSR-250注解 注释配置相对于 XML 配置具有很多的优势: 它可以充分利用 Java 的反射机制获取类结构信息,这些信息可以有效减少配置的工作.如使用 JPA 注释配置 ORM 映射时,我们就不需要指定 PO 的属性名.类型等信息,如果关系表字段和 PO 属性名.类型都一致,您甚至无需编写任务属性映射信息——因为这些信息都可以通过 Java 反射机制获取. 注释和 Java 代码位于一个文件中,而 XML 配置采用独立的配置文件,大多数配置信息在程序开发完成后都不会调整,如果

spring常用注解使用讲解

spring常用注解使用讲解 本文讲述spring的几个常用的注解 @RequestMapping @RequestParam @ResponseBody @RequestBody @Autowired 一.@RequestMapping  RequestMapping是一个用来处理请求地址映射的注解,可用于类或方法上.用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径. RequestMapping注解有六个属性,下面我们把她分成三类进行说明. 1. value, method: v

转:Spring AOP 注解方式实现的一些“坑”

使用过Spring AOP的注解方式实现,也入过不少坑,现在做一下记录,希望有同样需求的朋友可以少走弯路 使用之前还是先过一下官方文档吧,至少可以少走弯路,不用担心英文读不懂,其实只看代码例子就能理解很多问题! 1. SpringBoot如何获得Aspect支持? 看这里:Spring Boot 开启AOP的方法 2. 我按照上面的方法开启了AOP支持,为什么始终是不走切入点方法呢? 首先仔细检查一下,Aspect类是不是少了@Component注解(这一点很重要!),只有一个@Aspect注解

aop注解 自定义切面的注解写法

spring.xml中 1 <?xml version="1.0" encoding="UTF-8"?> 2 <beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:context="http://www.spr

spring(7)--注解式控制器的数据验证、类型转换及格式化

7.1.简介 在编写可视化界面项目时,我们通常需要对数据进行类型转换.验证及格式化. 一.在Spring3之前,我们使用如下架构进行类型转换.验证及格式化: 流程: ①:类型转换:首先调用PropertyEditor的setAsText(String),内部根据需要调用setValue(Object)方法进行设置转换后的值: ②:数据验证:需要显示调用Spring的Validator接口实现进行数据验证: ③:格式化显示:需要调用PropertyEditor的getText进行格式化显示. 使用