如何在Spring Boot应用中优雅的使用Date和LocalDateTime的教程详解
Java8已经发布很多年了,但是很多人在开发时仍然坚持使用着Date
和SimpleDateFormat
进行时间操作。SimpleDateFormat
不是线程安全的,而Date
处理时间很麻烦,所以Java8提供了LocalDateTime
、LocalDate
和LocalTime
等全新的时间操作API。无论是Date
还是LocalDate
,在开发Spring Boot应用时经常需要在每个实体类的日期字段上加上@DateTimeFormat
注解来接收前端传值与日期字段绑定,加上@JsonFormat
注解来让返回前端的日期字段格式化成我们想要的时间格式。时间和日期类型在开发中使用的频率是非常高的,如果每个字段都加上这两个注解的话是非常繁琐的,有没有一种全局设置的处理方式呢?今天就来向大家介绍一下。
注:本文基于Springboot2.3.0版本。
根据不同的请求方式需要做不同的配置,下文中分为了JSON方式传参和GET请求及POST表单方式传参两种情况。
JSON方式传参
这种情况指的是类型POST,Content-Type 是application/json 方式的请求。对于这类请求,controller中需要加上@RequestBody
注解来标注到我们用来接收请求参数的局部变量上,代码如下:
@SpringBootApplication @RestController public class SpringbootDateLearningApplication { public static void main(String[] args) { SpringApplication.run(SpringbootDateLearningApplication.class, args); } /** * DateTime格式化字符串 */ private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; /** * Date格式化字符串 */ private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd"; /** * Time格式化字符串 */ private static final String DEFAULT_TIME_PATTERN = "HH:mm:ss"; public static class DateEntity { private LocalDate date; private LocalDateTime dateTime; private Date originalDate; public LocalDate getDate() { return date; } public void setDate(LocalDate date) { this.date = date; } public LocalDateTime getDateTime() { return dateTime; } public void setDateTime(LocalDateTime dateTime) { this.dateTime = dateTime; } public Date getOriginalDate() { return originalDate; } public void setOriginalDate(Date originalDate) { this.originalDate = originalDate; } } @RequestMapping("/date") public DateEntity getDate(@RequestBody DateEntity dateEntity) { return dateEntity; } }
假设默认的接收和返回值的格式都是yyyy-MM-dd HH:mm:ss
,可以有以下几个方案。
配置application.yml 文件
在application.yml文件中配置上如下内容:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8
小结:
- 支持Content-Type 是application/json的POST请求,请求参数字符串和返回的格式都是
yyyy-MM-dd HH:mm:ss
如果请求参数是其他格式,如yyyy-MM-dd
字符串则报400 Bad Request异常。 - 不支持LocalDate等Java8日期API。
增加Jackson配置
/** * Jackson序列化和反序列化转换器,用于转换Post请求体中的json以及将对象序列化为返回响应的json */ @Bean public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { return builder -> builder .serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN))) .serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN))) .serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN))) .serializerByType(Date.class, new DateSerializer(false, new SimpleDateFormat(DEFAULT_DATETIME_PATTERN))) .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN))) .deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN))) .deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN))) .deserializerByType(Date.class, new DateDeserializers.DateDeserializer(DateDeserializers.DateDeserializer.instance, new SimpleDateFormat(DEFAULT_DATETIME_PATTERN), DEFAULT_DATETIME_PATTERN)) ; }
小结:
- 支持Content-Type 是application/json的POST请求,请求参数字符串和返回的格式都是
yyyy-MM-dd HH:mm:ss
如果请求参数是其他格式,如yyyy-MM-dd
字符串则报400 Bad Request异常。 - 支持LocalDate等Java8日期API。
PS:上面的方式是通过配置一个Jackson2ObjectMapperBuilderCustomizer
Bean完成的,除了这种,也可以通过自定义一个MappingJackson2HttpMessageConverter
来实现。
@Bean public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); ObjectMapper objectMapper = new ObjectMapper(); // 指定时区 objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8:00")); // 日期类型字符串处理 objectMapper.setDateFormat(new SimpleDateFormat(DEFAULT_DATETIME_PATTERN)); // Java8日期日期处理 JavaTimeModule javaTimeModule = new JavaTimeModule(); javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN))); javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN))); javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN))); javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN))); javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN))); javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN))); objectMapper.registerModule(javaTimeModule); converter.setObjectMapper(objectMapper); return converter; }
以上几种方式都可以实现JSON传参时的全局化配置,更推荐后两种代码中增加配置bean的方式,可以同时支持Date
和LocalDate
。
GET请求及POST表单方式传参
这种方式和上面的JSON方式,在Spring Boot处理的方式是完全不同的。上一种JSON方式传参是在HttpMessgeConverter
中通过jackson的ObjectMapper
将http请求体转换成我们写在controller中的参数对象的,而这种方式用的是Converter
接口(spring-core中定义的用于将源类型(一般是String
)转成目标类型的接口),两者是有本质区别的。
自定义参数转换器(Converter)
自定义一个参数转换器,实现上面提到的org.springframework.core.convert.converter.Converter
接口,在配置类里配置上以下几个bean,示例如下:
@Bean public Converter<String, Date> dateConverter() { return new Converter<>() { @Override public Date convert(String source) { SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN); try { return formatter.parse(source); } catch (Exception e) { throw new RuntimeException(String.format("Error parsing %s to Date", source)); } } }; } @Bean public Converter<String, LocalDate> localDateConverter() { return new Converter<>() { @Override public LocalDate convert(String source) { return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)); } }; } @Bean public Converter<String, LocalDateTime> localDateTimeConverter() { return new Converter<>() { @Override public LocalDateTime convert(String source) { return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)); } }; }
同时把controller接口增加一些参数,可以发现在接口里单独用变量接收也是可以正常转换的。
@RequestMapping("/date") public DateEntity getDate( LocalDate date, LocalDateTime dateTime, Date originalDate, DateEntity dateEntity) { System.out.printf("date=%s, dateTime=%s, originalDate=%s \n", date, dateTime, originalDate); return dateEntity; }
小结:
- GET请求及POST表单方式请求。
- 支持LocalDate等Java8日期API。
使用@DateTimeFormat
注解
和前面提到的一样,GET请求及POST表单方式也是可以用@DateTimeFormat
来处理的,单独在controller接口参数或者实体类属性中都可以使用,比如@DateTimeFormat(pattern = "yyyy-MM-dd") Date originalDate
。注意,如果使用了自定义参数转化器(Converter),Spring会优先使用该方式进行处理,即@DateTimeFormat
注解不生效,两种方式是不兼容的。
那么假如我们使用了自定义参数转换器,但是还是想兼容用yyyy-MM-dd
形式接受呢?我们可以把前面的dateConverter
改成用正则匹配方式,这样也不失为一种不错的解决方案,示例如下。
/** * 日期正则表达式 */ private static final String DATE_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])"; /** * 时间正则表达式 */ private static final String TIME_REGEX = "(20|21|22|23|[0-1]\\d):[0-5]\\d:[0-5]\\d"; /** * 日期和时间正则表达式 */ private static final String DATE_TIME_REGEX = DATE_REGEX + "\\s" + TIME_REGEX; /** * 13位时间戳正则表达式 */ private static final String TIME_STAMP_REGEX = "1\\d{12}"; /** * 年和月正则表达式 */ private static final String YEAR_MONTH_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])"; /** * 年和月格式 */ private static final String YEAR_MONTH_PATTERN = "yyyy-MM"; @Bean public Converter<String, Date> dateConverter() { return new Converter<String, Date>() { @SuppressWarnings("NullableProblems") @Override public Date convert(String source) { if (StrUtil.isEmpty(source)) { return null; } if (source.matches(TIME_STAMP_REGEX)) { return new Date(Long.parseLong(source)); } DateFormat format; if (source.matches(DATE_TIME_REGEX)) { format = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN); } else if (source.matches(DATE_REGEX)) { format = new SimpleDateFormat(DEFAULT_DATE_FORMAT); } else if (source.matches(YEAR_MONTH_REGEX)) { format = new SimpleDateFormat(YEAR_MONTH_PATTERN); } else { throw new IllegalArgumentException(); } try { return format.parse(source); } catch (ParseException e) { throw new RuntimeException(e); } } }; }
小结:
- GET请求及POST表单方式请求,但是需要在每个使用的地方加上
@DateTimeFormat
注解。 - 与自定义参数转化器(Converter)不兼容。
- 支持LocalDate等Java8日期API。
使用@ControllerAdvice
配合@initBinder
/* * 在类上加上@ControllerAdvice */ @ControllerAdvice @SpringBootApplication @RestController public class SpringbootDateLearningApplication { ... @InitBinder protected void initBinder(WebDataBinder binder) { binder.registerCustomEditor(LocalDate.class, new PropertyEditorSupport() { @Override public void setAsText(String text) throws IllegalArgumentException { setValue(LocalDate.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN))); } }); binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() { @Override public void setAsText(String text) throws IllegalArgumentException { setValue(LocalDateTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN))); } }); binder.registerCustomEditor(LocalTime.class, new PropertyEditorSupport() { @Override public void setAsText(String text) throws IllegalArgumentException { setValue(LocalTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN))); } }); binder.registerCustomEditor(Date.class, new PropertyEditorSupport() { @Override public void setAsText(String text) throws IllegalArgumentException { SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN); try { setValue(formatter.parse(text)); } catch (Exception e) { throw new RuntimeException(String.format("Error parsing %s to Date", text)); } } }); } ... }
在实际应用中,我们可以把上面代码放到父类中,所有接口继承这个父类,达到全局处理的效果。原理就是与AOP类似,在参数进入handler之前进行转换时使用我们定义的PropertyEditorSupport
来处理。
小结:
- GET请求及POST表单方式请求。
- 支持LocalDate等Java8日期API。
- 局部差异化处理
假设按照前面的全局日期格式设置的是:yyyy-MM-dd HH:mm:ss
,但是某个Date
类型的字段需要特殊处理成yyyy/MM/dd
格式来接收或者返回,有以下方案可以选择。
使用@DateTimeFormat
和@JsonFormat
注解
@JsonFormat(pattern = "yyyy/MM/dd", timezone = "GMT+8") @DateTimeFormat(pattern = "yyyy/MM/dd HH:mm:ss") private Date originalDate;
如上所示,可以在字段上增加@DateTimeFormat
和@JsonFormat
注解,可以分别单独指定该字段的接收和返回的日期格式。
PS:@JsonFormat
和@DateTimeFormat
注解都不是Spring Boot提供的,在Spring应用中也可以使用。
再次提醒,如果使用了自定义参数转化器(Converter),Spring会优先使用该方式进行处理,即@DateTimeFormat
注解不生效。
自定义序列化器和反序列化器
/** * {@link Date} 序列化器 */ public class DateJsonSerializer extends JsonSerializer<Date> { @Override public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); jsonGenerator.writeString(dateFormat.format(date)); } } /** * {@link Date} 反序列化器 */ public class DateJsonDeserializer extends JsonDeserializer<Date> { @Override public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { try { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); return dateFormat.parse(jsonParser.getText()); } catch (ParseException e) { throw new IOException(e); } } } /** * 使用方式 */ @JsonSerialize(using = DateJsonSerializer.class) @JsonDeserialize(using = DateJsonDeserializer.class) private Date originalDate;
如上所示,可以在字段上使用@JsonSerialize
和@JsonDeserialize
注解来指定在序列化和反序列化时使用我们自定义的序列化器和反序列化器。
最后再来个兼容JSON方式和GET请求及POST表单方式的完整的配置吧。
@Configuration public class GlobalDateTimeConfig { /** * 日期正则表达式 */ private static final String DATE_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])"; /** * 时间正则表达式 */ private static final String TIME_REGEX = "(20|21|22|23|[0-1]\\d):[0-5]\\d:[0-5]\\d"; /** * 日期和时间正则表达式 */ private static final String DATE_TIME_REGEX = DATE_REGEX + "\\s" + TIME_REGEX; /** * 13位时间戳正则表达式 */ private static final String TIME_STAMP_REGEX = "1\\d{12}"; /** * 年和月正则表达式 */ private static final String YEAR_MONTH_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])"; /** * 年和月格式 */ private static final String YEAR_MONTH_PATTERN = "yyyy-MM"; /** * DateTime格式化字符串 */ private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; /** * Date格式化字符串 */ private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; /** * Time格式化字符串 */ private static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; /** * LocalDate转换器,用于转换RequestParam和PathVariable参数 */ @Bean public Converter<String, LocalDate> localDateConverter() { return new Converter<String, LocalDate>() { @SuppressWarnings("NullableProblems") @Override public LocalDate convert(String source) { if (StringUtils.isEmpty(source)) { return null; } return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)); } }; } /** * LocalDateTime转换器,用于转换RequestParam和PathVariable参数 */ @Bean public Converter<String, LocalDateTime> localDateTimeConverter() { return new Converter<String, LocalDateTime>() { @SuppressWarnings("NullableProblems") @Override public LocalDateTime convert(String source) { if (StringUtils.isEmpty(source)) { return null; } return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)); } }; } /** * LocalDate转换器,用于转换RequestParam和PathVariable参数 */ @Bean public Converter<String, LocalTime> localTimeConverter() { return new Converter<String, LocalTime>() { @SuppressWarnings("NullableProblems") @Override public LocalTime convert(String source) { if (StringUtils.isEmpty(source)) { return null; } return LocalTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)); } }; } /** * Date转换器,用于转换RequestParam和PathVariable参数 */ @Bean public Converter<String, Date> dateConverter() { return new Converter<String, Date>() { @SuppressWarnings("NullableProblems") @Override public Date convert(String source) { if (StringUtils.isEmpty(source)) { return null; } if (source.matches(TIME_STAMP_REGEX)) { return new Date(Long.parseLong(source)); } DateFormat format; if (source.matches(DATE_TIME_REGEX)) { format = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN); } else if (source.matches(DATE_REGEX)) { format = new SimpleDateFormat(DEFAULT_DATE_FORMAT); } else if (source.matches(YEAR_MONTH_REGEX)) { format = new SimpleDateFormat(YEAR_MONTH_PATTERN); } else { throw new IllegalArgumentException(); } try { return format.parse(source); } catch (ParseException e) { throw new RuntimeException(e); } } }; } /** * Json序列化和反序列化转换器,用于转换Post请求体中的json以及将我们的对象序列化为返回响应的json */ @Bean public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { return builder -> builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN))) .serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))) .serializerByType(Long.class, ToStringSerializer.instance) .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN))) .deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))); } }
源码剖析
在了解完怎么样进行全局设置后,接下来我们通过debug源码来深入剖析一下Spring MVC是如何进行参数绑定的。
仍然是以上面的controller为例进行debug。
@RequestMapping("/date") public DateEntity getDate( LocalDate date, LocalDateTime dateTime, Date originalDate, DateEntity dateEntity) { System.out.printf("date=%s, dateTime=%s, originalDate=%s \n", date, dateTime, originalDate); return dateEntity; }
以下是收到请求后的方法调用栈的一些关键方法:
// DispatcherServlet处理请求 doService:943, DispatcherServlet // 处理请求 doDispatch:1040, DispatcherServlet // 生成调用链(前处理、实际调用方法、后处理) handle:87, AbstractHandlerMethodAdapter handleInternal:793, RequestMappingHandlerAdapter // 反射获取到实际调用方法,准备开始调用 invokeHandlerMethod:879, RequestMappingHandlerAdapter invokeAndHandle:105, ServletInvocableHandlerMethod // 关键步骤,从这里开始处理请求参数 invokeForRequest:134, InvocableHandlerMethod getMethodArgumentValues:167, InvocableHandlerMethod resolveArgument:121, HandlerMethodArgumentResolverComposite
下面我们从关键的invokeForRequest:134, InvocableHandlerMethod
处开始分析,源码如下
// InvocableHandlerMethod.java @Nullable public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { // 这里完成参数的转换,得到的是转换后的值 Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); if (logger.isTraceEnabled()) { logger.trace("Arguments: " + Arrays.toString(args)); } // 反射调用,真正开始执行方法 return doInvoke(args); } // 具体实现 protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { // 获取当前handler method的方法参数数组,封装了入参信息,比如类型、泛型等 MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } // 该数组用来存放从MethodParameter转换后的结果 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; } // resolvers是定义的成员变量,HandlerMethodArgumentResolverComposite类型,是各式各样的HandlerMethodArgumentResolver的集合。这里来判断一下是否存在支持当前方法参数的参数处理器 if (!this.resolvers.supportsParameter(parameter)) { throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver")); } try { // 调用HandlerMethodArgumentResolverComposite来处理参数,下面会重点看一下内部的逻辑 args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); } catch (Exception ex) { ...... } } return args; }
下面需要进入HandlerMethodArgumentResolverComposite#resolveArgument
方法源码里面。
// HandlerMethodArgumentResolverComposite.java @Override @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { // 这里来获取匹配当前方法参数的参数解析器 HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); if (resolver == null) { throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first."); } // 调用真正的参数解析器来处理参数并返回 return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); } // 获取匹配当前方法参数的参数解析器 @Nullable private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { // 首先从缓存中查询是否有适配当前方法参数的参数解析器,首次进入是没有的 HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); if (result == null) { for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { // 逐个遍历argumentResolvers这个list里的参数解析器来判断是否支持 if (resolver.supportsParameter(parameter)) { result = resolver; this.argumentResolverCache.put(parameter, result); break; } } } return result; }
argumentResolvers
里一共有26个参数解析器,下面罗列一下常见的。
this.argumentResolvers = {LinkedList@6072} size = 26 0 = {RequestParamMethodArgumentResolver@6098} 1 = {RequestParamMapMethodArgumentResolver@6104} 2 = {PathVariableMethodArgumentResolver@6111} 3 = {PathVariableMapMethodArgumentResolver@6112} ...... 7 = {RequestResponseBodyMethodProcessor@6116} 8 = {RequestPartMethodArgumentResolver@6117} 9 = {RequestHeaderMethodArgumentResolver@6118} 10 = {RequestHeaderMapMethodArgumentResolver@6119} ...... 14 = {RequestAttributeMethodArgumentResolver@6123} 15 = {ServletRequestMethodArgumentResolver@6124} ...... 24 = {RequestParamMethodArgumentResolver@6107} 25 = {ServletModelAttributeMethodProcessor@6133}
所有的参数解析器都实现了HandlerMethodArgumentResolver
接口。
public interface HandlerMethodArgumentResolver { // 上面用到用来判断当前参数解析器是否支持给定的方法参数 boolean supportsParameter(MethodParameter parameter); // 解析给定的方法参数并返回 @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; }
到这里我们整理一下思路,对方法参数的解析都是通过逐个遍历找到合适的HandlerMethodArgumentResolver
来完成的。比如,如果参数上标注了@RequestParam
或者@RequestBody
或者@PathVariable
注解,SpringMVC会用不同的参数解析器来解析。下面挑一个最常用的RequestParamMethodArgumentResolver
来深入分析一下详细的解析流程。
RequestParamMethodArgumentResolver
继承自AbstractNamedValueMethodArgumentResolver
,AbstractNamedValueMethodArgumentResolver
实现了HandlerMethodArgumentResolver
接口的resolveArgument
方法。
// AbstractNamedValueMethodArgumentResolver.java @Override @Nullable public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { // 解析出传入的原始值,作为下面方法的参数 Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); ...... if (binderFactory != null) { // 创建 DataBinder WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); try { // 通过DataBinder进行参数绑定,参数列表:原始值,目标类型,方法参数 arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); } ...... } handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest); return arg; } // DataBinder.java @Override @Nullable public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType, @Nullable MethodParameter methodParam) throws TypeMismatchException { // 调用子类的convertIfNecessary方法,这里的具体实现是TypeConverterSupport return getTypeConverter().convertIfNecessary(value, requiredType, methodParam); } // TypeConverterSupport.java @Override @Nullable public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType, @Nullable MethodParameter methodParam) throws TypeMismatchException { // 调用重载的convertIfNecessary方法,通过MethodParameter构造了类型描述符TypeDescriptor return convertIfNecessary(value, requiredType, (methodParam != null ? new TypeDescriptor(methodParam) : TypeDescriptor.valueOf(requiredType))); } // convertIfNecessary方法 @Nullable @Override public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException { Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate"); try { // 调用TypeConverterDelegate的convertIfNecessary方法 return this.typeConverterDelegate.convertIfNecessary(null, null, value, requiredType, typeDescriptor); } ...... }
接下来进入TypeConverterDelegate
的源码。
// TypeConverterDelegate.java @Nullable public <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException { // 查找是否有适合需求类型的自定义的PropertyEditor。还记得上面的 使用@ControllerAdvice配合@initBinder 那一节吗,如果有按那样配置,这里就会找到 PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName); ConversionFailedException conversionAttemptEx = null; // 查找到类型转换服务 ConversionService ConversionService conversionService = this.propertyEditorRegistry.getConversionService(); // 关键判断,如果没有PropertyEditor 就使用ConversionService if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) { TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue); if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) { try { // #1,类型转换服务转换完成后就返回,下面会详细解释 return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor); } catch (ConversionFailedException ex) { // fallback to default conversion logic below conversionAttemptEx = ex; } } } Object convertedValue = newValue; // 关键判断,如果有PropertyEditor就使用PropertyEditor if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) { ...... // 由editor完成转换 convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor); } boolean standardConversion = false; if (requiredType != null) { // Try to apply some standard type conversion rules if appropriate. if (convertedValue != null) { if (Object.class == requiredType) { return (T) convertedValue; } // 下面是数组、集合类型属性的处理,这里会遍历集合元素,递归调用convertIfNecessary转化,再收集处理结果 else if (requiredType.isArray()) { // Array required -> apply appropriate conversion of elements. if (convertedValue instanceof String && Enum.class.isAssignableFrom(requiredType.getComponentType())) { convertedValue = StringUtils.commaDelimitedListToStringArray((String) convertedValue); } return (T) convertToTypedArray(convertedValue, propertyName, requiredType.getComponentType()); } else if (convertedValue instanceof Collection) { // Convert elements to target type, if determined. convertedValue = convertToTypedCollection( (Collection<?>) convertedValue, propertyName, requiredType, typeDescriptor); standardConversion = true; } else if (convertedValue instanceof Map) { // Convert keys and values to respective target type, if determined. convertedValue = convertToTypedMap( (Map<?, ?>) convertedValue, propertyName, requiredType, typeDescriptor); standardConversion = true; } if (convertedValue.getClass().isArray() && Array.getLength(convertedValue) == 1) { convertedValue = Array.get(convertedValue, 0); standardConversion = true; } if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) { // We can stringify any primitive value... return (T) convertedValue.toString(); } else if (convertedValue instanceof String && !requiredType.isInstance(convertedValue)) { ...... } else if (convertedValue instanceof Number && Number.class.isAssignableFrom(requiredType)) { convertedValue = NumberUtils.convertNumberToTargetClass( (Number) convertedValue, (Class<Number>) requiredType); standardConversion = true; } } else { // convertedValue == null,空值处理 if (requiredType == Optional.class) { convertedValue = Optional.empty(); } } ...... } // 异常处理 if (conversionAttemptEx != null) { if (editor == null && !standardConversion && requiredType != null && Object.class != requiredType) { throw conversionAttemptEx; } logger.debug("Original ConversionService attempt failed - ignored since " + "PropertyEditor based conversion eventually succeeded", conversionAttemptEx); } return (T) convertedValue; }
假如我们配置了自定义的Converter
,会进入#1的分支,由ConversionService
进行类型转换,以其子类GenericConversionService
为例。
// GenericConversionService.java @Override @Nullable public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { ...... // 从缓存中找到匹配类型的conveter,以LocalDateTime为例,会找到我们自定义的localDateTimeConverter GenericConverter converter = getConverter(sourceType, targetType); if (converter != null) { // 通过工具方法调用真正的converter完成类型转换。至此,完成了源类型到目标类型的转换 Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType); return handleResult(sourceType, targetType, result); } return handleConverterNotFound(source, sourceType, targetType); }
以上就是处理标注@RequestParam
注解的参数的RequestParamMethodArgumentResolver
解析流程。
下面来看一下处理标注@RequestBody
注解的参数的RequestResponseBodyMethodProcessor
的解析流程,仍然是从resolveArgument
方法切入。
// RequestResponseBodyMethodProcessor.java @Override public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { parameter = parameter.nestedIfOptional(); // 在这里完成参数的解析 Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); ...... return adaptArgumentIfNecessary(arg, parameter); } @Override protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter, Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class); Assert.state(servletRequest != null, "No HttpServletRequest"); ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest); // 调用父类AbstractMessageConverterMethodArgumentResolver完成参数解析 Object arg = readWithMessageConverters(inputMessage, parameter, paramType); if (arg == null && checkRequired(parameter)) { throw new HttpMessageNotReadableException("Required request body is missing: " + parameter.getExecutable().toGenericString(), inputMessage); } return arg; }
下面进入父类AbstractMessageConverterMethodArgumentResolver
的源码。
// AbstractMessageConverterMethodArgumentResolver.java @Nullable protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { ...... EmptyBodyCheckingHttpInputMessage message; try { message = new EmptyBodyCheckingHttpInputMessage(inputMessage); // 遍历HttpMessageConverter for (HttpMessageConverter<?> converter : this.messageConverters) { Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass(); GenericHttpMessageConverter<?> genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null); if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) : (targetClass != null && converter.canRead(targetClass, contentType))) { if (message.hasBody()) { HttpInputMessage msgToUse = getAdvice().beforeBodyRead(message, parameter, targetType, converterType); // 实际由MappingJackson2HttpMessageConverter调用父类AbstractJackson2HttpMessageConverter的read方法完成解析, body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) : ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse)); body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType); } else { body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType); } break; } } } ...... return body; } // AbstractJackson2HttpMessageConverter.java @Override public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { // 获得要转换的目标参数Java类型,如LocalDateTime等 JavaType javaType = getJavaType(type, contextClass); // 调用本类的readJavaType方法 return readJavaType(javaType, inputMessage); } // AbstractJackson2HttpMessageConverter.java private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException { try { if (inputMessage instanceof MappingJacksonInputMessage) { Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView(); if (deserializationView != null) { return this.objectMapper.readerWithView(deserializationView).forType(javaType). readValue(inputMessage.getBody()); } } // 调用jackson类库,将HTTP的json请求信息解析为需要的参数类型。至此,将json请求转换成目标Java类型 return this.objectMapper.readValue(inputMessage.getBody(), javaType); } ...... }
总结
controller方法的参数是通过不同的HandlerMethodArgumentResolver
完成解析的。如果参数标注了@RequestBody
注解,实际上是通过MappingJackson2HttpMessageConverter
的ObjectMapper
将传入json格式数据反序列化解析成目标类型的。如果标注了@RequestParam
注解,是通过在应用初始化时注入到ConversionService
的一个个Converter
来实现的。其他的HandlerMethodArgumentResolver
也是各有各的用处,大家可以再看看相关代码,以便加深理解。
到此这篇关于在Spring Boot应用中优雅的使用Date和LocalDateTime的教程详解的文章就介绍到这了,更多相关Spring Boot使用Date和LocalDateTime内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
相关文章
Spring中@ConfigurationProperties的用法解析
这篇文章主要介绍了Spring中@ConfigurationProperties的用法解析,传统的Spring一般都是基本xml配置的,后来spring3.0新增了许多java config的注解,特别是spring boot,基本都是清一色的java config,需要的朋友可以参考下2023-11-11使用HttpServletResponse对象获取请求行信息
这篇文章主要介绍了使用HttpServletResponse对象获取请求行信息,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2022-02-02springboot新建项目jdk只有17/21,无法选中1.8解决办法
最近博主也有创建springboot项目,发现了IntelliJ IDEA在通过Spring Initilizer初始化项目的时候已经没有java8版本的选项了,这里给大家总结下,这篇文章主要给大家介绍了springboot新建项目jdk只有17/21,无法选中1.8的解决办法,需要的朋友可以参考下2023-12-12
最新评论