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

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

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

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

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

     public void afterPropertiesSet() {
......
if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
......
} /**
* Return the list of argument resolvers to use including built-in resolvers
* and custom resolvers provided via {@link #setCustomArgumentResolvers}.
*/
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>(); // Annotation-based argument resolution
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver());
resolvers.add(new PathVariableMapMethodArgumentResolver());
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
resolvers.add(new ServletModelAttributeMethodProcessor(false));
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new SessionAttributeMethodArgumentResolver());
resolvers.add(new RequestAttributeMethodArgumentResolver()); // Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RedirectAttributesMethodArgumentResolver());
resolvers.add(new ModelMethodProcessor());
resolvers.add(new MapMethodProcessor());
resolvers.add(new ErrorsMethodArgumentResolver());
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;
}

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

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

这里注意两点:

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

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

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

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

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

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

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

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

 @Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestModel {
String value() default "";
boolean required() default false;
}

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

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

 public class RequestModelArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestModel.class);
} @Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
final String parameterJson = webRequest.getParameter(parameter.getParameterName()); //parameter.getGenericParameterType() 返回参数的完整类型(带泛型)
final Type type = parameter.getGenericParameterType();
final Object o = JSON.parseObject(parameterJson, type);
return o;
}
}

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

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

 @Configuration
public class WebConfig extends WebMvcConfigurerAdapter {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new RequestModelArgumentResolver());
}
}

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

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

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

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

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

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

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

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

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

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

     /**
* Return the list of argument resolvers to use including built-in resolvers
* and custom resolvers provided via {@link #setCustomArgumentResolvers}.
*/
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<HandlerMethodArgumentResolver>();
...
//Map解析器
resolvers.add(new MapMethodProcessor());
...
// 自定义解析器
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}
return resolvers;
}

看一下MapMethodProcessor#supportsParameter

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

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

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

     @Override
public boolean supportsParameter(MethodParameter parameter) {
if(parameter.hasParameterAnnotation(RequestModel.class)){
return false;
}
return Map.class.isAssignableFrom(parameter.getParameterType());
}

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

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

 @Configuration
public class MethodArgumentResolver {
@Autowired
private RequestMappingHandlerAdapter adapter; @PostConstruct
public void injectSelfMethodArgumentResolver() {
List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();
argumentResolvers.add(new RequestModelArgumentResolver());
argumentResolvers.addAll(adapter.getArgumentResolvers());
adapter.setArgumentResolvers(argumentResolvers);
}
}

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

  1. spring mvc注解之@RequestBody和@RequestParm

    @RequestBody http://localhost:8080/user/login { "username":"jack", "passwar ...

  2. Spring boot中自定义Json参数解析器

    转载请注明出处... 一.介绍 用过springMVC/spring boot的都清楚,在controller层接受参数,常用的都是两种接受方式,如下 /** * 请求路径 http://127.0. ...

  3. 使用spring validation完成数据后端校验-自定义校验的注解-判断是否为空

    引入依赖 我们使用maven构建springboot应用来进行demo演示. <dependencies> <dependency> <groupId>org.sp ...

  4. 学习Spring Boot:(十一) 自定义装配参数

    前言 SpringMVC 中 Controller 中方法的参数非常灵活,得益于它的强大自动装配,这次将根据上次遗留下的问题,将研究下装配参数. 正文 SpringMVC中使用了两个接口来处理参数: ...

  5. spring:自定义限定符注解@interface, 首选bean

    spring:自定义限定符注解@interface, 首选bean 1.首选bean 在声明bean的时候,通过将其中一个可选的bean设置为首选(primary)bean能够避免自动装配时的歧义性. ...

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

    使用自定义参数注解获取 token 中User数据 使用背景 在springboot项目开发中需要从token中获取用户信息时通常的方式要经历几个步骤 拦截器中截获token TokenUtil工具类 ...

  7. 这一次搞懂Spring自定义标签以及注解解析原理

    前言 在上一篇文章中分析了Spring是如何解析默认标签的,并封装为BeanDefinition注册到缓存中,这一篇就来看看对于像context这种自定义标签是如何解析的.同时我们常用的注解如:@Se ...

  8. 自定义Spring注解bean的命名策略

    由于项目的需要spring的业务相关的bean不是写在xml文件中,因为项目是一个模块一个模块提交的,提交的时候不想修改xml文件,因此就用到了spring的注解Service. 例如: Java代码 ...

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

    6.1.注解式控制器简介 一.Spring2.5之前,我们都是通过实现Controller接口或其实现来定义我们的处理器类.已经@Deprecated.   二.Spring2.5引入注解式处理器支持 ...

随机推荐

  1. JMeter中BeanShell Sampler调试分享

    BeanShell脚本 String s = "s"; String y = "y"; boolean result = s.equals(y); vars.p ...

  2. vsftp 基于虚拟用户的ftp服务器 如何做配额

    做配额的方法: 1,是用磁盘配额,但是虚拟用户好像没有好办法.只能应用于本地用户.与Vsftpd设置无关 2,文件夹限制大小,是占用的.这和Vsftpd没有关系 所以可以先把用户禁锢在自己工作目录里面 ...

  3. Spring AOP中args()、arg-names、argNames

    先小结一下: args()是用来匹配并且接收目标方法的参数的. argNames(用在注解中)与arg-names(用在XML中),他们是同一个东西. argNames用来接收AspectJ表达式中的 ...

  4. Python题目练习(二)

    1.如何实现对python列表去重,并保持原来顺序 li = [1,2,5,3,1,6,3,8,0,3,2,4] l = [] for i in li: if i not in l: l.append ...

  5. VMware下centos7安装

    VMware下centos7安装 转载地址:https://blog.csdn.net/hui_2016/article/details/68927487 一. 软件准备 二. Vmware12安装 ...

  6. Python编程基础[条件语句if 循环语句 for,while](二)

    ython条件语句是通过一条或多条语句的执行结果(True或者False)来决定执行的代码块. 可以通过下图来简单了解条件语句的执行过程: if 判断条件: 执行语句……else: 执行语句…… x= ...

  7. Python json 读取 json 文件并转为 dict

    Python json 读取 json 文件并转为 dict 在 D 盘 新建 test.json: { "test": "测试\n换行", "dic ...

  8. [ 中危 ] 发布处存在CSRF及CSRF设想

    漏洞存在于菜品发布处,使用A账号在添加/发布菜品的时候拦截数据包,使用burpsuite构造 CSRF的POC,再用B账号打开该HTML POC ,生成菜品. 该CSRF400RMB,主要因为是核心业 ...

  9. 超出JavaScript安全整数限制的数字计算-BigInt

    JavaScript中的基本数据类Number是双精度浮点数,它可以表示的最大安全范围是正负9007199254740991,也就是2的53次方减一,在浏览器控制台分别输入Number.MAX_SAF ...

  10. C# 中BindingSource 的用法

    .引言 BindingSource组件是数据源和控件间的一座桥,同时提供了大量的API和Event供我们使用.使用这些API我们可以将Code与各种具体类型数据源进行解耦:使用这些Event我们可以洞 ...