前言
主要現在專案中使用的引數繫結五花八門的,搞得很頭大,例如有些用字串接收日期,用字串接受陣列等等,完全沒有利用好 SpringMVC 的優勢,這裡自己也總結一下,免得到時又要百度谷歌查詢。
以下實踐的 Spring 版本是:5.2.7.RELEASE
一、SpringMVC 中不同型別的資料繫結
1.1、基礎資料型別
- 預設引數名
// http://localhost:8080/baseType3?a=123
@GetMapping("/baseType")
@ResponseBody
public String baseType(int a) {
return "baseType " + a;
}
- 使用@RequestParam 自定義請求引數名稱
// http://localhost:8080/baseType3?b=123
@GetMapping("/baseType3")
@ResponseBody
public String baseType3(@RequestParam(value = "b", required = true) Integer a) {
return "baseType3 " + a;
}
- 多個引數
// http://localhost:8080/baseType4?age=10&name=Java
@GetMapping("/baseType4")
public String baseType3(@RequestParam Integer age, String name) {
return "baseType4 age:" + age + " name="+name;
}
1.2、 物件型別
超過三個引數及以上,則推薦使用物件來接收傳遞的引數
- 定義簡單物件接收引數
@Data //這裡使用了 lombok 外掛
public class User {
Integer id;
String name;
}
// http://localhost:8080/objectType?id=1&name=Java
@GetMapping("/objectType")
public String objectType(User user) {
return "objectType " + user;
}
- 內嵌物件接收引數
@Data
public class Order {
Integer id;
User user;
}
// http://localhost:8080/objectType2?id=1&user.name=Java&user.id=2
@GetMapping("/objectType2")
public String objectType2(Order order) {
return "objectType2 " + order;
}
-
使用 DataBinder 解決不同物件,引數名相同覆蓋問題
- 定義物件
@Data
public class Friend {
Integer id;
String name; //與User 物件name 名稱衝突
}
@Data
public class User {
Integer id;
String name;
}
-
InitBinder 配置
在 Controller 中定義,只對當前 Controller 有效,也可以在 @ControllerAdvice 類中全域性定義
/**
* 初始化繫結引數user 標識字首
*
* @param binder
*/
@InitBinder("user")
public void initBinderUser(WebDataBinder binder) {
binder.setFieldDefaultPrefix("user.");
}
/**
* 初始化繫結引數friend 標識字首
*
* @param binder
*/
@InitBinder("friend")
public void initBinderFriend(WebDataBinder binder) {
binder.setFieldDefaultPrefix("friend.");
}
- 編寫請求
//http://localhost:8080/objectType3?name=Java name會同時填充到User 和Friend物件上
//http://localhost:8080/objectType3?user.name=Java&friend.name=Python 分別填充資料到各自的物件中去
@GetMapping("/objectType3")
public String objectType3(User user, Friend friend) {
return "objectType3 user" + user + " friend " + friend;
}
1.3、 日期型別
日期型別的引數傳遞方式比較多,正式專案中建議統一規定日期型別的引數繫結的格式
1.3.1、使用時間戳傳遞(不是引數繫結方式)
// http://localhost:8080/dateType6?date=1628752881
@GetMapping("/dateType6")
public String dateType5(Long date) {
return "dateType6 date" + new Date(date);
}
1.3.2、使用字串接收(不是引數繫結方式)
// http://localhost:8080/dateType7?date=2021-08-12
@GetMapping("/dateType7")
public String dateType7(String date) throws ParseException {
return "dateType7 date" + new SimpleDateFormat("yyyy-MM-dd").parse(date);
}
1.3.3、使用 SpringMVC 預設提供的 @DateTimeFormat (對於 json 引數無效)
// http://localhost:8080/dateType2?date1=2020-01-01
@GetMapping("/dateType2")
public String dateType2(@DateTimeFormat(pattern = "yyyy-MM-dd") Date date1) {
return "dateType2 date " + date1;
}
1.3.4、使用 @InitBinder 註冊轉換器
- 新增轉換器
/**
* 註冊日期轉換 date
*
* @param binder
*/
@InitBinder
public void initBinderDate(WebDataBinder binder) {
binder.addCustomFormatter(new Formatter<Date>() {
@Override
public Date parse(String text, Locale locale) throws ParseException {
System.out.println("InitBinder addCustomFormatter String to Date ");
return new SimpleDateFormat("yyyy-MM-dd").parse(text);
}
@Override
public String print(Date date, Locale locale) {
System.out.println("InitBinder addCustomFormatter Date to String ");
return new SimpleDateFormat("yyyy-MM-dd").format(date);
}
});
}
- 請求
// http://localhost:8080/dateType?date=2020-01-01
@GetMapping("/dateType")
public String dateType(Date date) {
return "dateType date" + date;
}
1.3.5、全域性配置 Formatter
對於 json 引數(@RequestBody 修飾的引數)無效
@Configuration
public class WebConfig implements WebMvcConfigurer {
/**
* 註冊 Converters 和 Formatters
*
* @param registry
*/
@Override
public void addFormatters(FormatterRegistry registry) {
//引數傳出格式化
registry.addFormatter(new DateFormatter("yyyy-MM-dd"));
}
}
1.3.6、@JsonFormat 單獨配置欄位格式化
只對 @RequestBody 修飾的引數有效
- 定義實體
@Data
public class UserDate {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM")
private Date birthday;
}
- 請求
/**
* http://localhost:8080/dateType4
* {
* "birthday": "2020-08"
* }
*/
@PostMapping("/dateType4")
@ResponseBody
public UserDate dateType4(@RequestBody UserDate userDate) {
return userDate;
}
1.3.7、全域性配置 JSON 引數日期格式化
注意: 全域性配置後,依然可以使用 @JsonFormat 註解,用來接收特殊的日期引數格式。
- 配置
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
//指定時區
.timeZone(TimeZone.getTimeZone("GMT+8:00"))
//日期格式化
.dateFormat(new SimpleDateFormat("yyyy-MM-dd"));
converters.add(0, new MappingJackson2HttpMessageConverter(builder.build()));
}
}
- 實體
@Data
public class UserDate {
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM")
private Date birthday;
private Date date;
}
- 請求
/**
* http://localhost:8080/dateType4
* {
* "birthday": "2020-08",
* "date": "2021-08-13"
* }
*/
@PostMapping("/dateType4")
@ResponseBody
public UserDate dateType4(@RequestBody UserDate userDate) {
return userDate;
}
1.4、 複雜型別
複雜型別包括陣列和集合型別,像 List、Set、Map。以下以 List 為例
- 使用逗號分割形式
/**
* 請求形式
* http://localhost:8080/complexType2_1?list=1,2,3
*/
@GetMapping("/complexType2_1")
public String complexType2_1(@RequestParam("list") List<String> list) {
return "complexType2_1 " + list;
}
- 相同引數明傳遞多次
/**
* 請求形式
* http://localhost:8080/complexType2?list=1&list=2
*/
@GetMapping("/complexType2")
public String complexType2(@RequestParam("list") List<String> list) {
return "complexType2 " + list;
}
- 使用 JSON 字串傳遞
/**
* 請求形式
* http://localhost:8080/complexType4
* <p>
* 請求體
* [1,2,3]
*/
@PostMapping("/complexType4")
public String complexType4(@RequestBody List<String> list) {
return "complexType4 " + list;
}
1.5、 特殊型別
- xml
@Data
@XmlRootElement(name ="user")
public class User {
Integer id;
String name;
}
/**
* http://localhost:8080/xmlType
<?xml version="1.0" encoding="utf-8"?>
<user>
<id>1</id>
<name>Java</name>
</user>
*/
@PostMapping(path = "/xmlType", consumes = "application/xml;charset=UTF-8")
public String xmlType(@RequestBody User user) {
return "xmlType " + user;
}
- json
/**
* 請求
* http://localhost:8080/jsonType
* 請求體
{
"id": 1,
"name": "Java"
}
*
* @RequestBody 不支援GET請求
*/
@PostMapping(value = "/jsonType", consumes = "application/json")
public String jsonType(@RequestBody User user) {
return "jsonType " + user;
}
二、瞭解底層實現
2.1、SpringMVC 方法引數繫結
2.1.1、認識 HandlerMethodArgumentResolver 介面
public interface HandlerMethodArgumentResolver {
//該解析器是否支援parameter引數的解析
boolean supportsParameter(MethodParameter parameter);
//從給定請求(webRequest)解析為引數值並填充到指定物件中
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
2.1.2、內建的 HandlerMethodArgumentResolver
//在初始化RequestMappingHandlerAdapter 時會預設載入引數解析器
// org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#afterPropertiesSet
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>();
// Annotation-based argument resolution
//處理 @RequestParam 註解標識的引數
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
//處理@RequestParam 註解標識的Map引數且不能指定引數名稱
resolvers.add(new RequestParamMapMethodArgumentResolver());
//處理@PathVariable 註解標識路徑引數 如/pathVariable/{a}
resolvers.add(new PathVariableMethodArgumentResolver());
//處理@PathVariable 註解標識的Map引數且不能指定引數名稱
resolvers.add(new PathVariableMapMethodArgumentResolver());
//處理@MatrixVariable註解標識的引數
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
resolvers.add(new ServletModelAttributeMethodProcessor(false));
//處理@RequestBody 註解
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
//處理請求頭
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
//處理Cookie 值
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());
// 新增自定義的解析器
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}
// Catch-all
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
resolvers.add(new ServletModelAttributeMethodProcessor(true));
return resolvers;
}
2.1.2、執行過程
- 初始化解析器到 RequestMappingHandlerAdapter 中
// org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#requestMappingHandlerAdapter
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(
@Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
@Qualifier("mvcConversionService") FormattingConversionService conversionService,
@Qualifier("mvcValidator") Validator validator) {
RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
adapter.setContentNegotiationManager(contentNegotiationManager);
adapter.setMessageConverters(getMessageConverters());
adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer(conversionService, validator));
//可以實現org.springframework.web.servlet.config.annotation.WebMvcConfigurer介面
//設定自定義的引數解析器
adapter.setCustomArgumentResolvers(getArgumentResolvers());
adapter.setCustomReturnValueHandlers(getReturnValueHandlers());
if (jackson2Present) {
adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
}
AsyncSupportConfigurer configurer = new AsyncSupportConfigurer();
configureAsyncSupport(configurer);
if (configurer.getTaskExecutor() != null) {
adapter.setTaskExecutor(configurer.getTaskExecutor());
}
if (configurer.getTimeout() != null) {
adapter.setAsyncRequestTimeout(configurer.getTimeout());
}
adapter.setCallableInterceptors(configurer.getCallableInterceptors());
adapter.setDeferredResultInterceptors(configurer.getDeferredResultInterceptors());
return adapter;
}
// org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#afterPropertiesSet
@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);
}
}
- 尋找合適的解析器
//1. org.springframework.web.servlet.DispatcherServlet#doDispatch
//2. org.springframework.web.servlet.HandlerAdapter#handle
//3. org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod#invokeAndHandle
//4. org.springframework.web.method.support.InvocableHandlerMethod#getMethodArgumentValues
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
//獲取方法引數
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
//判斷是否支援解析該引數
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
//HandlerMethodArgumentResolverComposite 組合模式
//使用具體HandlerMethodArgumentResolver 解析引數
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}
- 解析引數
// @RequestParam 註解的引數
// org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver#resolveArgument
//不同解析器實現不一樣
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
//根據引數定義建立一個NamedValueInfo物件
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
//如果引數是使用Optional包裹,則獲取內嵌的引數物件
MethodParameter nestedParameter = parameter.nestedIfOptional();
// 處理引數名稱
Object resolvedName = resolveStringValue(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException(
"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
}
//解析請求引數值
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
arg = resolveStringValue(namedValueInfo.defaultValue);
}
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
arg = resolveStringValue(namedValueInfo.defaultValue);
}
if (binderFactory != null) {
//建立WebDataBinder
WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
try {
//轉換請求引數為對應方法形參
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
}
catch (ConversionNotSupportedException ex) {
throw new MethodArgumentConversionNotSupportedException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
catch (TypeMismatchException ex) {
throw new MethodArgumentTypeMismatchException(arg, ex.getRequiredType(),
namedValueInfo.name, parameter, ex.getCause());
}
}
//處理路徑引數
handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
return arg;
}
2.2、WebDataBinder 原理
2.2.1、初始化 WebDataBinder 方式
- @Controller 在每個控制器中定義(或者提取到 BaseController )
public class BaseController {
// @InitBinder 註解的方法,返回值需要宣告為void
@InitBinder
public void initBinderUser(WebDataBinder binder) {
System.out.println("BaseController WebDataBinder 執行" );
}
}
@RestController
public class DemoDataBindingController extends BaseController {
}
- @ControllerAdvice 類 中定義,每個請求都會執行,適合全域性配置
@ControllerAdvice
public class ControllerAdviceConfig {
@InitBinder
public void initBinderUser(WebDataBinder binder) {
System.out.println("ControllerAdvice WebDataBinder 執行" );
}
}
- 自定義 WebBindingInitializer
//預設實現 ConfigurableWebBindingInitializer
public interface WebBindingInitializer {
// org.springframework.web.bind.support.DefaultDataBinderFactory#createBinder 建立時呼叫
// 比@InitBinder 註解的方法先執行
void initBinder(WebDataBinder binder);
@Deprecated
default void initBinder(WebDataBinder binder, WebRequest request) {
initBinder(binder);
}
}
@Configuration
public class CustomConfigurableWebBindingInitializer extends ConfigurableWebBindingInitializer {
@Override
public void initBinder(WebDataBinder binder) {
super.initBinder(binder);
System.out.println("CustomConfigurableWebBindingInitializer initBinder");
}
}
//發起請求時,控制檯輸出
//CustomConfigurableWebBindingInitializer initBinder
//ControllerAdvice WebDataBinder 執行
2.2.2、WebDataBinder 有什麼作用?
- 用於繫結請求引數(Form 表單引數,query 引數)到模型物件中
- 用於轉換 字串引數(請求引數、路徑引數、header 屬性、Cookie) 為 Controller 方法形參的對應型別
- 格式化物件為指定字串格式
2.2.3、WebDataBinder 執行過程
- 定義初始化 WebDataBinder 方式(#2.2.1)
- 建立 DataBinderFactory
//1. org.springframework.web.servlet.DispatcherServlet#doDispatch
//2. org.springframework.web.servlet.HandlerAdapter#handle
//3. org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter#invokeHandlerMethod
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
Class<?> handlerType = handlerMethod.getBeanType();
Set<Method> methods = this.initBinderCache.get(handlerType);
if (methods == null) {
// 查詢@Controller中 @InitBinder 註解的方法
methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
this.initBinderCache.put(handlerType, methods);
}
List<InvocableHandlerMethod> initBinderMethods = new ArrayList<InvocableHandlerMethod>();
// Global methods first
// initBinderAdviceCache 在 RequestMappingHandlerAdapter#afterPropertiesSet 裡初始化
// 1. 先載入 在@ControllerAdvice類定義的 @InitBinder 註解的方法
for (Entry<ControllerAdviceBean, Set<Method>> entry : this.initBinderAdviceCache.entrySet()) {
if (entry.getKey().isApplicableToBeanType(handlerType)) {
Object bean = entry.getKey().resolveBean();
for (Method method : entry.getValue()) {
initBinderMethods.add(createInitBinderMethod(bean, method));
}
}
}
//2. 再載入@Controller中 @InitBinder 註解的方法
for (Method method : methods) {
Object bean = handlerMethod.getBean();
initBinderMethods.add(createInitBinderMethod(bean, method));
}
return createDataBinderFactory(initBinderMethods);
}
- 執行 initBinder 方法
// org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver#resolveArgument
// org.springframework.web.bind.support.DefaultDataBinderFactory#createBinder
@Override
@SuppressWarnings("deprecation")
public final WebDataBinder createBinder(
NativeWebRequest webRequest, @Nullable Object target, String objectName) throws Exception {
WebDataBinder dataBinder = createBinderInstance(target, objectName, webRequest);
if (this.initializer != null) {
//執行 WebBindingInitializer 定義的initBinder方法
this.initializer.initBinder(dataBinder, webRequest);
}
//執行 @InitBinder 註解的方法
initBinder(dataBinder, webRequest);
return dataBinder;
}
到此,對 SpringMVC 的引數繫結講解完成了。