如何在Spring Boot應(yīng)用中優(yōu)雅的使用Date和LocalDateTime的教程詳解
Java8已經(jīng)發(fā)布很多年了,但是很多人在開(kāi)發(fā)時(shí)仍然堅(jiān)持使用著Date和SimpleDateFormat進(jìn)行時(shí)間操作。SimpleDateFormat不是線(xiàn)程安全的,而Date處理時(shí)間很麻煩,所以Java8提供了LocalDateTime、LocalDate和LocalTime等全新的時(shí)間操作API。無(wú)論是Date還是LocalDate,在開(kāi)發(fā)Spring Boot應(yīng)用時(shí)經(jīng)常需要在每個(gè)實(shí)體類(lèi)的日期字段上加上@DateTimeFormat注解來(lái)接收前端傳值與日期字段綁定,加上@JsonFormat注解來(lái)讓返回前端的日期字段格式化成我們想要的時(shí)間格式。時(shí)間和日期類(lèi)型在開(kāi)發(fā)中使用的頻率是非常高的,如果每個(gè)字段都加上這兩個(gè)注解的話(huà)是非常繁瑣的,有沒(méi)有一種全局設(shè)置的處理方式呢?今天就來(lái)向大家介紹一下。
注:本文基于Springboot2.3.0版本。
根據(jù)不同的請(qǐng)求方式需要做不同的配置,下文中分為了JSON方式傳參和GET請(qǐng)求及POST表單方式傳參兩種情況。
JSON方式傳參
這種情況指的是類(lèi)型POST,Content-Type 是application/json 方式的請(qǐng)求。對(duì)于這類(lèi)請(qǐng)求,controller中需要加上@RequestBody注解來(lái)標(biāo)注到我們用來(lái)接收請(qǐng)求參數(shù)的局部變量上,代碼如下:
@SpringBootApplication@RestControllerpublic 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; }}
假設(shè)默認(rèn)的接收和返回值的格式都是yyyy-MM-dd HH:mm:ss,可以有以下幾個(gè)方案。
配置application.yml 文件
在application.yml文件中配置上如下內(nèi)容:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8
小結(jié):
支持Content-Type 是application/json的POST請(qǐng)求,請(qǐng)求參數(shù)字符串和返回的格式都是yyyy-MM-dd HH:mm:ss如果請(qǐng)求參數(shù)是其他格式,如yyyy-MM-dd字符串則報(bào)400 Bad Request異常。 不支持LocalDate等Java8日期API。增加Jackson配置
/** * Jackson序列化和反序列化轉(zhuǎn)換器,用于轉(zhuǎn)換Post請(qǐng)求體中的json以及將對(duì)象序列化為返回響應(yīng)的json */@Beanpublic 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)) ;}
小結(jié):
支持Content-Type 是application/json的POST請(qǐng)求,請(qǐng)求參數(shù)字符串和返回的格式都是yyyy-MM-dd HH:mm:ss如果請(qǐng)求參數(shù)是其他格式,如yyyy-MM-dd字符串則報(bào)400 Bad Request異常。 支持LocalDate等Java8日期API。PS:上面的方式是通過(guò)配置一個(gè)Jackson2ObjectMapperBuilderCustomizerBean完成的,除了這種,也可以通過(guò)自定義一個(gè)MappingJackson2HttpMessageConverter來(lái)實(shí)現(xiàn)。
@Beanpublic MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() { MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); ObjectMapper objectMapper = new ObjectMapper(); // 指定時(shí)區(qū) objectMapper.setTimeZone(TimeZone.getTimeZone('GMT+8:00')); // 日期類(lèi)型字符串處理 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;}
以上幾種方式都可以實(shí)現(xiàn)JSON傳參時(shí)的全局化配置,更推薦后兩種代碼中增加配置bean的方式,可以同時(shí)支持Date和LocalDate。
GET請(qǐng)求及POST表單方式傳參
這種方式和上面的JSON方式,在Spring Boot處理的方式是完全不同的。上一種JSON方式傳參是在HttpMessgeConverter中通過(guò)jackson的ObjectMapper將http請(qǐng)求體轉(zhuǎn)換成我們寫(xiě)在controller中的參數(shù)對(duì)象的,而這種方式用的是Converter接口(spring-core中定義的用于將源類(lèi)型(一般是String)轉(zhuǎn)成目標(biāo)類(lèi)型的接口),兩者是有本質(zhì)區(qū)別的。
自定義參數(shù)轉(zhuǎn)換器(Converter)
自定義一個(gè)參數(shù)轉(zhuǎn)換器,實(shí)現(xiàn)上面提到的org.springframework.core.convert.converter.Converter接口,在配置類(lèi)里配置上以下幾個(gè)bean,示例如下:
@Beanpublic 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)); } } };}@Beanpublic Converter<String, LocalDate> localDateConverter() { return new Converter<>() { @Override public LocalDate convert(String source) { return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)); } };}@Beanpublic Converter<String, LocalDateTime> localDateTimeConverter() { return new Converter<>() { @Override public LocalDateTime convert(String source) { return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)); } };}
同時(shí)把controller接口增加一些參數(shù),可以發(fā)現(xiàn)在接口里單獨(dú)用變量接收也是可以正常轉(zhuǎn)換的。
@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;}
小結(jié):
GET請(qǐng)求及POST表單方式請(qǐng)求。 支持LocalDate等Java8日期API。使用@DateTimeFormat注解
和前面提到的一樣,GET請(qǐng)求及POST表單方式也是可以用@DateTimeFormat來(lái)處理的,單獨(dú)在controller接口參數(shù)或者實(shí)體類(lèi)屬性中都可以使用,比如@DateTimeFormat(pattern = 'yyyy-MM-dd') Date originalDate。注意,如果使用了自定義參數(shù)轉(zhuǎn)化器(Converter),Spring會(huì)優(yōu)先使用該方式進(jìn)行處理,即@DateTimeFormat注解不生效,兩種方式是不兼容的。
那么假如我們使用了自定義參數(shù)轉(zhuǎn)換器,但是還是想兼容用yyyy-MM-dd形式接受呢?我們可以把前面的dateConverter改成用正則匹配方式,這樣也不失為一種不錯(cuò)的解決方案,示例如下。
/** * 日期正則表達(dá)式 */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])';/** * 時(shí)間正則表達(dá)式 */private static final String TIME_REGEX = '(20|21|22|23|[0-1]d):[0-5]d:[0-5]d';/** * 日期和時(shí)間正則表達(dá)式 */private static final String DATE_TIME_REGEX = DATE_REGEX + 's' + TIME_REGEX;/** * 13位時(shí)間戳正則表達(dá)式 */private static final String TIME_STAMP_REGEX = '1d{12}';/** * 年和月正則表達(dá)式 */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';@Beanpublic 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); } } };}
小結(jié):
GET請(qǐng)求及POST表單方式請(qǐng)求,但是需要在每個(gè)使用的地方加上@DateTimeFormat注解。 與自定義參數(shù)轉(zhuǎn)化器(Converter)不兼容。 支持LocalDate等Java8日期API。使用@ControllerAdvice配合@initBinder
/* * 在類(lèi)上加上@ControllerAdvice */@ControllerAdvice@SpringBootApplication@RestControllerpublic 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)); } } }); } ...}
在實(shí)際應(yīng)用中,我們可以把上面代碼放到父類(lèi)中,所有接口繼承這個(gè)父類(lèi),達(dá)到全局處理的效果。原理就是與AOP類(lèi)似,在參數(shù)進(jìn)入handler之前進(jìn)行轉(zhuǎn)換時(shí)使用我們定義的PropertyEditorSupport來(lái)處理。
小結(jié):
GET請(qǐng)求及POST表單方式請(qǐng)求。 支持LocalDate等Java8日期API。 局部差異化處理假設(shè)按照前面的全局日期格式設(shè)置的是:yyyy-MM-dd HH:mm:ss,但是某個(gè)Date類(lèi)型的字段需要特殊處理成yyyy/MM/dd格式來(lái)接收或者返回,有以下方案可以選擇。
使用@DateTimeFormat和@JsonFormat注解
@JsonFormat(pattern = 'yyyy/MM/dd', timezone = 'GMT+8')@DateTimeFormat(pattern = 'yyyy/MM/dd HH:mm:ss')private Date originalDate;
如上所示,可以在字段上增加@DateTimeFormat和@JsonFormat注解,可以分別單獨(dú)指定該字段的接收和返回的日期格式。
PS:@JsonFormat和@DateTimeFormat注解都不是Spring Boot提供的,在Spring應(yīng)用中也可以使用。
再次提醒,如果使用了自定義參數(shù)轉(zhuǎn)化器(Converter),Spring會(huì)優(yōu)先使用該方式進(jìn)行處理,即@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注解來(lái)指定在序列化和反序列化時(shí)使用我們自定義的序列化器和反序列化器。
最后再來(lái)個(gè)兼容JSON方式和GET請(qǐng)求及POST表單方式的完整的配置吧。
@Configurationpublic class GlobalDateTimeConfig { /** * 日期正則表達(dá)式 */ 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])'; /** * 時(shí)間正則表達(dá)式 */ private static final String TIME_REGEX = '(20|21|22|23|[0-1]d):[0-5]d:[0-5]d'; /** * 日期和時(shí)間正則表達(dá)式 */ private static final String DATE_TIME_REGEX = DATE_REGEX + 's' + TIME_REGEX; /** * 13位時(shí)間戳正則表達(dá)式 */ private static final String TIME_STAMP_REGEX = '1d{12}'; /** * 年和月正則表達(dá)式 */ 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轉(zhuǎn)換器,用于轉(zhuǎn)換RequestParam和PathVariable參數(shù) */ @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轉(zhuǎn)換器,用于轉(zhuǎn)換RequestParam和PathVariable參數(shù) */ @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轉(zhuǎn)換器,用于轉(zhuǎn)換RequestParam和PathVariable參數(shù) */ @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轉(zhuǎn)換器,用于轉(zhuǎn)換RequestParam和PathVariable參數(shù) */ @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序列化和反序列化轉(zhuǎn)換器,用于轉(zhuǎn)換Post請(qǐng)求體中的json以及將我們的對(duì)象序列化為返回響應(yīng)的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))); }}
源碼剖析
在了解完怎么樣進(jìn)行全局設(shè)置后,接下來(lái)我們通過(guò)debug源碼來(lái)深入剖析一下Spring MVC是如何進(jìn)行參數(shù)綁定的。
仍然是以上面的controller為例進(jìn)行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;}
以下是收到請(qǐng)求后的方法調(diào)用棧的一些關(guān)鍵方法:
// DispatcherServlet處理請(qǐng)求doService:943, DispatcherServlet// 處理請(qǐng)求doDispatch:1040, DispatcherServlet// 生成調(diào)用鏈(前處理、實(shí)際調(diào)用方法、后處理)handle:87, AbstractHandlerMethodAdapterhandleInternal:793, RequestMappingHandlerAdapter// 反射獲取到實(shí)際調(diào)用方法,準(zhǔn)備開(kāi)始調(diào)用invokeHandlerMethod:879, RequestMappingHandlerAdapterinvokeAndHandle:105, ServletInvocableHandlerMethod // 關(guān)鍵步驟,從這里開(kāi)始處理請(qǐng)求參數(shù)invokeForRequest:134, InvocableHandlerMethodgetMethodArgumentValues:167, InvocableHandlerMethodresolveArgument:121, HandlerMethodArgumentResolverComposite
下面我們從關(guān)鍵的invokeForRequest:134, InvocableHandlerMethod處開(kāi)始分析,源碼如下
// InvocableHandlerMethod.java@Nullablepublic Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { // 這里完成參數(shù)的轉(zhuǎn)換,得到的是轉(zhuǎn)換后的值 Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); if (logger.isTraceEnabled()) { logger.trace('Arguments: ' + Arrays.toString(args)); } // 反射調(diào)用,真正開(kāi)始執(zhí)行方法 return doInvoke(args);}// 具體實(shí)現(xiàn)protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { // 獲取當(dāng)前handler method的方法參數(shù)數(shù)組,封裝了入?yún)⑿畔ⅲ热珙?lèi)型、泛型等 MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } // 該數(shù)組用來(lái)存放從MethodParameter轉(zhuǎn)換后的結(jié)果 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類(lèi)型,是各式各樣的HandlerMethodArgumentResolver的集合。這里來(lái)判斷一下是否存在支持當(dāng)前方法參數(shù)的參數(shù)處理器 if (!this.resolvers.supportsParameter(parameter)) { throw new IllegalStateException(formatArgumentError(parameter, 'No suitable resolver')); } try { // 調(diào)用HandlerMethodArgumentResolverComposite來(lái)處理參數(shù),下面會(huì)重點(diǎn)看一下內(nèi)部的邏輯 args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); } catch (Exception ex) { ...... } } return args;}
下面需要進(jìn)入HandlerMethodArgumentResolverComposite#resolveArgument方法源碼里面。
// HandlerMethodArgumentResolverComposite.java@Override@Nullablepublic Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { // 這里來(lái)獲取匹配當(dāng)前方法參數(shù)的參數(shù)解析器 HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); if (resolver == null) { throw new IllegalArgumentException('Unsupported parameter type [' + parameter.getParameterType().getName() + ']. supportsParameter should be called first.'); } // 調(diào)用真正的參數(shù)解析器來(lái)處理參數(shù)并返回 return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);}// 獲取匹配當(dāng)前方法參數(shù)的參數(shù)解析器@Nullableprivate HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { // 首先從緩存中查詢(xún)是否有適配當(dāng)前方法參數(shù)的參數(shù)解析器,首次進(jìn)入是沒(méi)有的 HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); if (result == null) { for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { // 逐個(gè)遍歷argumentResolvers這個(gè)list里的參數(shù)解析器來(lái)判斷是否支持 if (resolver.supportsParameter(parameter)) { result = resolver; this.argumentResolverCache.put(parameter, result); break; } } } return result;}
argumentResolvers里一共有26個(gè)參數(shù)解析器,下面羅列一下常見(jiàn)的。
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}
所有的參數(shù)解析器都實(shí)現(xiàn)了HandlerMethodArgumentResolver接口。
public interface HandlerMethodArgumentResolver { // 上面用到用來(lái)判斷當(dāng)前參數(shù)解析器是否支持給定的方法參數(shù) boolean supportsParameter(MethodParameter parameter); // 解析給定的方法參數(shù)并返回 @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;}
到這里我們整理一下思路,對(duì)方法參數(shù)的解析都是通過(guò)逐個(gè)遍歷找到合適的HandlerMethodArgumentResolver來(lái)完成的。比如,如果參數(shù)上標(biāo)注了@RequestParam或者@RequestBody或者@PathVariable注解,SpringMVC會(huì)用不同的參數(shù)解析器來(lái)解析。下面挑一個(gè)最常用的RequestParamMethodArgumentResolver來(lái)深入分析一下詳細(xì)的解析流程。
RequestParamMethodArgumentResolver繼承自AbstractNamedValueMethodArgumentResolver,AbstractNamedValueMethodArgumentResolver實(shí)現(xiàn)了HandlerMethodArgumentResolver接口的resolveArgument方法。
// AbstractNamedValueMethodArgumentResolver.java@Override@Nullablepublic final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { // 解析出傳入的原始值,作為下面方法的參數(shù) Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); ...... if (binderFactory != null) { // 創(chuàng)建 DataBinder WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); try { // 通過(guò)DataBinder進(jìn)行參數(shù)綁定,參數(shù)列表:原始值,目標(biāo)類(lèi)型,方法參數(shù) arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); } ...... } handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest); return arg;}// DataBinder.java@Override@Nullablepublic <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType, @Nullable MethodParameter methodParam) throws TypeMismatchException { // 調(diào)用子類(lèi)的convertIfNecessary方法,這里的具體實(shí)現(xiàn)是TypeConverterSupport return getTypeConverter().convertIfNecessary(value, requiredType, methodParam);}// TypeConverterSupport.java@Override@Nullablepublic <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType, @Nullable MethodParameter methodParam) throws TypeMismatchException { // 調(diào)用重載的convertIfNecessary方法,通過(guò)MethodParameter構(gòu)造了類(lèi)型描述符TypeDescriptor return convertIfNecessary(value, requiredType, (methodParam != null ? new TypeDescriptor(methodParam) : TypeDescriptor.valueOf(requiredType)));}// convertIfNecessary方法@Nullable@Overridepublic <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException { Assert.state(this.typeConverterDelegate != null, 'No TypeConverterDelegate'); try { // 調(diào)用TypeConverterDelegate的convertIfNecessary方法 return this.typeConverterDelegate.convertIfNecessary(null, null, value, requiredType, typeDescriptor); } ......}
接下來(lái)進(jìn)入TypeConverterDelegate的源碼。
// TypeConverterDelegate.java@Nullablepublic <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException { // 查找是否有適合需求類(lèi)型的自定義的PropertyEditor。還記得上面的 使用@ControllerAdvice配合@initBinder 那一節(jié)嗎,如果有按那樣配置,這里就會(huì)找到 PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName); ConversionFailedException conversionAttemptEx = null; // 查找到類(lèi)型轉(zhuǎn)換服務(wù) ConversionService ConversionService conversionService = this.propertyEditorRegistry.getConversionService(); // 關(guān)鍵判斷,如果沒(méi)有PropertyEditor 就使用ConversionService if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) { TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue); if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) { try { // #1,類(lèi)型轉(zhuǎn)換服務(wù)轉(zhuǎn)換完成后就返回,下面會(huì)詳細(xì)解釋 return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor); } catch (ConversionFailedException ex) { // fallback to default conversion logic below conversionAttemptEx = ex; } } } Object convertedValue = newValue; // 關(guān)鍵判斷,如果有PropertyEditor就使用PropertyEditor if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) { ...... // 由editor完成轉(zhuǎn)換 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; } // 下面是數(shù)組、集合類(lèi)型屬性的處理,這里會(huì)遍歷集合元素,遞歸調(diào)用convertIfNecessary轉(zhuǎn)化,再收集處理結(jié)果 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,會(huì)進(jìn)入#1的分支,由ConversionService進(jìn)行類(lèi)型轉(zhuǎn)換,以其子類(lèi)GenericConversionService為例。
// GenericConversionService.java@Override@Nullablepublic Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { ...... // 從緩存中找到匹配類(lèi)型的conveter,以L(fǎng)ocalDateTime為例,會(huì)找到我們自定義的localDateTimeConverter GenericConverter converter = getConverter(sourceType, targetType); if (converter != null) { // 通過(guò)工具方法調(diào)用真正的converter完成類(lèi)型轉(zhuǎn)換。至此,完成了源類(lèi)型到目標(biāo)類(lèi)型的轉(zhuǎn)換 Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType); return handleResult(sourceType, targetType, result); } return handleConverterNotFound(source, sourceType, targetType);}
以上就是處理標(biāo)注@RequestParam注解的參數(shù)的RequestParamMethodArgumentResolver解析流程。
下面來(lái)看一下處理標(biāo)注@RequestBody注解的參數(shù)的RequestResponseBodyMethodProcessor的解析流程,仍然是從resolveArgument方法切入。
// RequestResponseBodyMethodProcessor.java@Overridepublic Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { parameter = parameter.nestedIfOptional(); // 在這里完成參數(shù)的解析 Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType()); ...... return adaptArgumentIfNecessary(arg, parameter);}@Overrideprotected <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); // 調(diào)用父類(lèi)AbstractMessageConverterMethodArgumentResolver完成參數(shù)解析 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;}
下面進(jìn)入父類(lèi)AbstractMessageConverterMethodArgumentResolver的源碼。
// AbstractMessageConverterMethodArgumentResolver.java@Nullableprotected <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); // 實(shí)際由MappingJackson2HttpMessageConverter調(diào)用父類(lèi)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@Overridepublic Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { // 獲得要轉(zhuǎn)換的目標(biāo)參數(shù)Java類(lèi)型,如LocalDateTime等 JavaType javaType = getJavaType(type, contextClass); // 調(diào)用本類(lèi)的readJavaType方法 return readJavaType(javaType, inputMessage);}// AbstractJackson2HttpMessageConverter.javaprivate 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()); } } // 調(diào)用jackson類(lèi)庫(kù),將HTTP的json請(qǐng)求信息解析為需要的參數(shù)類(lèi)型。至此,將json請(qǐng)求轉(zhuǎn)換成目標(biāo)Java類(lèi)型 return this.objectMapper.readValue(inputMessage.getBody(), javaType); } ......}
總結(jié)
controller方法的參數(shù)是通過(guò)不同的HandlerMethodArgumentResolver完成解析的。如果參數(shù)標(biāo)注了@RequestBody注解,實(shí)際上是通過(guò)MappingJackson2HttpMessageConverter的ObjectMapper將傳入json格式數(shù)據(jù)反序列化解析成目標(biāo)類(lèi)型的。如果標(biāo)注了@RequestParam注解,是通過(guò)在應(yīng)用初始化時(shí)注入到ConversionService的一個(gè)個(gè)Converter來(lái)實(shí)現(xiàn)的。其他的HandlerMethodArgumentResolver也是各有各的用處,大家可以再看看相關(guān)代碼,以便加深理解。
到此這篇關(guān)于在Spring Boot應(yīng)用中優(yōu)雅的使用Date和LocalDateTime的教程詳解的文章就介紹到這了,更多相關(guān)Spring Boot使用Date和LocalDateTime內(nèi)容請(qǐng)搜索好吧啦網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持好吧啦網(wǎng)!
相關(guān)文章:
1. 利用promise及參數(shù)解構(gòu)封裝ajax請(qǐng)求的方法2. JSP數(shù)據(jù)交互實(shí)現(xiàn)過(guò)程解析3. windows服務(wù)器使用IIS時(shí)thinkphp搜索中文無(wú)效問(wèn)題4. .NET中l(wèi)ambda表達(dá)式合并問(wèn)題及解決方法5. Nginx+php配置文件及原理解析6. 淺談python出錯(cuò)時(shí)traceback的解讀7. ASP 信息提示函數(shù)并作返回或者轉(zhuǎn)向8. Ajax實(shí)現(xiàn)表格中信息不刷新頁(yè)面進(jìn)行更新數(shù)據(jù)9. Python importlib動(dòng)態(tài)導(dǎo)入模塊實(shí)現(xiàn)代碼10. python matplotlib:plt.scatter() 大小和顏色參數(shù)詳解
