如何在Spring Boot應用中優(yōu)雅的使用Date和LocalDateTime的教程詳解
Java8已經(jīng)發(fā)布很多年了,但是很多人在開發(fā)時仍然堅持使用著Date和SimpleDateFormat進行時間操作。SimpleDateFormat不是線程安全的,而Date處理時間很麻煩,所以Java8提供了LocalDateTime、LocalDate和LocalTime等全新的時間操作API。無論是Date還是LocalDate,在開發(fā)Spring Boot應用時經(jīng)常需要在每個實體類的日期字段上加上@DateTimeFormat注解來接收前端傳值與日期字段綁定,加上@JsonFormat注解來讓返回前端的日期字段格式化成我們想要的時間格式。時間和日期類型在開發(fā)中使用的頻率是非常高的,如果每個字段都加上這兩個注解的話是非常繁瑣的,有沒有一種全局設置的處理方式呢?今天就來向大家介紹一下。
注:本文基于Springboot2.3.0版本。
根據(jù)不同的請求方式需要做不同的配置,下文中分為了JSON方式傳參和GET請求及POST表單方式傳參兩種情況。
JSON方式傳參
這種情況指的是類型POST,Content-Type 是application/json 方式的請求。對于這類請求,controller中需要加上@RequestBody注解來標注到我們用來接收請求參數(shù)的局部變量上,代碼如下:
@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請求,請求參數(shù)字符串和返回的格式都是
yyyy-MM-dd HH:mm:ss如果請求參數(shù)是其他格式,如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請求,請求參數(shù)字符串和返回的格式都是
yyyy-MM-dd HH:mm:ss如果請求參數(shù)是其他格式,如yyyy-MM-dd字符串則報400 Bad Request異常。 - 支持LocalDate等Java8日期API。
PS:上面的方式是通過配置一個Jackson2ObjectMapperBuilderCustomizerBean完成的,除了這種,也可以通過自定義一個MappingJackson2HttpMessageConverter來實現(xiàn)。
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
// 指定時區(qū)
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;
}
以上幾種方式都可以實現(xiàn)JSON傳參時的全局化配置,更推薦后兩種代碼中增加配置bean的方式,可以同時支持Date和LocalDate。
GET請求及POST表單方式傳參
這種方式和上面的JSON方式,在Spring Boot處理的方式是完全不同的。上一種JSON方式傳參是在HttpMessgeConverter中通過jackson的ObjectMapper將http請求體轉換成我們寫在controller中的參數(shù)對象的,而這種方式用的是Converter接口(spring-core中定義的用于將源類型(一般是String)轉成目標類型的接口),兩者是有本質區(qū)別的。
自定義參數(shù)轉換器(Converter)
自定義一個參數(shù)轉換器,實現(xiàn)上面提到的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接口增加一些參數(shù),可以發(fā)現(xià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;
}
小結:
- GET請求及POST表單方式請求。
- 支持LocalDate等Java8日期API。
使用@DateTimeFormat注解
和前面提到的一樣,GET請求及POST表單方式也是可以用@DateTimeFormat來處理的,單獨在controller接口參數(shù)或者實體類屬性中都可以使用,比如@DateTimeFormat(pattern = "yyyy-MM-dd") Date originalDate。注意,如果使用了自定義參數(shù)轉化器(Converter),Spring會優(yōu)先使用該方式進行處理,即@DateTimeFormat注解不生效,兩種方式是不兼容的。
那么假如我們使用了自定義參數(shù)轉換器,但是還是想兼容用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注解。 - 與自定義參數(shù)轉化器(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類似,在參數(shù)進入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應用中也可以使用。
再次提醒,如果使用了自定義參數(shù)轉化器(Converter),Spring會優(yōu)先使用該方式進行處理,即@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參數(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轉換器,用于轉換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轉換器,用于轉換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轉換器,用于轉換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序列化和反序列化轉換器,用于轉換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是如何進行參數(shù)綁定的。
仍然是以上面的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 // 關鍵步驟,從這里開始處理請求參數(shù) 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 {
// 這里完成參數(shù)的轉換,得到的是轉換后的值
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
// 反射調用,真正開始執(zhí)行方法
return doInvoke(args);
}
// 具體實現(xiàn)
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 獲取當前handler method的方法參數(shù)數(shù)組,封裝了入?yún)⑿畔?,比如類型、泛型?
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
// 該數(shù)組用來存放從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的集合。這里來判斷一下是否存在支持當前方法參數(shù)的參數(shù)處理器
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
// 調用HandlerMethodArgumentResolverComposite來處理參數(shù),下面會重點看一下內部的邏輯
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 {
// 這里來獲取匹配當前方法參數(shù)的參數(shù)解析器
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unsupported parameter type [" +
parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
}
// 調用真正的參數(shù)解析器來處理參數(shù)并返回
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
// 獲取匹配當前方法參數(shù)的參數(shù)解析器
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
// 首先從緩存中查詢是否有適配當前方法參數(shù)的參數(shù)解析器,首次進入是沒有的
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
// 逐個遍歷argumentResolvers這個list里的參數(shù)解析器來判斷是否支持
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
argumentResolvers里一共有26個參數(shù)解析器,下面羅列一下常見的。
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ù)解析器都實現(xiàn)了HandlerMethodArgumentResolver接口。
public interface HandlerMethodArgumentResolver {
// 上面用到用來判斷當前參數(shù)解析器是否支持給定的方法參數(shù)
boolean supportsParameter(MethodParameter parameter);
// 解析給定的方法參數(shù)并返回
@Nullable
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
到這里我們整理一下思路,對方法參數(shù)的解析都是通過逐個遍歷找到合適的HandlerMethodArgumentResolver來完成的。比如,如果參數(shù)上標注了@RequestParam或者@RequestBody或者@PathVariable注解,SpringMVC會用不同的參數(shù)解析器來解析。下面挑一個最常用的RequestParamMethodArgumentResolver來深入分析一下詳細的解析流程。
RequestParamMethodArgumentResolver繼承自AbstractNamedValueMethodArgumentResolver,AbstractNamedValueMethodArgumentResolver實現(xiàn)了HandlerMethodArgumentResolver接口的resolveArgument方法。
// AbstractNamedValueMethodArgumentResolver.java
@Override
@Nullable
public 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 {
// 通過DataBinder進行參數(shù)綁定,參數(shù)列表:原始值,目標類型,方法參數(shù)
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方法,這里的具體實現(xiàn)是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 那一節(jié)嗎,如果有按那樣配置,這里就會找到
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;
}
// 下面是數(shù)組、集合類型屬性的處理,這里會遍歷集合元素,遞歸調用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注解的參數(shù)的RequestParamMethodArgumentResolver解析流程。
下面來看一下處理標注@RequestBody注解的參數(shù)的RequestResponseBodyMethodProcessor的解析流程,仍然是從resolveArgument方法切入。
// RequestResponseBodyMethodProcessor.java
@Override
public 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);
}
@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完成參數(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;
}
下面進入父類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 {
// 獲得要轉換的目標參數(shù)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請求信息解析為需要的參數(shù)類型。至此,將json請求轉換成目標Java類型
return this.objectMapper.readValue(inputMessage.getBody(), javaType);
}
......
}
總結
controller方法的參數(shù)是通過不同的HandlerMethodArgumentResolver完成解析的。如果參數(shù)標注了@RequestBody注解,實際上是通過MappingJackson2HttpMessageConverter的ObjectMapper將傳入json格式數(shù)據(jù)反序列化解析成目標類型的。如果標注了@RequestParam注解,是通過在應用初始化時注入到ConversionService的一個個Converter來實現(xiàn)的。其他的HandlerMethodArgumentResolver也是各有各的用處,大家可以再看看相關代碼,以便加深理解。
到此這篇關于在Spring Boot應用中優(yōu)雅的使用Date和LocalDateTime的教程詳解的文章就介紹到這了,更多相關Spring Boot使用Date和LocalDateTime內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Spring中@ConfigurationProperties的用法解析
這篇文章主要介紹了Spring中@ConfigurationProperties的用法解析,傳統(tǒng)的Spring一般都是基本xml配置的,后來spring3.0新增了許多java config的注解,特別是spring boot,基本都是清一色的java config,需要的朋友可以參考下2023-11-11
使用HttpServletResponse對象獲取請求行信息
這篇文章主要介紹了使用HttpServletResponse對象獲取請求行信息,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02
springboot新建項目jdk只有17/21,無法選中1.8解決辦法
最近博主也有創(chuàng)建springboot項目,發(fā)現(xiàn)了IntelliJ IDEA在通過Spring Initilizer初始化項目的時候已經(jīng)沒有java8版本的選項了,這里給大家總結下,這篇文章主要給大家介紹了springboot新建項目jdk只有17/21,無法選中1.8的解決辦法,需要的朋友可以參考下2023-12-12

