SpringMVC-请求参数绑定原理
- 控制层框架历史:
Servlet->Structs1.0->Structs2.0->SpringMVC->SpringBoot(嵌入式)
- 其实控制层框架包含两个层面的绑定:路由地址的绑定(涉及控制器及具体路由方法绑定)、请求参数的绑定,本文讲的是后者 后续有时间会同步前者
-
1. 你知道的参数绑定注解有哪些?
所属jar:org.springframework:spring-web:5.2.12.RELEASE
org.springframework:spring-web:5.2.12.RELEASE
所属package:package org.springframework.web.bind.annotation
package org.springframework.web.bind.annotation
标注有@Target(ElementType.PARAMETER)注解的均属于参数绑定注解如下:
@CookieValue(绑定客户端的cookie属性)
@MatrixVariable(是对@PathVariable的增强)
@ModelAttribute
@PathVariable(路径参数绑定)
@RequestAttribute(服务端自身参数绑定)
@RequestBody(请求体json或者xml参数绑定)
@RequestHeader(请求头参数绑定)
@RequestParam(param参数绑定)
@RequestPart(文件上传参数绑定)
@SessionAttribute(session属性绑定)
@SessionAttributes(session属性绑定) 作用域类上
-
2. 简单介绍一下上面注解的使用方法 参考:ParamBindController.java
涉及的相关代码如下:
public class RequestAttributeInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
request.setAttribute("username", "小喇叭");
//设置session属性
HttpSession session = request.getSession();
session.setAttribute("age", 100);
return true;
}
}
/**
* Spring 3.2就已经支持@MatrixVariable特性,但直至现在其依然为默认禁用的状态
*
* @author lvsheng
* @version 1.0.0
* @date 2022/03/08 13:14
* @see WebMvcConfigurer
*/
@Configuration
public class MatrixVariableConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
WebMvcConfigurer.super.addInterceptors(registry);
registry.addInterceptor(new RequestAttributeInterceptor()).addPathPatterns("/**/request-attribute", "/**/session-attribute");
}
}
package com.oceanus.bind.sharing;
import cn.hutool.core.util.StrUtil;
import java.util.List;
import java.util.Map;
import javax.annotation.Resource;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.MatrixVariable;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.SessionAttribute;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.multipart.MultipartFile;
@Slf4j
@Controller
@SessionAttributes(types = {UserDto.class}, value = "userDto")
@RequestMapping("/param")
public class ParamBindController {
/**
* 响应
*/
@Resource
private HttpServletResponse response;
/**
* 请求
*/
@Resource
private HttpServletRequest request;
// /**
// * 预请求:
// *
// * 被@ModelAttribute注释的方法会在这个控制器每个方法执行前被执行
// *
// * @author lvsheng
// * @date 2022/03/08 15:54
// */
// @ModelAttribute
// public void preRequest() {
// preRequestForRequestAttribute();
// preRequestForSessionAttribute();
// }
/***
* 预请求-cookie值
*
* @author lvsheng
* @date 2022/03/08 10:56
*/
@ResponseBody
@GetMapping("/pre-request/cookie-value")
public void preRequestForCookieValue() {
Cookie cookie = new Cookie("username", "lvsheng");
response.addCookie(cookie);
}
/**
* 预请求-request-attribute请求转发
*
* @return {@link String }
* @author lvsheng
* @date 2022/03/08 14:32
*/
@GetMapping("/pre-request/request-attribute")
public String preRequestForRequestAttribute() {
//设置服务端系统属性参数
request.setAttribute("username", "小麻花");
return "forward:/param/request-attribute";
}
/**
* 预请求-session-attribute请求转发
*
* @return {@link String }
* @author lvsheng
* @date 2022/03/08 14:32
*/
@GetMapping("/pre-request/session-attribute")
public String preRequestForSessionAttribute() {
//设置session属性
HttpSession session = request.getSession();
session.setAttribute("age", 100);
return "forward:/param/session-attribute";
}
/***
* cookie值
* Since:
* 3.0
*
* 操作步骤:
*
* STEP1: http://localhost:8041/param/pre-request/cookie-value
* STEP2: http://localhost:8041/param/cookie-value
*
* @param username 用户名称
* @return {@link String }
* @author lvsheng
* @date 2022/03/08 10:56
*/
@ResponseBody
@GetMapping("/cookie-value")
public String cookieValue(@CookieValue(value = "username", defaultValue = "xmz") String username) {
return "username=" + username;
}
/***
* 矩阵变量-单个属性接收
*
* Spring 3.2就已经支持@MatrixVariable特性,但直至现在其依然为默认禁用的状态
* 是为了增强URL请求地址的功能 需要配置removeSemicolonContent属性值为false才生效
* com.compass.msg.web.core.config.MatrixVariableConfig#configurePathMatch(org.springframework.web.servlet.config.annotation.PathMatchConfigurer)
*
* 用法: ;是用来分割变量的 ,是用来分割多个值的(多个值也可以使用多个相同的变量命名)
*
* 测试:
* http://localhost:8041/param/matrix-variable/attribute/username=lvsheng;age=100
*
* @param username 用户名
* @return {@link String }
* @author lvsheng
* @date 2022/03/08 11:05
*/
@ResponseBody
@GetMapping("/matrix-variable/attribute/{variable}")
public String matrixVariableAttribute(@MatrixVariable(value = "username", defaultValue = "xmz") String username,
@MatrixVariable("age") Integer age) {
return "username=" + username + ";age=" + age;
}
/***
* 矩阵变量-Map接收
*
* 测试:
* http://localhost:8041/param/matrix-variable/attribute/username=lvsheng;age=100
*
* @return {@link String }
* @author lvsheng
* @date 2022/03/08 13:46
*/
@ResponseBody
@GetMapping("/matrix-variable/map/{variable}")
public String matrixVariableMap(@MatrixVariable Map<String, Object> map) {
return "username=" + map.get("username") + ";age=" + map.get("age");
}
/***
* 矩阵变量-List接收
*
* 测试:
* http://localhost:8041/param/matrix-variable/list/address=上海,苏州,昆山
* http://localhost:8041/param/matrix-variable/list/address=上海;address=苏州;address=昆山
*
* @param addressList 地址列表
* @return {@link String }
* @author lvsheng
* @date 2022/03/08 13:24
*/
@ResponseBody
@GetMapping("/matrix-variable/list/{variable}")
public String matrixVariableList(@MatrixVariable("address") List<String> addressList) {
return String.join(StrUtil.COMMA, addressList);
}
/***
* 矩阵变量-数组接收
*
* 测试:
* http://localhost:8041/param/matrix-variable/arr/address=上海,苏州,昆山
* http://localhost:8041/param/matrix-variable/arr/address=上海;address=苏州;address=昆山
*
* @param addressArr 地址数组
* @return {@link String }
* @author lvsheng
* @date 2022/03/08 13:24
*/
@ResponseBody
@GetMapping("/matrix-variable/arr/{variable}")
public String matrixVariableArr(@MatrixVariable("address") String[] addressArr) {
return String.join(StrUtil.COMMA, addressArr);
}
/***
* 矩阵变量的路径PathVariable及MatrixVariable搭配使用
*
* 测试:
* http://localhost:8041/param/matrix-variable/path/lvsheng;address=上海
*
* @param username 用户名
* @param address 地址
* @return {@link String }
* @author lvsheng
* @date 2022/03/08 13:40
*/
@ResponseBody
@GetMapping("/matrix-variable/path/{username}")
public String matrixVariablePath(@PathVariable("username") String username, @MatrixVariable("address") String address) {
return "username=" + username + ";address=" + address;
}
/***
* 模型属性
* Since:
* 2.5
*
* 测试:
* http://localhost:8041/param/model-attribute?username=lvsheng&age=100&address=山海
*
* @param userDto 用户
* @return {@link String }
* @author lvsheng
* @date 2022/03/08 14:00
*/
@ResponseBody
@GetMapping("/model-attribute")
public String modelAttribute(@ModelAttribute UserDto userDto) {
return "username=" + userDto.getUsername() + ";age=" + userDto.getAge() + ";address=" + userDto.getAddress();
}
/***
* 路径变量
* Since:
* 3.0
* 测试:
* http://localhost:8041/param/path-variable/你好
*
* @return {@link String }
* @author lvsheng
* @date 2022/03/08 14:02
*/
@ResponseBody
@GetMapping("/path-variable/{info}")
public String pathVariable(@PathVariable String info) {
return info;
}
/***
* 请求属性 Since: 4.3
*
* 接收服务端设置的属性值 并非客户端(前端传递)的属性值-@RequestParam等其他注解均是客户端参数
*
* 测试:
* WAY1: http://localhost:8041/param/pre-request/request-attribute(请求转发)
*
* WAY2: http://localhost:8041/param/request-attribute (拦截器操作)
*
* WAY3: 将preRequest注释代码打开(@ModelAttribute预存值)
*
* @param username 用户名
* @return {@link String }
* @author lvsheng
* @date 2022/03/08 14:04
*/
@ResponseBody
@GetMapping("/request-attribute")
public String requestAttribute(@RequestAttribute(required = false) String username) {
return "username=" + username;
}
/***
*
* 请求体
*
* Since:
* 3.0
*
* 测试:
*
* curl --location --request GET 'http://localhost:8041/param/request-body' \
* --header 'Content-Type: application/json' \
* --header 'Cookie: username=lvsheng' \
* --data-raw '{
* "username": "吕升",
* "age": 100,
* "address": "上海"
* }'
*
* @param userDto 用户
* @return {@link String }
* @author lvsheng
* @date 2022/03/08 14:50
*/
@ResponseBody
@GetMapping("/request-body")
public String requestBody(@RequestBody UserDto userDto) {
return userDto.toString();
}
/***
* 请求头
* Since:
* 3.0
*
* 测试:
*
* curl --location --request GET 'http://localhost:8041/param/request-header' \
* --header 'username: zhangsan' \
* --header 'Cookie: username=lvsheng'
*
* @param username 用户名
* @return {@link String }
* @author lvsheng
* @date 2022/03/08 14:52
*/
@ResponseBody
@GetMapping("/request-header")
public String requestHeader(@RequestHeader String username) {
return "username=" + username;
}
/***
* 请求参数
* Since:
* 2.5
*
* 测试:
*
* http://localhost:8041/param/request-param?username=lvsheng
*
* @param username 用户名
* @return {@link String }
* @author lvsheng
* @date 2022/03/08 14:53
*/
@ResponseBody
@GetMapping("/request-param")
public String requestParam(@RequestParam String username) {
return "username=" + username;
}
/***
* 请求的一部分
* Since:
* 3.1
*
* 测试: curl --location --request GET 'http://localhost:8041/param/request-part' \ --header 'Cookie: username=lvsheng;
* JSESSIONID=AE346BDC6BB4211C14CBF6A7F84D5B58' \ --form 'upload=@"/C:/Users/sheng.lv/AppData/Local/Postman/app-8.12.4/Squirrel-UpdateSelf.log"'
*
* @param multipartFile 文件
* @return {@link String }
* @author lvsheng
* @date 2022/03/08 15:07
*/
@ResponseBody
@GetMapping("/request-part")
public String requestPart(@RequestPart("upload") MultipartFile multipartFile) {
return multipartFile.getOriginalFilename();
}
/***
* 会话属性
* Since:
* 4.3
*
* 测试:
*
* WAY1: http://localhost:8041/param/pre-request/session-attribute(请求转发)
*
* WAY2: http://localhost:8041/param/session-attribute(拦截器)
*
* WAY3: 将preRequest注释代码打开(@ModelAttribute预存值)
*
* @param age 年龄
* @return {@link String }
* @author lvsheng
* @date 2022/03/08 15:06
*/
@ResponseBody
@GetMapping("/session-attribute")
public String sessionAttribute(@SessionAttribute Integer age) {
return "age=" + age;
}
/**
* 会话属性 需要结合@SessionAttributes注解使用 该注解是作用于类上的
*
* @param model 模型
* @return {@link String }
* @author lvsheng
* @date 2022/03/09 09:09
*/
@GetMapping("/session-attributes")
public String sessionAttributes(Model model) {
UserDto userDto = new UserDto()
.setUsername("吕升")
.setAge(100)
.setAddress("上海");
// 这步操作结合+@SessionAttributes(types = {UserDto.class}, value = "userDto")会将userDto放置到
// HttpSession中
model.addAttribute("userDto", userDto);
return "forward:/param/session-attributes/get";
}
/**
* 获取会话属性
*
* @param userDto 用户请求实体
* @return {@link String }
* @author lvsheng
* @date 2022/03/09 09:09
*/
@ResponseBody
@GetMapping("/session-attributes/get")
public String getSessionAttribute(@SessionAttribute UserDto userDto) {
return userDto.toString();
}
}
-
拓展:
filter>interceptor>advice>aspect>controller
-
3. 常用的参数绑定注解如下:
(1) ModelAttribute
(2) PathVariable
(3) RequestBody
(4) RequestParam
-
4. 选取部分注解研究一下内部绑定流程:
@PathVariable
@RequestBody RequestBody 绑定对象是使用setter方法还是直接属性赋值?
(jackson object-mapper setter赋值)
-
5. 源码跟踪
-
5.1 @PathVariable参数绑定的核心流程:
1 ) org.springframework.web.servlet.DispatcherServlet#doDispatch
//调用处理器 在这个步骤中完成参数绑定操作
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
2 ) 根据handler的参数注解定位到对应参数解析器
3 ) 调用对应参数解析器的org.springframework.web.servlet.mvc.method.annotation.PathVariableMethodArgumentResolver#resolveName方法
4 ) 反射调用handler(Method)的invoke方法
org.springframework.web.method.support.InvocableHandlerMethod#doInvoke
/**
* Invoke the handler method with the given argument values.
*/
@Nullable
protected Object doInvoke(Object... args) throws Exception {
ReflectionUtils.makeAccessible(getBridgedMethod());
try {
return getBridgedMethod().invoke(getBean(), args);
}
catch (IllegalArgumentException ex) {
assertTargetBean(getBridgedMethod(), getBean(), args);
String text = (ex.getMessage() != null ? ex.getMessage() : "Illegal argument");
throw new IllegalStateException(formatInvokeError(text, args), ex);
}
catch (InvocationTargetException ex) {
// Unwrap for HandlerExceptionResolvers ...
Throwable targetException = ex.getTargetException();
if (targetException instanceof RuntimeException) {
throw (RuntimeException) targetException;
}
else if (targetException instanceof Error) {
throw (Error) targetException;
}
else if (targetException instanceof Exception) {
throw (Exception) targetException;
}
else {
throw new IllegalStateException(formatInvokeError("Invocation failure", args), targetException);
}
}
}
-
关键点:
org.springframework.web.method.HandlerMethod#parameters 参数初始化过程:
在实例化SourceController的时候进行参数初始化
debug栈 -
HandlerMethodArgumentResolver 初始化的流程:
step1:
org.springframework.context.support.AbstractApplicationContext#prepareBeanFactory 向org.springframework.beans.factory.support.DefaultListableBeanFactory#beanDefinitionNames中添加RequestMappingHandlerAdapter
@SpringBootApplication->@EnableAutoConfiguration->spring.factories->org.springframework.boot.autoconfigure.EnableAutoConfiguration->org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration->WebMvcAutoConfiguration$EnableWebMvcConfiguration->
step2:
org.springframework.context.support.AbstractApplicationContext#finishBeanFactoryInitialization中实例化RequestMappingHandlerAdapter Bean并调用org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#afterPropertiesSet初始化HandlerMethodArgumentResolver
- 注入RequestMappingHandlerAdapter代码:
@Bean
@Override
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcValidator") Validator validator) {
RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter(contentNegotiationManager,
conversionService, validator);
adapter.setIgnoreDefaultModelOnRedirect(
this.mvcProperties == null || this.mvcProperties.isIgnoreDefaultModelOnRedirect());
return adapter;
}
- RequestMappingHandlerAdapter的afterPropertiesSet方法初始化HandlerMethodArgumentResolver对象列表
org.springframework.beans.factory.InitializingBean
其实是维护在HandlerMethodArgumentResolverComposite的argumentResolvers属性中
@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBody advice beans
initControllerAdviceCache();
if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.initBinderArgumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
- org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#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<>(30);
// 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;
}
- org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#getDefaultInitBinderArgumentResolvers
/**
* Return the list of argument resolvers to use for {@code @InitBinder}
* methods including built-in and custom resolvers.
*/
private List<HandlerMethodArgumentResolver> getDefaultInitBinderArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(20);
// 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 ExpressionValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new SessionAttributeMethodArgumentResolver());
resolvers.add(new RequestAttributeMethodArgumentResolver());
// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
// Custom arguments
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}
// Catch-all
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
return resolvers;
}
-
5.2 @RequestBody参数绑定的核心流程:
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor
相同映射方法名及参数名对比缓存值
同@PathVariable相比多了MessageConvert处理
-
6. 相关:
@RequestParam同RequestAttribute区别:@RequestParam绑定的是客户端参数 @RequestAttribute绑定的是服务端参数(过滤器、拦截器、Aop等场景设置的参数)
@ModelAttribute和@RequestBody 区别