- 控制層框架歷史:
Servlet->Structs1.0->Structs2.0->SpringMVC->SpringBoot(嵌入式)
- 其實控制層框架包含兩個層面的綁定:路由地址的綁定(涉及控制器及具體路由方法綁定)、請求參數(shù)的綁定戒突,本文講的是后者 后續(xù)有時間會同步前者
-
1. 你知道的參數(shù)綁定注解有哪些屯碴?
所屬jar:org.springframework:spring-web:5.2.12.RELEASE
所屬package:package org.springframework.web.bind.annotation
標注有@Target(ElementType.PARAMETER)注解的均屬于參數(shù)綁定注解如下:
@CookieValue(綁定客戶端的cookie屬性)
@MatrixVariable(是對@PathVariable的增強)
@ModelAttribute
@PathVariable(路徑參數(shù)綁定)
@RequestAttribute(服務端自身參數(shù)綁定)
@RequestBody(請求體json或者xml參數(shù)綁定)
@RequestHeader(請求頭參數(shù)綁定)
@RequestParam(param參數(shù)綁定)
@RequestPart(文件上傳參數(shù)綁定)
@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特性,但直至現(xiàn)在其依然為默認禁用的狀態(tài)
*
* @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注釋的方法會在這個控制器每個方法執(zhí)行前被執(zhí)行
// *
// * @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請求轉發(fā)
*
* @return {@link String }
* @author lvsheng
* @date 2022/03/08 14:32
*/
@GetMapping("/pre-request/request-attribute")
public String preRequestForRequestAttribute() {
//設置服務端系統(tǒng)屬性參數(shù)
request.setAttribute("username", "小麻花");
return "forward:/param/request-attribute";
}
/**
* 預請求-session-attribute請求轉發(fā)
*
* @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特性膊存,但直至現(xiàn)在其依然為默認禁用的狀態(tài)
* 是為了增強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);
}
/***
* 矩陣變量-數(shù)組接收
*
* 測試:
* http://localhost:8041/param/matrix-variable/arr/address=上海,蘇州,昆山
* http://localhost:8041/param/matrix-variable/arr/address=上海;address=蘇州;address=昆山
*
* @param addressArr 地址數(shù)組
* @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等其他注解均是客戶端參數(shù)
*
* 測試:
* WAY1: http://localhost:8041/param/pre-request/request-attribute(請求轉發(fā))
*
* 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;
}
/***
* 請求參數(shù)
* 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(請求轉發(fā))
*
* 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. 常用的參數(shù)綁定注解如下:
(1) ModelAttribute
(2) PathVariable
(3) RequestBody
(4) RequestParam
-
4. 選取部分注解研究一下內部綁定流程:
@PathVariable
@RequestBody RequestBody 綁定對象是使用setter方法還是直接屬性賦值导而?
(jackson object-mapper setter賦值)
-
5. 源碼跟蹤
-
5.1 @PathVariable參數(shù)綁定的核心流程:
1 ) org.springframework.web.servlet.DispatcherServlet#doDispatch
//調用處理器 在這個步驟中完成參數(shù)綁定操作
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
2 ) 根據(jù)handler的參數(shù)注解定位到對應參數(shù)解析器
3 ) 調用對應參數(shù)解析器的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 參數(shù)初始化過程:
在實例化SourceController的時候進行參數(shù)初始化
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參數(shù)綁定的核心流程:
org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor
同@PathVariable相比多了MessageConvert處理
-
6. 相關:
@RequestParam同RequestAttribute區(qū)別:@RequestParam綁定的是客戶端參數(shù) @RequestAttribute綁定的是服務端參數(shù)(過濾器、攔截器陌僵、Aop等場景設置的參數(shù))
@ModelAttribute和@RequestBody 區(qū)別