SpringBoot是如何解析参数的
前言
前几天笔者在写Rest接口的时候,看到了一种传值方式是以前没有写过的,就萌生了一探究竟的想法。在此之前,有篇文章曾涉及到这个话题,但那篇文章着重于处理流程的分析,并未深入。
本文重点来看几种传参方式,看看它们都是如何被解析并应用到方法参数上的。
一、HTTP请求处理流程
不论在SpringBoot还是SpringMVC中,一个HTTP请求会被DispatcherServlet
类接收,它本质是一个Servlet
,因为它继承自HttpServlet
。在这里,Spring负责解析请求,匹配到Controller
类上的方法,解析参数并执行方法,最后处理返回值并渲染视图。
我们今天的重点在于解析参数,对应到上图的目标方法调用
这一步骤。既然说到参数解析,那么针对不同类型的参数,肯定有不同的解析器。Spring已经帮我们注册了一堆这东西。
它们有一个共同的接口HandlerMethodArgumentResolver
。supportsParameter
用来判断方法参数是否可以被当前解析器解析,如果可以就调用resolveArgument
去解析。
public interface HandlerMethodArgumentResolver {//判断方法参数是否可以被当前解析器解析boolean supportsParameter(MethodParameter var1);//解析参数@NullableObject resolveArgument(MethodParameter var1, @Nullable ModelAndViewContainer var2, NativeWebRequest var3, @Nullable WebDataBinderFactory var4)throws Exception;
}
复制代码
二、RequestParam
在Controller方法中,如果你的参数标注了RequestParam
注解,或者是一个简单数据类型。
@RequestMapping("/test1")
@ResponseBody
public String test1(String t1, @RequestParam(name = "t2",required = false) String t2,HttpServletRequest request){logger.info("参数:{},{}",t1,t2);return "Java";
}
复制代码
我们的请求路径是这样的:http://localhost:8080/test1?t1=Jack&t2=Java
如果按照以前的写法,我们直接根据参数名称或者RequestParam
注解的名称从Request对象中获取值就行。比如像这样:
String parameter = request.getParameter("t1");
在Spring中,这里对应的参数解析器是RequestParamMethodArgumentResolver
。与我们的想法差不多,就是拿到参数名称后,直接从Request中获取值。
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception {HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);//...省略部分代码...if (arg == null) {String[] paramValues = request.getParameterValues(name);if (paramValues != null) {arg = paramValues.length == 1 ? paramValues[0] : paramValues;}}return arg;
}
复制代码
三、RequestBody
如果我们需要前端传输更多的参数内容,那么通过一个POST请求,将参数放在Body中传输是更好的方式。当然,比较友好的数据格式当属JSON。
面对这样一个请求,我们在Controller方法中可以通过RequestBody
注解来接收它,并自动转换为合适的Java Bean对象。
@ResponseBody
@RequestMapping("/test2")
public String test2(@RequestBody SysUser user){logger.info("参数信息:{}",JSONObject.toJSONString(user));return "Hello";
}
复制代码
在没有Spring的情况下,我们考虑一下如何解决这一问题呢?
首先呢,还是要依靠Request对象。对于Body中的数据,我们可以通过request.getReader()
方法来获取,然后读取字符串,最后通过JSON工具类再转换为合适的Java对象。
比如像下面这样:
@RequestMapping("/test2")
@ResponseBody
public String test2(HttpServletRequest request) throws IOException {BufferedReader reader = request.getReader();StringBuilder builder = new StringBuilder();String line;while ((line = reader.readLine()) != null){builder.append(line);}logger.info("Body数据:{}",builder.toString());SysUser sysUser = JSONObject.parseObject(builder.toString(), SysUser.class);logger.info("转换后的Bean:{}",JSONObject.toJSONString(sysUser));return "Java";
}
复制代码
当然,在实际场景中,上面的SysUser.class需要动态获取参数类型。
在Spring中,RequestBody
注解的参数会由RequestResponseBodyMethodProcessor
类来负责解析。
它的解析由父类AbstractMessageConverterMethodArgumentResolver
负责。整个过程我们分为三个步骤来看。
1、获取请求辅助信息
在开始之前需要先获取请求的一些辅助信息,比如HTTP请求的数据格式,上下文Class信息、参数类型Class、HTTP请求方法类型等。
protected <T> Object readWithMessageConverters(){boolean noContentType = false;MediaType contentType;try {contentType = inputMessage.getHeaders().getContentType();} catch (InvalidMediaTypeException var16) {throw new HttpMediaTypeNotSupportedException(var16.getMessage());}if (contentType == null) {noContentType = true;contentType = MediaType.APPLICATION_OCTET_STREAM;}Class<?> contextClass = parameter.getContainingClass();Class<T> targetClass = targetType instanceof Class ? (Class)targetType : null;if (targetClass == null) {ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);targetClass = resolvableType.resolve();}HttpMethod httpMethod = inputMessage instanceof HttpRequest ?((HttpRequest)inputMessage).getMethod() : null; //.......
}
复制代码
2、确定消息转换器
上面获取到的辅助信息是有作用的,就是要确定一个消息转换器。消息转换器有很多,它们的共同接口是HttpMessageConverter
。在这里,Spring帮我们注册了很多转换器,所以需要循环它们,来确定使用哪一个来做消息转换。
如果是JSON数据格式的,会选择MappingJackson2HttpMessageConverter
来处理。它的构造函数正是指明了这一点。
public MappingJackson2HttpMessageConverter(ObjectMapper objectMapper) {super(objectMapper, new MediaType[]{MediaType.APPLICATION_JSON, new MediaType("application", "*+json")});
}
复制代码
3、解析
既然确定了消息转换器,那么剩下的事就很简单了。通过Request获取Body,然后调用转换器解析就好了。
protected <T> Object readWithMessageConverters(){if (message.hasBody()) {HttpInputMessage msgToUse = this.getAdvice().beforeBodyRead(message, parameter, targetType, converterType);body = genericConverter.read(targetType, contextClass, msgToUse);body = this.getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);}
}
复制代码
再往下就是Jackson包的内容了,不再深究。虽然写出来的过程比较啰嗦,但实际上主要就是为了寻找两个东西:
方法解析器RequestResponseBodyMethodProcessor
消息转换器MappingJackson2HttpMessageConverter
都找到之后调用方法解析即可。
四、GET请求参数转换Bean
还有一种写法是这样的,在Controller方法上用Java Bean接收。
@RequestMapping("/test3")
@ResponseBody
public String test3(SysUser user){logger.info("参数:{}",JSONObject.toJSONString(user));return "Java";
}
复制代码
然后用GET方法请求:
http://localhost:8080/test3?id=1001&name=Jack&password=1234&address=北京市海淀区
URL后面的参数名称对应Bean对象里面的属性名称,也可以自动转换。那么,这里它又是怎么做的呢 ?
笔者首先想到的就是Java的反射机制。从Request对象中获取参数名称,然后和目标类上的方法一一对应设置值进去。
比如像下面这样:
public String test3(SysUser user,HttpServletRequest request)throws Exception {//从Request中获取所有的参数key 和 valueMap<String, String[]> parameterMap = request.getParameterMap();Iterator<Map.Entry<String, String[]>> iterator = parameterMap.entrySet().iterator();//获取目标类的对象Object target = user.getClass().newInstance();Field[] fields = target.getClass().getDeclaredFields();while (iterator.hasNext()){Map.Entry<String, String[]> next = iterator.next();String key = next.getKey();String value = next.getValue()[0];for (Field field:fields){String name = field.getName();if (key.equals(name)){field.setAccessible(true);field.set(target,value);break;}}}logger.info("userInfo:{}",JSONObject.toJSONString(target));return "Python";
}
复制代码
除了反射,Java还有一种内省机制可以完成这件事。我们可以获取目标类的属性描述符对象,然后拿到它的Method对象, 通过invoke来设置。
private void setProperty(Object target,String key,String value) {try {PropertyDescriptor propDesc = new PropertyDescriptor(key, target.getClass());Method method = propDesc.getWriteMethod();method.invoke(target, value);} catch (Exception e) {e.printStackTrace();}
}复制代码
然后在上面的循环中,我们就可以调用这个方法来实现。
while (iterator.hasNext()){Map.Entry<String, String[]> next = iterator.next();String key = next.getKey();String value = next.getValue()[0];setProperty(userInfo,key,value);
}
复制代码
为什么要说到内省机制呢?因为Spring在处理这件事的时候,最终也是靠它处理的。
简单来说,它是通过BeanWrapperImpl
来处理的。关于BeanWrapperImpl
有个很简单的使用方法:
SysUser user = new SysUser();
BeanWrapper wrapper = new BeanWrapperImpl(user.getClass());wrapper.setPropertyValue("id","20001");
wrapper.setPropertyValue("name","Jack");Object instance = wrapper.getWrappedInstance();
System.out.println(instance);
复制代码
wrapper.setPropertyValue
最后就会调用到BeanWrapperImpl#BeanPropertyHandler.setValue()
方法。
它的setValue
方法和我们上面的setProperty
方法大致相同。
private class BeanPropertyHandler extends PropertyHandler {//属性描述符private final PropertyDescriptor pd;public void setValue(@Nullable Object value) throws Exception {//获取set方法Method writeMethod = this.pd.getWriteMethod();ReflectionUtils.makeAccessible(writeMethod);//设置writeMethod.invoke(BeanWrapperImpl.this.getWrappedInstance(), value);}
}
复制代码
通过上面的方式,就完成了GET请求参数到Java Bean对象的自动转换。
回过头来,我们再看Spring。虽然我们上面写的很简单,但真正用起来还需要考虑的很多很多。Spring中处理这种参数的解析器是ServletModelAttributeMethodProcessor
。
它的解析过程在其父类ModelAttributeMethodProcessor.resolveArgument()
方法。整个过程,我们也可以分为三个步骤来看。
1、获取目标类的构造函数
根据参数类型,先生成一个目标类的构造函数,以供后面绑定数据的时候使用。
2、创建数据绑定器WebDataBinder
WebDataBinder
继承自DataBinder
。而DataBinder
主要的作用,简言之就是利用BeanWrapper
给对象的属性设值。
3、绑定数据到目标类,并返回
在这里,又把WebDataBinder
转换成ServletRequestDataBinder
对象,然后调用它的bind方法。
接下来有个很重要的步骤是,将request中的参数转换为MutablePropertyValues pvs
对象。
然后接下来就是循环pvs,调用setPropertyValue
设置属性。当然了,最后调用的其实就是BeanWrapperImpl#BeanPropertyHandler.setValue()
。
下面有段代码可以更好的理解这一过程,效果是一样的:
//模拟Request参数
Map<String,Object> map = new HashMap();
map.put("id","1001");
map.put("name","Jack");
map.put("password","123456");
map.put("address","北京市海淀区");//将request对象转换为MutablePropertyValues对象
MutablePropertyValues propertyValues = new MutablePropertyValues(map);
SysUser sysUser = new SysUser();
//创建数据绑定器
ServletRequestDataBinder binder = new ServletRequestDataBinder(sysUser);
//bind数据
binder.bind(propertyValues);
System.out.println(JSONObject.toJSONString(sysUser));
复制代码
五、自定义参数解析器
我们说所有的消息解析器都实现了HandlerMethodArgumentResolver
接口。我们也可以定义一个参数解析器,让它实现这个接口就好了。
首先,我们可以定义一个RequestXuner
注解。
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestXuner {String name() default "";boolean required() default false;String defaultValue() default "default";
}
复制代码
然后是实现了HandlerMethodArgumentResolver
接口的解析器类。
public class XunerArgumentResolver implements HandlerMethodArgumentResolver {@Overridepublic boolean supportsParameter(MethodParameter parameter) {return parameter.hasParameterAnnotation(RequestXuner.class);}@Overridepublic Object resolveArgument(MethodParameter methodParameter,ModelAndViewContainer modelAndViewContainer,NativeWebRequest nativeWebRequest,WebDataBinderFactory webDataBinderFactory){//获取参数上的注解RequestXuner annotation = methodParameter.getParameterAnnotation(RequestXuner.class);String name = annotation.name();//从Request中获取参数值String parameter = nativeWebRequest.getParameter(name);return "HaHa,"+parameter;}
}
复制代码
不要忘记需要配置一下。
@Configuration
public class WebMvcConfiguration extends WebMvcConfigurationSupport {@Overrideprotected void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {resolvers.add(new XunerArgumentResolver());}
}
复制代码
一顿操作后,在Controller中我们可以这样使用它:
@ResponseBody
@RequestMapping("/test4")
public String test4(@RequestXuner(name="xuner") String xuner){logger.info("参数:{}",xuner);return "Test4";
}
复制代码
六、总结
本文内容通过相关示例代码展示了Spring中部分解析器解析参数的过程。说到底,无论参数如何变化,参数类型再怎么复杂。
它们都是通过HTTP请求发送过来的,那么就可以通过HttpServletRequest
来获取到一切。Spring做的就是通过注解,尽量适配大部分应用场景。
转载于:https://juejin.im/post/5cd182c6f265da034d2a2b45
SpringBoot是如何解析参数的相关推荐
- SpringBoot 自动解析参数:HandlerMethodArgumentResolver
HandlerMethodArgumentResolver 是SpringBoot 中的一个接口,也是Spring 面向切面的编程的一种提现,这个接口我们从名称上大概能了解其中的意思:操作方法参数解析 ...
- SpringBoot是如何解析HTTP参数的?
文章转载自公众号 清幽之地的博客 , 作者 清幽之地 前言 前几天笔者在写Rest接口的时候,看到了一种传值方式是以前没有写过的,就萌生了一探究竟的想法.在此之前,有篇文章曾涉及到这个话题,但那篇文 ...
- springboot MVC视图解析流程源码分析
Servlet的基础知识 为什么要先了解Servlet的知识呢,因为后面你会看到我们所熟悉的SpringMVC其实也是一个Servlet,只是它封装了很多的东西并和Spring进行了整合,后面我们进行 ...
- SpringBoot各类型参数解析原理(源码)
上次那篇我们只分析了doDispatch中的getHandler方法(获取执行链,执行链里包括当前请求URL对应的 handler 以及拦截器(Controller.method绑定关系)),今儿继续 ...
- SpringBoot之接收url参数
url参数如何添加呢? 我们先看测试工具postman postman设置参数,在这里设置,但是具体是什么样子呢? 我们再使用抓包工具wireshark 通过上述观察,我们发现 URL 参数是追加到 ...
- 【Springboot+mybatis】 解析Excel并批量导入到数据库
[Springboot+mybatis] 解析Excel并批量导入到数据库 置顶 2018年01月16日 20:05:52 冉野丶 阅读数:4060 标签: excel导入数据库 文件上传 excel ...
- SpringBoot源码解析
SpringBoot源码解析 1.启动的过程: 首先在main函数中启动当前应用程序(SpringApplication启动) 创建SpringApplication对象(new SpringAppl ...
- SpringBoot接收Xml格式参数并转换为POJO对象
SpringBoot接收Xml格式参数并转换为POJO对象 1. 所需JavaBean @XmlAccessorType(XmlAccessType.FIELD) @XmlType(propOrder ...
- SpringBoot @Validated原理解析
文章目录 一.开发使用`@Validated` 出现问题 1.1 代码 1.2 请求 1.3 响应 二.源码: RequestResponseBodyMethodProcessor.resolveAr ...
最新文章
- CryEngine最新版发布,支持Vulkan API
- 基于windows平台搭建elasticsearch 补充
- C语言实现前部插入创建链表以及尾部插入链表
- webcomponents安装了没有用_Web Components 入门实例教程
- 删除sql下注册服务器
- Java通过反射机制修改类中的私有属性的值
- Java实验9 T3.对二进制数据文件中的所有数据求和
- 开了个会:破局企业云通信,华为加速 Buff 开发者!
- cesium cesium is not defined
- APACHE ACTIVEMQ安装
- python-gui-pyqt5的使用方法-6--lambda传递参数的方法:
- 如何在手机浏览器中实现条形码/QR码扫描
- jQuery表格新增行
- 计算机组成原理浮点运算方法,计算机组成原理第二章 第11讲 浮点运算方法和浮点运算器.ppt...
- 带着问题看 react-redux 源码实现
- openstack neutron相关命令出现异常HttpException: 503
- JavaScript计算圆周率(解析几何+定积分)
- 好用的电视盒子软件推荐:无广告看电视我选这两款
- 网站(B/s)架构发展探索、分析
- kwgt 歌词_eight for kwgt
热门文章
- python画皇冠_用Python画小女孩放风筝的示例
- dockerclient 查看端口占用_docker 端口被占用问题解决
- tps协议和onvif协议_做监控的你,应该了解的ONVIF协议!
- 报错注入_sqli-labs less5 Double Query- Single Quotes- String
- python开发小型数据库_Python开发【第十七篇】:MySQL(一)
- python交互式命令_从python内部运行交互式命令
- windows下多tomcat部署
- Third week-homework(员工管理系统)
- Android 7.0 Nougat介绍
- 初识Python(1)__Python基础