【工作篇】再次熟悉SpringMVC 参数绑定
前言
主要现在项目中使用的参数绑定五花八门的,搞得很头大,例如有些用字符串接收日期,用字符串接受数组等等,完全没有利用好 SpringMVC 的优势,这里自己也总结一下,免得到时又要百度谷歌查找。
以下实践的 Spring 版本是:5.2.7.RELEASE
一、SpringMVC 中不同类型的数据绑定
1.1、基础数据类型
- 默认参数名
// http://localhost:8080/baseType3?a=123@GetMapping("/baseType")@ResponseBodypublic String baseType(int a) {return "baseType " + a;}
- 使用@RequestParam 自定义请求参数名称
// http://localhost:8080/baseType3?b=123@GetMapping("/baseType3")@ResponseBodypublic 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;}
- 内嵌对象接收参数
@Datapublic 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 解决不同对象,参数名相同覆盖问题
- 定义对象
@Datapublic class Friend {Integer id;String name; //与User 对象name 名称冲突}@Datapublic 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*/@InitBinderpublic void initBinderDate(WebDataBinder binder) {binder.addCustomFormatter(new Formatter<Date>() {@Overridepublic Date parse(String text, Locale locale) throws ParseException {System.out.println("InitBinder addCustomFormatter String to Date ");return new SimpleDateFormat("yyyy-MM-dd").parse(text);}@Overridepublic 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 修饰的参数)无效
@Configurationpublic class WebConfig implements WebMvcConfigurer {/*** 注册 Converters 和 Formatters** @param registry*/@Overridepublic void addFormatters(FormatterRegistry registry) {//参数传出格式化registry.addFormatter(new DateFormatter("yyyy-MM-dd"));}}
1.3.6、@JsonFormat 单独配置字段格式化
只对 @RequestBody 修饰的参数有效
- 定义实体
@Datapublic class UserDate {@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM")private Date birthday;}
- 请求
/*** http://localhost:8080/dateType4* {* "birthday": "2020-08"* }*/@PostMapping("/dateType4")@ResponseBodypublic UserDate dateType4(@RequestBody UserDate userDate) {return userDate;}
1.3.7、全局配置 JSON 参数日期格式化
注意: 全局配置后,依然可以使用 @JsonFormat 注解,用来接收特殊的日期参数格式。
- 配置
@Configurationpublic class WebConfig implements WebMvcConfigurer {@Overridepublic 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()));}}
- 实体
@Datapublic 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")@ResponseBodypublic 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)解析为参数值并填充到指定对象中@NullableObject 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#afterPropertiesSetprivate 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 resolutionresolvers.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-allresolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));resolvers.add(new ServletModelAttributeMethodProcessor(true));return resolvers;}
2.1.2、执行过程
- 初始化解析器到 RequestMappingHandlerAdapter 中
// org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#requestMappingHandlerAdapter@Beanpublic 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@Overridepublic void afterPropertiesSet() {// Do this first, it may add ResponseBody advice beansinitControllerAdviceCache();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#getMethodArgumentValuesprotected 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@Nullablepublic 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) {//创建WebDataBinderWebDataBinder 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@InitBinderpublic void initBinderUser(WebDataBinder binder) {System.out.println("BaseController WebDataBinder 执行" );}}@RestControllerpublic class DemoDataBindingController extends BaseController {}
- @ControllerAdvice 类 中定义,每个请求都会执行,适合全局配置
@ControllerAdvicepublic class ControllerAdviceConfig {@InitBinderpublic void initBinderUser(WebDataBinder binder) {System.out.println("ControllerAdvice WebDataBinder 执行" );}}
- 自定义 WebBindingInitializer
//默认实现 ConfigurableWebBindingInitializerpublic interface WebBindingInitializer {// org.springframework.web.bind.support.DefaultDataBinderFactory#createBinder 创建时调用// 比@InitBinder 注解的方法先执行void initBinder(WebDataBinder binder);@Deprecateddefault void initBinder(WebDataBinder binder, WebRequest request) {initBinder(binder);}}
@Configurationpublic class CustomConfigurableWebBindingInitializer extends ConfigurableWebBindingInitializer {@Overridepublic 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#invokeHandlerMethodprivate 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 的参数绑定讲解完成了。
