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

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

目录

1 @RequestBody的单体限制

2 自定义spring的参数注解

3 编写spring的参数注解解析器

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

5 指定参数解析器的优先级

一、@RequestBody的单体限制

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

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

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

1 public voidafterPropertiesSet() {2 ......3 if (this.argumentResolvers == null) {4 List resolvers =getDefaultArgumentResolvers();5 this.argumentResolvers = newHandlerMethodArgumentResolverComposite().addResolvers(resolvers);6 }7 ......8 }9

10 /**

11 * Return the list of argument resolvers to use including built-in resolvers12 * and custom resolvers provided via {@link#setCustomArgumentResolvers}.13 */

14 private ListgetDefaultArgumentResolvers() {15 List resolvers = new ArrayList();16

17 //Annotation-based argument resolution

18 resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));19 resolvers.add(newRequestParamMapMethodArgumentResolver());20 resolvers.add(newPathVariableMethodArgumentResolver());21 resolvers.add(newPathVariableMapMethodArgumentResolver());22 resolvers.add(newMatrixVariableMethodArgumentResolver());23 resolvers.add(newMatrixVariableMapMethodArgumentResolver());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(newRequestHeaderMethodArgumentResolver(getBeanFactory()));28 resolvers.add(newRequestHeaderMapMethodArgumentResolver());29 resolvers.add(newServletCookieValueMethodArgumentResolver(getBeanFactory()));30 resolvers.add(newExpressionValueMethodArgumentResolver(getBeanFactory()));31 resolvers.add(newSessionAttributeMethodArgumentResolver());32 resolvers.add(newRequestAttributeMethodArgumentResolver());33

34 //Type-based argument resolution

35 resolvers.add(newServletRequestMethodArgumentResolver());36 resolvers.add(newServletResponseMethodArgumentResolver());37 resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));38 resolvers.add(newRedirectAttributesMethodArgumentResolver());39 resolvers.add(newModelMethodProcessor());40 resolvers.add(newMapMethodProcessor());41 resolvers.add(newErrorsMethodArgumentResolver());42 resolvers.add(newSessionStatusMethodArgumentResolver());43 resolvers.add(newUriComponentsBuilderMethodArgumentResolver());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 returnresolvers;55 }

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

1 @Override2 public booleansupportsParameter(MethodParameter parameter) {3 return parameter.hasParameterAnnotation(RequestBody.class);4 }5

6 @Override7 publicObject resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,8 NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throwsException {9 //这里使用MappingJackson2HttpMessageConverter将输入流body体中的转化为Book对象

10 }

这里注意两点:

1、一个参数解析器最重要的方法有两个:

(1)supportsParameter 指定哪些参数使用该解析器进行解析

(2)resolveArgument 对参数进行真正的解析操作

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

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

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

1 @RequestMapping(value = "/two-body", method =RequestMethod.POST)2 publicBook testCommon(@RequestBody Book book1, @RequestBody Book book2) {3 Book book = newBook();4 book.setId(Optional.ofNullable(book1).orElse(book2).getId());5 book.setName(Optional.ofNullable(book1).orElse(book2).getName());6 returnbook;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 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 @interfaceRequestModel {4 String value() default "";5 boolean required() default false;6 }

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

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

1 public class RequestModelArgumentResolver implementsHandlerMethodArgumentResolver {2 @Override3 public booleansupportsParameter(MethodParameter parameter) {4 return parameter.hasParameterAnnotation(RequestModel.class);5 }6

7 @Override8 publicObject resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,9 NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throwsException {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 returno;16 }17 }

注意:

1 supportsParameter方法指明RequestModelArgumentResolver只处理带有@RequestModel注解的参数;

2 resolveArgument方法对入参进行解析:首先获取参数值(json串),然后获取参数的完整类型(带泛型),最后使用fastjson解析器将json格式的参数值转化为具体类型的对象。

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

1 @Configuration2 public class WebConfig extendsWebMvcConfigurerAdapter {3 @Override4 public void addArgumentResolvers(ListargumentResolvers) {5 argumentResolvers.add(newRequestModelArgumentResolver());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 = newBook();4 book.setId(book1.getId());5 book.setName(book2.getName());6 returnbook;7 }

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

1 const params = newURLSearchParams()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 testMap(@RequestModel(value = "title2Book") Maptitle2Book) {3 returntitle2Book;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 resolvers3 * and custom resolvers provided via {@link#setCustomArgumentResolvers}.4 */

5 private ListgetDefaultArgumentResolvers() {6 List resolvers = new ArrayList();7 ...8 //Map解析器

9 resolvers.add(newMapMethodProcessor());10 ...11 //自定义解析器

12 if (getCustomArgumentResolvers() != null) {13 resolvers.addAll(getCustomArgumentResolvers());14 }15 returnresolvers;16 }

看一下MapMethodProcessor#supportsParameter

1 @Override2 public booleansupportsParameter(MethodParameter parameter) {3 return Map.class.isAssignableFrom(parameter.getParameterType());4 }

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

第一种方案

在服务启动时,动态替换掉MapMethodProcessor#supportsParameter的字节码。

1 @Override2 public booleansupportsParameter(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 @Configuration2 public classMethodArgumentResolver {3 @Autowired4 privateRequestMappingHandlerAdapter adapter;5

6 @PostConstruct7 public voidinjectSelfMethodArgumentResolver() {8 List argumentResolvers = new ArrayList<>();9 argumentResolvers.add(newRequestModelArgumentResolver());10 argumentResolvers.addAll(adapter.getArgumentResolvers());11 adapter.setArgumentResolvers(argumentResolvers);12 }13 }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值