前言

前几天笔者在写Rest接口的时候,看到了一种传值方式是以前没有写过的,就萌生了一探究竟的想法。在此之前,有篇文章曾涉及到这个话题,但那篇文章着重于处理流程的分析,并未深入。

本文重点来看几种传参方式,看看它们都是如何被解析并应用到方法参数上的。

一、HTTP请求处理流程

不论在SpringBoot还是SpringMVC中,一个HTTP请求会被DispatcherServlet类接收,它本质是一个Servlet,因为它继承自HttpServlet。在这里,Spring负责解析请求,匹配到Controller类上的方法,解析参数并执行方法,最后处理返回值并渲染视图。

image

我们今天的重点在于解析参数,对应到上图的目标方法调用这一步骤。既然说到参数解析,那么针对不同类型的参数,肯定有不同的解析器。Spring已经帮我们注册了一堆这东西。

它们有一个共同的接口HandlerMethodArgumentResolver。supportsParameter用来判断方法参数是否可以被当前解析器解析,如果可以就调用resolveArgument去解析。

public interface HandlerMethodArgumentResolver {

//判断方法参数是否可以被当前解析器解析

boolean supportsParameter(MethodParameter var1);

//解析参数

@Nullable

Object 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。

image

面对这样一个请求,我们在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 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 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 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 和 value

Map parameterMap = request.getParameterMap();

Iterator> iterator = parameterMap.entrySet().iterator();

//获取目标类的对象

Object target = user.getClass().newInstance();

Field[] fields = target.getClass().getDeclaredFields();

while (iterator.hasNext()){

Map.Entry 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 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 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 {

@Override

public boolean supportsParameter(MethodParameter parameter) {

return parameter.hasParameterAnnotation(RequestXuner.class);

}

@Override

public 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 {

@Override

protected void addArgumentResolvers(List 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做的就是通过注解,尽量适配大部分应用场景。

springboot处理参数再转发请求_SpringBoot是如何解析HTTP参数的相关推荐

  1. springboot处理参数再转发请求_SpringBoot图文教程6—SpringBoot中过滤器的使用

    有天上飞的概念,就要有落地的实现 概念十遍不如代码一遍,朋友,希望你把文中所有的代码案例都敲一遍 先赞后看,养成习惯 SpringBoot 图文系列教程技术大纲 SpringBoot 图文教程系列文章 ...

  2. springboot处理参数再转发请求_Springboot 2.0---WebFlux请求处理流程

    笔记是学习了小马哥在慕课网的课程的<Spring Boot 2.0深度实践之核心技术篇>的内容结合自己的需要和理解做的笔记. 前言 在了解了WebFlux核心组件之后,我们就该了解相应的请 ...

  3. bean validation校验方法参数_Spring Validation最佳实践及其实现原理,参数校验没那么简单!

    本文同名博客老炮说Java:https://www.laopaojava.com/,每天更新Spring/SpringMvc/SpringBoot/实战项目等文章资料 顺便再给大家推荐一套Spring ...

  4. [源码解析] 机器学习参数服务器 Paracel (1)-----总体架构

    [源码解析] 机器学习参数服务器 Paracel (1)-----总体架构 文章目录 [源码解析] 机器学习参数服务器 Paracel (1)-----总体架构 0x00 摘要 0x01使用 1.1 ...

  5. url 通配符解析成参数

    需求:url 参数是通配符,需要把通配符解析成参数并且拼接到 url 中 例如:https://xxx.cn/index.html$a=1$b=2;  解析成 https://xxx.cn/index ...

  6. android webview参数,Android webView解析URL参数

    2015年6月18日 13:56:21 星期四 又当爹又当娘啊............ public void onPageFinished(WebView view, String url) { s ...

  7. ServletRequest HttpServletRequest 请求方法 获取请求参数 请求转发 请求包含 请求转发与重定向区别 获取请求头字段...

    原文地址:ServletRequest HttpServletRequest 请求方法 获取请求参数 请求转发 请求包含 请求转发与重定向区别 获取请求头字段 ServletRequest 基本概念 ...

  8. springboot post 请求参数过长请求接口报错 An HTTP line is larger than 4096 bytes

    springboot项目post请求参数过长请求接口报错如下: 解决办法将post请求参数值调大: server:tomcat:max-http-form-post-size: 5MB

  9. php 转发请求及参数,php – Symfony 2转发请求传递GET / POST参数

    我没有看到任何理由通过内核转发请求.您可以按照建议的方式将验证服务中封装此逻辑的路由,或者您可以创建一个在路由器侦听器之后运行的kernel.request侦听器,并且仅在满足条件的情况下才应用_co ...

最新文章

  1. 这41条科研经验,让你少走很多弯路!
  2. STM32 基础系列教程 23 - USB_cdc
  3. Fedora 17 meld 显示行号以及语法高亮
  4. .NET六大剑客:栈、堆、值类型、引用类型、装箱和拆箱
  5. MyBatis的删除数据操作
  6. LNMP一键安装包在安装完成后的一些安全设置
  7. 微软 CEO 萨提亚·纳德拉:不要重复造轮子,提升技术强密度
  8. linux交叉编译jpeg,libjpeg的交叉编译以及jpeg图片的缩放(缩略图)
  9. ACM竞赛数论知识积累
  10. ART加载OAT文件的过程分析
  11. 校赛题解(部分)+反思
  12. 兆比特每秒和兆字节每秒_bit ( 比特 )和 Byte(字节)的关系 以及 网速怎么算
  13. NYOJ54-小明的存钱计划
  14. AARRR模型:用户激活指标+方法,很实用!
  15. “工业互联网+安全生产”,提升工业企业安全水平
  16. primeng使用步骤
  17. wGlasses AR智能眼镜正式发布!影育科技带来端云协同、软硬件及资源零代码、一体化的AR生态科技盛宴!
  18. ArcFaceSDK3.0 Python Demo
  19. 洛谷 P1456 Monkey King 题解
  20. bytearray函数

热门文章

  1. 国内互联网广告生态现状【计算广告】
  2. isinstance函数
  3. 解读阿里云oss-android/ios-sdk 断点续传(多线程)
  4. Chrome 隐藏 SSL 证书信息 禁止禁用 DRM
  5. Spring源码深度解析第2天
  6. 用于matplotlib对齐很有用的算法,可用于面试笔试
  7. best introduction to camera calibration
  8. 今天又听了蓝色的缘分
  9. 十分钟玩转 jQuery、实例大全
  10. 通用权限实现的核心设计思想