⾃定义spring参数注解-打破@RequestBody单体限制本⽂主要描述怎样⾃定义类似@RequestBody这样的参数注解来打破@RequestBody的单体限制。
⽬录
1 @RequestBody的单体限制
2 ⾃定义spring的参数注解
3 编写spring的参数注解解析器
4 将⾃定义参数注解解析器设置到spring的参数解析器集合中
5 指定参数解析器的优先级
⼀、@RequestBody的单体限制
@RequestBody的作⽤:将请求体中的整体数据转化为对象。
1    @RequestMapping(value = "/body", method = RequestMethod.POST)
2public Book testCommon(@RequestBody Book book) {
3return book;
4    }
springmvc的注解有哪些
springmvc具有⼀个参数解析器容器RequestMappingHandlerAdapter.argumentResolvers,该参数的初始化在RequestMappingHandlerAdapter#afterPropertiesSet()
1public void afterPropertiesSet() {
2        ......
3if (this.argumentResolvers == null) {
4            List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
5this.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*/
14private 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(), questResponseBodyAdvice));
26        resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), questResponseBodyAdvice));
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(), questResponseBodyAdvice));
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
46if (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
54return resolvers;
55    }
可以看出springmvc的参数解析器容器中存放着内置的参数解析器 + ⾃定义解析器,这⾥边就包括@RequestBody的解析器RequestResponseBodyMethodProcessor,来看⼀下这个解析器的主要⽅法:
1    @Override
2public boolean supportsParameter(MethodParameter parameter) {
3return parameter.hasParameterAnnotation(RequestBody.class);
4    }
5
6    @Override
7public 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)
2public 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());
6return book;
7    }
有两个@RequestBody,⼀执⾏,结果抛错:
1 {
2  "status": 400,
3  "error": "Bad Request",
4  "exception": "org.verter.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)
3public @interface RequestModel {
4    String value() default "";
5boolean required() default false;
6 }
⾃定义注解很简单:@Target指明注解应⽤于参数上;@Retention指明注解应⽤于运⾏时。
三、编写spring的参数注解解析器
1public class RequestModelArgumentResolver implements HandlerMethodArgumentResolver {
2    @Override
3public boolean supportsParameter(MethodParameter parameter) {
4return parameter.hasParameterAnnotation(RequestModel.class);
5    }
6
7    @Override
8public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
9                                  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
10final String parameterJson = ParameterName());
11
12//GenericParameterType() 返回参数的完整类型(带泛型)
13final Type type = GenericParameterType();
14final Object o = JSON.parseObject(parameterJson, type);
15return o;
16    }
17 }
注意:
1 supportsParameter⽅法指明RequestModelArgumentResolver只处理带有@RequestModel注解的参数;
2 resolveArgument⽅法对⼊参进⾏解析:⾸先获取参数值(json串),然后获取参数的完整类型(带泛型),最后使⽤fastjson解析器将json格式的参数值转化为具体类型的对象。
四、将⾃定义参数解析器设置到spring的参数解析器集合中
1 @Configuration
2public class WebConfig extends WebMvcConfigurerAdapter {
3    @Override
4public 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)
2public Book testModel(@RequestModel(value = "book1") Book book1, @RequestModel(value = "book2") Book book2) {
3        Book book = new Book();
4        book.Id());
5        book.Name());
6return book;
7    }
前端调⽤:(有错误跳过)
1 const params = new URLSearchParams()
2 params.append('book1', '{"id": 1,"name": "11"}')
3 params.append('book2', '{"id": 2,"name": "22"}')
4return axios.post('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)
2public Map<String, Book> testMap(@RequestModel(value = "title2Book") Map<String, Book> title2Book) {
3return title2Book;
4    }
我们在“三”中的RequestModelArgumentResolver#supportsParameter⽅法中打断点来debug⼀下,发现上边这个例⼦根本不会⾛进去,也就是说此时我们⾃定义的RequestModelArgumentResolver不再起作⽤了。
原因:在springmvc的解析器容器中,⾃定义解析器是放在内置解析器之后的,这个顺序也是解析器的优先级,也就是说假设有⼀个参数同时满⾜两个解析器,只有第⼀个解析器会⽣效。⽽springmvc对M
ap是专门有⼀个内置解析器的,这个解析器位于我们的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*/
5private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
6        List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();
7        ...
8//Map解析器
9        resolvers.add(new MapMethodProcessor());
10        ...
11// ⾃定义解析器
12if (getCustomArgumentResolvers() != null) {
13            resolvers.addAll(getCustomArgumentResolvers());
14        }
15return resolvers;
16    }
看⼀下MapMethodProcessor#supportsParameter
1    @Override
2public boolean supportsParameter(MethodParameter parameter) {
3return Map.class.ParameterType());
4    }
原因明了了以后,就要去想解决⽅案。(如果spring可以提供为参数解析器设置order的能⼒,那么就好了,但是spring没有提供)
第⼀种⽅案
在服务启动时,动态替换掉MapMethodProcessor#supportsParameter的字节码。
1    @Override
2public boolean supportsParameter(MethodParameter parameter) {
3if(parameter.hasParameterAnnotation(RequestModel.class)){
4return false;
5                }
6return Map.class.ParameterType());
7    }
使⽤javassist可以实现这⼀点,但是这样去做,代码复杂性较⾼。“任何⼀个功能的实现,都要想办法降低代码复杂性”
第⼆种⽅案
⾸先删除"四"中的WebConfig,让spring不再⾃动的将⾃定义解析器加到RequestMappingHandlerAdapter的解析器容器中;然后我们通过下⾯的⽅式⼿动的将RequestModelArgumentResolver加载到RequestMappingHandlerAdapter的解析容器中。(通过这样的⽅式,我们可以任意的指定解析器的顺序)
1 @Configuration
2public class MethodArgumentResolver {
3    @Autowired
4private RequestMappingHandlerAdapter adapter;
5
6    @PostConstruct
7public void injectSelfMethodArgumentResolver() {
8        List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();
9        argumentResolvers.add(new RequestModelArgumentResolver());
10        argumentResolvers.ArgumentResolvers());
11        adapter.setArgumentResolvers(argumentResolvers);
12    }
13 }

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。