如何在Spring Boot应用中优雅的使用Date和LocalDateTime的教程详解
Java8已经发布很多年了,但是很多人在开发时仍然坚持使用着Date和SimpleDateFormat进行时间操作。SimpleDateFormat不是线程安全的,而Date处理时间很麻烦,所以Java8提供了LocalDateTime、LocalDate和LocalTime等全新的时间操作API。无论是Date还是LocalDate,在开发SpringBoot应用时经常需要在每个实体类的日期字段上加上@DateTimeFormat注解来接收前端传值与日期字段绑定,加上@JsonFormat注解来让返回前端的日期字段格式化成我们想要的时间格式。时间和日期类型在开发中使用的频率是非常高的,如果每个字段都加上这两个注解的话是非常繁琐的,有没有一种全局设置的处理方式呢?今天就来向大家介绍一下。
注:本文基于Springboot2.3.0版本。
根据不同的请求方式需要做不同的配置,下文中分为了JSON方式传参和GET请求及POST表单方式传参两种情况。
JSON方式传参
这种情况指的是类型POST,Content-Type是application/json方式的请求。对于这类请求,controller中需要加上@RequestBody注解来标注到我们用来接收请求参数的局部变量上,代码如下:
@SpringBootApplication @RestController publicclassSpringbootDateLearningApplication{ publicstaticvoidmain(String[]args){ SpringApplication.run(SpringbootDateLearningApplication.class,args); } /** *DateTime格式化字符串 */ privatestaticfinalStringDEFAULT_DATETIME_PATTERN="yyyy-MM-ddHH:mm:ss"; /** *Date格式化字符串 */ privatestaticfinalStringDEFAULT_DATE_PATTERN="yyyy-MM-dd"; /** *Time格式化字符串 */ privatestaticfinalStringDEFAULT_TIME_PATTERN="HH:mm:ss"; publicstaticclassDateEntity{ privateLocalDatedate; privateLocalDateTimedateTime; privateDateoriginalDate; publicLocalDategetDate(){ returndate; } publicvoidsetDate(LocalDatedate){ this.date=date; } publicLocalDateTimegetDateTime(){ returndateTime; } publicvoidsetDateTime(LocalDateTimedateTime){ this.dateTime=dateTime; } publicDategetOriginalDate(){ returnoriginalDate; } publicvoidsetOriginalDate(DateoriginalDate){ this.originalDate=originalDate; } } @RequestMapping("/date") publicDateEntitygetDate(@RequestBodyDateEntitydateEntity){ returndateEntity; } }
假设默认的接收和返回值的格式都是yyyy-MM-ddHH:mm:ss,可以有以下几个方案。
配置application.yml文件
在application.yml文件中配置上如下内容:
spring: jackson: date-format:yyyy-MM-ddHH:mm:ss time-zone:GMT+8
小结:
- 支持Content-Type是application/json的POST请求,请求参数字符串和返回的格式都是yyyy-MM-ddHH:mm:ss如果请求参数是其他格式,如yyyy-MM-dd字符串则报400BadRequest异常。
- 不支持LocalDate等Java8日期API。
增加Jackson配置
/** *Jackson序列化和反序列化转换器,用于转换Post请求体中的json以及将对象序列化为返回响应的json */ @Bean publicJackson2ObjectMapperBuilderCustomizerjackson2ObjectMapperBuilderCustomizer(){ returnbuilder->builder .serializerByType(LocalDateTime.class,newLocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN))) .serializerByType(LocalDate.class,newLocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN))) .serializerByType(LocalTime.class,newLocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN))) .serializerByType(Date.class,newDateSerializer(false,newSimpleDateFormat(DEFAULT_DATETIME_PATTERN))) .deserializerByType(LocalDateTime.class,newLocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN))) .deserializerByType(LocalDate.class,newLocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN))) .deserializerByType(LocalTime.class,newLocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN))) .deserializerByType(Date.class,newDateDeserializers.DateDeserializer(DateDeserializers.DateDeserializer.instance,newSimpleDateFormat(DEFAULT_DATETIME_PATTERN),DEFAULT_DATETIME_PATTERN)) ; }
小结:
- 支持Content-Type是application/json的POST请求,请求参数字符串和返回的格式都是yyyy-MM-ddHH:mm:ss如果请求参数是其他格式,如yyyy-MM-dd字符串则报400BadRequest异常。
- 支持LocalDate等Java8日期API。
PS:上面的方式是通过配置一个Jackson2ObjectMapperBuilderCustomizerBean完成的,除了这种,也可以通过自定义一个MappingJackson2HttpMessageConverter来实现。
@Bean publicMappingJackson2HttpMessageConvertermappingJackson2HttpMessageConverter(){ MappingJackson2HttpMessageConverterconverter=newMappingJackson2HttpMessageConverter(); ObjectMapperobjectMapper=newObjectMapper(); //指定时区 objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8:00")); //日期类型字符串处理 objectMapper.setDateFormat(newSimpleDateFormat(DEFAULT_DATETIME_PATTERN)); //Java8日期日期处理 JavaTimeModulejavaTimeModule=newJavaTimeModule(); javaTimeModule.addSerializer(LocalDateTime.class,newLocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN))); javaTimeModule.addSerializer(LocalDate.class,newLocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN))); javaTimeModule.addSerializer(LocalTime.class,newLocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN))); javaTimeModule.addDeserializer(LocalDateTime.class,newLocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN))); javaTimeModule.addDeserializer(LocalDate.class,newLocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN))); javaTimeModule.addDeserializer(LocalTime.class,newLocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN))); objectMapper.registerModule(javaTimeModule); converter.setObjectMapper(objectMapper); returnconverter; }
以上几种方式都可以实现JSON传参时的全局化配置,更推荐后两种代码中增加配置bean的方式,可以同时支持Date和LocalDate。
GET请求及POST表单方式传参
这种方式和上面的JSON方式,在SpringBoot处理的方式是完全不同的。上一种JSON方式传参是在HttpMessgeConverter中通过jackson的ObjectMapper将http请求体转换成我们写在controller中的参数对象的,而这种方式用的是Converter接口(spring-core中定义的用于将源类型(一般是String)转成目标类型的接口),两者是有本质区别的。
自定义参数转换器(Converter)
自定义一个参数转换器,实现上面提到的org.springframework.core.convert.converter.Converter接口,在配置类里配置上以下几个bean,示例如下:
@Bean publicConverterdateConverter(){ returnnewConverter<>(){ @Override publicDateconvert(Stringsource){ SimpleDateFormatformatter=newSimpleDateFormat(DEFAULT_DATETIME_PATTERN); try{ returnformatter.parse(source); }catch(Exceptione){ thrownewRuntimeException(String.format("Errorparsing%stoDate",source)); } } }; } @Bean publicConverter localDateConverter(){ returnnewConverter<>(){ @Override publicLocalDateconvert(Stringsource){ returnLocalDate.parse(source,DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)); } }; } @Bean publicConverter localDateTimeConverter(){ returnnewConverter<>(){ @Override publicLocalDateTimeconvert(Stringsource){ returnLocalDateTime.parse(source,DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)); } }; }
同时把controller接口增加一些参数,可以发现在接口里单独用变量接收也是可以正常转换的。
@RequestMapping("/date") publicDateEntitygetDate( LocalDatedate, LocalDateTimedateTime, DateoriginalDate, DateEntitydateEntity){ System.out.printf("date=%s,dateTime=%s,originalDate=%s\n",date,dateTime,originalDate); returndateEntity; }
小结:
- GET请求及POST表单方式请求。
- 支持LocalDate等Java8日期API。
使用@DateTimeFormat注解
和前面提到的一样,GET请求及POST表单方式也是可以用@DateTimeFormat来处理的,单独在controller接口参数或者实体类属性中都可以使用,比如@DateTimeFormat(pattern="yyyy-MM-dd")DateoriginalDate。注意,如果使用了自定义参数转化器(Converter),Spring会优先使用该方式进行处理,即@DateTimeFormat注解不生效,两种方式是不兼容的。
那么假如我们使用了自定义参数转换器,但是还是想兼容用yyyy-MM-dd形式接受呢?我们可以把前面的dateConverter改成用正则匹配方式,这样也不失为一种不错的解决方案,示例如下。
/** *日期正则表达式 */ privatestaticfinalStringDATE_REGEX="[1-9]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])"; /** *时间正则表达式 */ privatestaticfinalStringTIME_REGEX="(20|21|22|23|[0-1]\\d):[0-5]\\d:[0-5]\\d"; /** *日期和时间正则表达式 */ privatestaticfinalStringDATE_TIME_REGEX=DATE_REGEX+"\\s"+TIME_REGEX; /** *13位时间戳正则表达式 */ privatestaticfinalStringTIME_STAMP_REGEX="1\\d{12}"; /** *年和月正则表达式 */ privatestaticfinalStringYEAR_MONTH_REGEX="[1-9]\\d{3}-(0[1-9]|1[0-2])"; /** *年和月格式 */ privatestaticfinalStringYEAR_MONTH_PATTERN="yyyy-MM"; @Bean publicConverterdateConverter(){ returnnewConverter (){ @SuppressWarnings("NullableProblems") @Override publicDateconvert(Stringsource){ if(StrUtil.isEmpty(source)){ returnnull; } if(source.matches(TIME_STAMP_REGEX)){ returnnewDate(Long.parseLong(source)); } DateFormatformat; if(source.matches(DATE_TIME_REGEX)){ format=newSimpleDateFormat(DEFAULT_DATETIME_PATTERN); }elseif(source.matches(DATE_REGEX)){ format=newSimpleDateFormat(DEFAULT_DATE_FORMAT); }elseif(source.matches(YEAR_MONTH_REGEX)){ format=newSimpleDateFormat(YEAR_MONTH_PATTERN); }else{ thrownewIllegalArgumentException(); } try{ returnformat.parse(source); }catch(ParseExceptione){ thrownewRuntimeException(e); } } }; }
小结:
- GET请求及POST表单方式请求,但是需要在每个使用的地方加上@DateTimeFormat注解。
- 与自定义参数转化器(Converter)不兼容。
- 支持LocalDate等Java8日期API。
使用@ControllerAdvice配合@initBinder
/* *在类上加上@ControllerAdvice */ @ControllerAdvice @SpringBootApplication @RestController publicclassSpringbootDateLearningApplication{ ... @InitBinder protectedvoidinitBinder(WebDataBinderbinder){ binder.registerCustomEditor(LocalDate.class,newPropertyEditorSupport(){ @Override publicvoidsetAsText(Stringtext)throwsIllegalArgumentException{ setValue(LocalDate.parse(text,DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN))); } }); binder.registerCustomEditor(LocalDateTime.class,newPropertyEditorSupport(){ @Override publicvoidsetAsText(Stringtext)throwsIllegalArgumentException{ setValue(LocalDateTime.parse(text,DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN))); } }); binder.registerCustomEditor(LocalTime.class,newPropertyEditorSupport(){ @Override publicvoidsetAsText(Stringtext)throwsIllegalArgumentException{ setValue(LocalTime.parse(text,DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN))); } }); binder.registerCustomEditor(Date.class,newPropertyEditorSupport(){ @Override publicvoidsetAsText(Stringtext)throwsIllegalArgumentException{ SimpleDateFormatformatter=newSimpleDateFormat(DEFAULT_DATETIME_PATTERN); try{ setValue(formatter.parse(text)); }catch(Exceptione){ thrownewRuntimeException(String.format("Errorparsing%stoDate",text)); } } }); } ... }
在实际应用中,我们可以把上面代码放到父类中,所有接口继承这个父类,达到全局处理的效果。原理就是与AOP类似,在参数进入handler之前进行转换时使用我们定义的PropertyEditorSupport来处理。
小结:
- GET请求及POST表单方式请求。
- 支持LocalDate等Java8日期API。
- 局部差异化处理
假设按照前面的全局日期格式设置的是:yyyy-MM-ddHH:mm:ss,但是某个Date类型的字段需要特殊处理成yyyy/MM/dd格式来接收或者返回,有以下方案可以选择。
使用@DateTimeFormat和@JsonFormat注解
@JsonFormat(pattern="yyyy/MM/dd",timezone="GMT+8") @DateTimeFormat(pattern="yyyy/MM/ddHH:mm:ss") privateDateoriginalDate;
如上所示,可以在字段上增加@DateTimeFormat和@JsonFormat注解,可以分别单独指定该字段的接收和返回的日期格式。
PS:@JsonFormat和@DateTimeFormat注解都不是SpringBoot提供的,在Spring应用中也可以使用。
再次提醒,如果使用了自定义参数转化器(Converter),Spring会优先使用该方式进行处理,即@DateTimeFormat注解不生效。
自定义序列化器和反序列化器
/** *{@linkDate}序列化器 */ publicclassDateJsonSerializerextendsJsonSerializer{ @Override publicvoidserialize(Datedate,JsonGeneratorjsonGenerator,SerializerProviderserializerProvider)throws IOException{ SimpleDateFormatdateFormat=newSimpleDateFormat("yyyy-MM-dd"); jsonGenerator.writeString(dateFormat.format(date)); } } /** *{@linkDate}反序列化器 */ publicclassDateJsonDeserializerextendsJsonDeserializer { @Override publicDatedeserialize(JsonParserjsonParser,DeserializationContextdeserializationContext)throwsIOException{ try{ SimpleDateFormatdateFormat=newSimpleDateFormat("yyyy-MM-dd"); returndateFormat.parse(jsonParser.getText()); }catch(ParseExceptione){ thrownewIOException(e); } } } /** *使用方式 */ @JsonSerialize(using=DateJsonSerializer.class) @JsonDeserialize(using=DateJsonDeserializer.class) privateDateoriginalDate;
如上所示,可以在字段上使用@JsonSerialize和@JsonDeserialize注解来指定在序列化和反序列化时使用我们自定义的序列化器和反序列化器。
最后再来个兼容JSON方式和GET请求及POST表单方式的完整的配置吧。
@Configuration publicclassGlobalDateTimeConfig{ /** *日期正则表达式 */ privatestaticfinalStringDATE_REGEX="[1-9]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])"; /** *时间正则表达式 */ privatestaticfinalStringTIME_REGEX="(20|21|22|23|[0-1]\\d):[0-5]\\d:[0-5]\\d"; /** *日期和时间正则表达式 */ privatestaticfinalStringDATE_TIME_REGEX=DATE_REGEX+"\\s"+TIME_REGEX; /** *13位时间戳正则表达式 */ privatestaticfinalStringTIME_STAMP_REGEX="1\\d{12}"; /** *年和月正则表达式 */ privatestaticfinalStringYEAR_MONTH_REGEX="[1-9]\\d{3}-(0[1-9]|1[0-2])"; /** *年和月格式 */ privatestaticfinalStringYEAR_MONTH_PATTERN="yyyy-MM"; /** *DateTime格式化字符串 */ privatestaticfinalStringDEFAULT_DATETIME_PATTERN="yyyy-MM-ddHH:mm:ss"; /** *Date格式化字符串 */ privatestaticfinalStringDEFAULT_DATE_FORMAT="yyyy-MM-dd"; /** *Time格式化字符串 */ privatestaticfinalStringDEFAULT_TIME_FORMAT="HH:mm:ss"; /** *LocalDate转换器,用于转换RequestParam和PathVariable参数 */ @Bean publicConverterlocalDateConverter(){ returnnewConverter (){ @SuppressWarnings("NullableProblems") @Override publicLocalDateconvert(Stringsource){ if(StringUtils.isEmpty(source)){ returnnull; } returnLocalDate.parse(source,DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)); } }; } /** *LocalDateTime转换器,用于转换RequestParam和PathVariable参数 */ @Bean publicConverter localDateTimeConverter(){ returnnewConverter (){ @SuppressWarnings("NullableProblems") @Override publicLocalDateTimeconvert(Stringsource){ if(StringUtils.isEmpty(source)){ returnnull; } returnLocalDateTime.parse(source,DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)); } }; } /** *LocalDate转换器,用于转换RequestParam和PathVariable参数 */ @Bean publicConverter localTimeConverter(){ returnnewConverter (){ @SuppressWarnings("NullableProblems") @Override publicLocalTimeconvert(Stringsource){ if(StringUtils.isEmpty(source)){ returnnull; } returnLocalTime.parse(source,DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)); } }; } /** *Date转换器,用于转换RequestParam和PathVariable参数 */ @Bean publicConverter dateConverter(){ returnnewConverter (){ @SuppressWarnings("NullableProblems") @Override publicDateconvert(Stringsource){ if(StringUtils.isEmpty(source)){ returnnull; } if(source.matches(TIME_STAMP_REGEX)){ returnnewDate(Long.parseLong(source)); } DateFormatformat; if(source.matches(DATE_TIME_REGEX)){ format=newSimpleDateFormat(DEFAULT_DATETIME_PATTERN); }elseif(source.matches(DATE_REGEX)){ format=newSimpleDateFormat(DEFAULT_DATE_FORMAT); }elseif(source.matches(YEAR_MONTH_REGEX)){ format=newSimpleDateFormat(YEAR_MONTH_PATTERN); }else{ thrownewIllegalArgumentException(); } try{ returnformat.parse(source); }catch(ParseExceptione){ thrownewRuntimeException(e); } } }; } /** *Json序列化和反序列化转换器,用于转换Post请求体中的json以及将我们的对象序列化为返回响应的json */ @Bean publicJackson2ObjectMapperBuilderCustomizerjackson2ObjectMapperBuilderCustomizer(){ returnbuilder->builder.serializerByType(LocalDateTime.class,newLocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN))) .serializerByType(LocalDate.class,newLocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .serializerByType(LocalTime.class,newLocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))) .serializerByType(Long.class,ToStringSerializer.instance) .deserializerByType(LocalDateTime.class,newLocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN))) .deserializerByType(LocalDate.class,newLocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT))) .deserializerByType(LocalTime.class,newLocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT))); } }
源码剖析
在了解完怎么样进行全局设置后,接下来我们通过debug源码来深入剖析一下SpringMVC是如何进行参数绑定的。
仍然是以上面的controller为例进行debug。
@RequestMapping("/date") publicDateEntitygetDate( LocalDatedate, LocalDateTimedateTime, DateoriginalDate, DateEntitydateEntity){ System.out.printf("date=%s,dateTime=%s,originalDate=%s\n",date,dateTime,originalDate); returndateEntity; }
以下是收到请求后的方法调用栈的一些关键方法:
//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 publicObjectinvokeForRequest(NativeWebRequestrequest,@NullableModelAndViewContainermavContainer, Object...providedArgs)throwsException{ //这里完成参数的转换,得到的是转换后的值 Object[]args=getMethodArgumentValues(request,mavContainer,providedArgs); if(logger.isTraceEnabled()){ logger.trace("Arguments:"+Arrays.toString(args)); } //反射调用,真正开始执行方法 returndoInvoke(args); } //具体实现 protectedObject[]getMethodArgumentValues(NativeWebRequestrequest,@NullableModelAndViewContainermavContainer, Object...providedArgs)throwsException{ //获取当前handlermethod的方法参数数组,封装了入参信息,比如类型、泛型等 MethodParameter[]parameters=getMethodParameters(); if(ObjectUtils.isEmpty(parameters)){ returnEMPTY_ARGS; } //该数组用来存放从MethodParameter转换后的结果 Object[]args=newObject[parameters.length]; for(inti=0;i下面需要进入HandlerMethodArgumentResolverComposite#resolveArgument方法源码里面。
//HandlerMethodArgumentResolverComposite.java @Override @Nullable publicObjectresolveArgument(MethodParameterparameter,@NullableModelAndViewContainermavContainer, NativeWebRequestwebRequest,@NullableWebDataBinderFactorybinderFactory)throwsException{ //这里来获取匹配当前方法参数的参数解析器 HandlerMethodArgumentResolverresolver=getArgumentResolver(parameter); if(resolver==null){ thrownewIllegalArgumentException("Unsupportedparametertype["+ parameter.getParameterType().getName()+"].supportsParametershouldbecalledfirst."); } //调用真正的参数解析器来处理参数并返回 returnresolver.resolveArgument(parameter,mavContainer,webRequest,binderFactory); } //获取匹配当前方法参数的参数解析器 @Nullable privateHandlerMethodArgumentResolvergetArgumentResolver(MethodParameterparameter){ //首先从缓存中查询是否有适配当前方法参数的参数解析器,首次进入是没有的 HandlerMethodArgumentResolverresult=this.argumentResolverCache.get(parameter); if(result==null){ for(HandlerMethodArgumentResolverresolver:this.argumentResolvers){ //逐个遍历argumentResolvers这个list里的参数解析器来判断是否支持 if(resolver.supportsParameter(parameter)){ result=resolver; this.argumentResolverCache.put(parameter,result); break; } } } returnresult; }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接口。
publicinterfaceHandlerMethodArgumentResolver{ //上面用到用来判断当前参数解析器是否支持给定的方法参数 booleansupportsParameter(MethodParameterparameter); //解析给定的方法参数并返回 @Nullable ObjectresolveArgument(MethodParameterparameter,@NullableModelAndViewContainermavContainer, NativeWebRequestwebRequest,@NullableWebDataBinderFactorybinderFactory)throwsException; }到这里我们整理一下思路,对方法参数的解析都是通过逐个遍历找到合适的HandlerMethodArgumentResolver来完成的。比如,如果参数上标注了@RequestParam或者@RequestBody或者@PathVariable注解,SpringMVC会用不同的参数解析器来解析。下面挑一个最常用的RequestParamMethodArgumentResolver来深入分析一下详细的解析流程。
RequestParamMethodArgumentResolver继承自AbstractNamedValueMethodArgumentResolver,AbstractNamedValueMethodArgumentResolver实现了HandlerMethodArgumentResolver接口的resolveArgument方法。
//AbstractNamedValueMethodArgumentResolver.java @Override @Nullable publicfinalObjectresolveArgument(MethodParameterparameter,@NullableModelAndViewContainermavContainer, NativeWebRequestwebRequest,@NullableWebDataBinderFactorybinderFactory)throwsException{ //解析出传入的原始值,作为下面方法的参数 Objectarg=resolveName(resolvedName.toString(),nestedParameter,webRequest); ...... if(binderFactory!=null){ //创建DataBinder WebDataBinderbinder=binderFactory.createBinder(webRequest,null,namedValueInfo.name); try{ //通过DataBinder进行参数绑定,参数列表:原始值,目标类型,方法参数 arg=binder.convertIfNecessary(arg,parameter.getParameterType(),parameter); } ...... } handleResolvedValue(arg,namedValueInfo.name,parameter,mavContainer,webRequest); returnarg; } //DataBinder.java @Override @Nullable publicTconvertIfNecessary(@NullableObjectvalue,@NullableClass requiredType, @NullableMethodParametermethodParam)throwsTypeMismatchException{ //调用子类的convertIfNecessary方法,这里的具体实现是TypeConverterSupport returngetTypeConverter().convertIfNecessary(value,requiredType,methodParam); } //TypeConverterSupport.java @Override @Nullable public TconvertIfNecessary(@NullableObjectvalue,@NullableClass requiredType, @NullableMethodParametermethodParam)throwsTypeMismatchException{ //调用重载的convertIfNecessary方法,通过MethodParameter构造了类型描述符TypeDescriptor returnconvertIfNecessary(value,requiredType, (methodParam!=null?newTypeDescriptor(methodParam):TypeDescriptor.valueOf(requiredType))); } //convertIfNecessary方法 @Nullable @Override public TconvertIfNecessary(@NullableObjectvalue,@NullableClass requiredType, @NullableTypeDescriptortypeDescriptor)throwsTypeMismatchException{ Assert.state(this.typeConverterDelegate!=null,"NoTypeConverterDelegate"); try{ //调用TypeConverterDelegate的convertIfNecessary方法 returnthis.typeConverterDelegate.convertIfNecessary(null,null,value,requiredType,typeDescriptor); } ...... } 接下来进入TypeConverterDelegate的源码。
//TypeConverterDelegate.java @Nullable publicTconvertIfNecessary(@NullableStringpropertyName,@NullableObjectoldValue,@NullableObjectnewValue, @NullableClass requiredType,@NullableTypeDescriptortypeDescriptor)throwsIllegalArgumentException{ //查找是否有适合需求类型的自定义的PropertyEditor。还记得上面的使用@ControllerAdvice配合@initBinder那一节吗,如果有按那样配置,这里就会找到 PropertyEditoreditor=this.propertyEditorRegistry.findCustomEditor(requiredType,propertyName); ConversionFailedExceptionconversionAttemptEx=null; //查找到类型转换服务ConversionService ConversionServiceconversionService=this.propertyEditorRegistry.getConversionService(); //关键判断,如果没有PropertyEditor就使用ConversionService if(editor==null&&conversionService!=null&&newValue!=null&&typeDescriptor!=null){ TypeDescriptorsourceTypeDesc=TypeDescriptor.forObject(newValue); if(conversionService.canConvert(sourceTypeDesc,typeDescriptor)){ try{ //#1,类型转换服务转换完成后就返回,下面会详细解释 return(T)conversionService.convert(newValue,sourceTypeDesc,typeDescriptor); } catch(ConversionFailedExceptionex){ //fallbacktodefaultconversionlogicbelow conversionAttemptEx=ex; } } } ObjectconvertedValue=newValue; //关键判断,如果有PropertyEditor就使用PropertyEditor if(editor!=null||(requiredType!=null&&!ClassUtils.isAssignableValue(requiredType,convertedValue))){ ...... //由editor完成转换 convertedValue=doConvertValue(oldValue,convertedValue,requiredType,editor); } booleanstandardConversion=false; if(requiredType!=null){ //Trytoapplysomestandardtypeconversionrulesifappropriate. if(convertedValue!=null){ if(Object.class==requiredType){ return(T)convertedValue; } //下面是数组、集合类型属性的处理,这里会遍历集合元素,递归调用convertIfNecessary转化,再收集处理结果 elseif(requiredType.isArray()){ //Arrayrequired->applyappropriateconversionofelements. if(convertedValueinstanceofString&&Enum.class.isAssignableFrom(requiredType.getComponentType())){ convertedValue=StringUtils.commaDelimitedListToStringArray((String)convertedValue); } return(T)convertToTypedArray(convertedValue,propertyName,requiredType.getComponentType()); } elseif(convertedValueinstanceofCollection){ //Convertelementstotargettype,ifdetermined. convertedValue=convertToTypedCollection( (Collection>)convertedValue,propertyName,requiredType,typeDescriptor); standardConversion=true; } elseif(convertedValueinstanceofMap){ //Convertkeysandvaluestorespectivetargettype,ifdetermined. 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())){ //Wecanstringifyanyprimitivevalue... return(T)convertedValue.toString(); } elseif(convertedValueinstanceofString&&!requiredType.isInstance(convertedValue)){ ...... } elseif(convertedValueinstanceofNumber&&Number.class.isAssignableFrom(requiredType)){ convertedValue=NumberUtils.convertNumberToTargetClass( (Number)convertedValue,(Class )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){ throwconversionAttemptEx; } logger.debug("OriginalConversionServiceattemptfailed-ignoredsince"+ "PropertyEditorbasedconversioneventuallysucceeded",conversionAttemptEx); } return(T)convertedValue; } 假如我们配置了自定义的Converter,会进入#1的分支,由ConversionService进行类型转换,以其子类GenericConversionService为例。
//GenericConversionService.java @Override @Nullable publicObjectconvert(@NullableObjectsource,@NullableTypeDescriptorsourceType,TypeDescriptortargetType){ ...... //从缓存中找到匹配类型的conveter,以LocalDateTime为例,会找到我们自定义的localDateTimeConverter GenericConverterconverter=getConverter(sourceType,targetType); if(converter!=null){ //通过工具方法调用真正的converter完成类型转换。至此,完成了源类型到目标类型的转换 Objectresult=ConversionUtils.invokeConverter(converter,source,sourceType,targetType); returnhandleResult(sourceType,targetType,result); } returnhandleConverterNotFound(source,sourceType,targetType); }以上就是处理标注@RequestParam注解的参数的RequestParamMethodArgumentResolver解析流程。
下面来看一下处理标注@RequestBody注解的参数的RequestResponseBodyMethodProcessor的解析流程,仍然是从resolveArgument方法切入。
//RequestResponseBodyMethodProcessor.java @Override publicObjectresolveArgument(MethodParameterparameter,@NullableModelAndViewContainermavContainer, NativeWebRequestwebRequest,@NullableWebDataBinderFactorybinderFactory)throwsException{ parameter=parameter.nestedIfOptional(); //在这里完成参数的解析 Objectarg=readWithMessageConverters(webRequest,parameter,parameter.getNestedGenericParameterType()); ...... returnadaptArgumentIfNecessary(arg,parameter); } @Override protectedObjectreadWithMessageConverters(NativeWebRequestwebRequest,MethodParameterparameter, TypeparamType)throwsIOException,HttpMediaTypeNotSupportedException,HttpMessageNotReadableException{ HttpServletRequestservletRequest=webRequest.getNativeRequest(HttpServletRequest.class); Assert.state(servletRequest!=null,"NoHttpServletRequest"); ServletServerHttpRequestinputMessage=newServletServerHttpRequest(servletRequest); //调用父类AbstractMessageConverterMethodArgumentResolver完成参数解析 Objectarg=readWithMessageConverters(inputMessage,parameter,paramType); if(arg==null&&checkRequired(parameter)){ thrownewHttpMessageNotReadableException("Requiredrequestbodyismissing:"+ parameter.getExecutable().toGenericString(),inputMessage); } returnarg; } 下面进入父类AbstractMessageConverterMethodArgumentResolver的源码。
//AbstractMessageConverterMethodArgumentResolver.java @Nullable protectedObjectreadWithMessageConverters(HttpInputMessageinputMessage,MethodParameterparameter, TypetargetType)throwsIOException,HttpMediaTypeNotSupportedException,HttpMessageNotReadableException{ ...... EmptyBodyCheckingHttpInputMessagemessage; try{ message=newEmptyBodyCheckingHttpInputMessage(inputMessage); //遍历HttpMessageConverter for(HttpMessageConverter>converter:this.messageConverters){ Class >converterType=(Class >)converter.getClass(); GenericHttpMessageConverter>genericConverter= (converterinstanceofGenericHttpMessageConverter?(GenericHttpMessageConverter>)converter:null); if(genericConverter!=null?genericConverter.canRead(targetType,contextClass,contentType): (targetClass!=null&&converter.canRead(targetClass,contentType))){ if(message.hasBody()){ HttpInputMessagemsgToUse= getAdvice().beforeBodyRead(message,parameter,targetType,converterType); //实际由MappingJackson2HttpMessageConverter调用父类AbstractJackson2HttpMessageConverter的read方法完成解析, body=(genericConverter!=null?genericConverter.read(targetType,contextClass,msgToUse): ((HttpMessageConverter )converter).read(targetClass,msgToUse)); body=getAdvice().afterBodyRead(body,msgToUse,parameter,targetType,converterType); } else{ body=getAdvice().handleEmptyBody(null,message,parameter,targetType,converterType); } break; } } } ...... returnbody; } //AbstractJackson2HttpMessageConverter.java @Override publicObjectread(Typetype,@NullableClass>contextClass,HttpInputMessageinputMessage) throwsIOException,HttpMessageNotReadableException{ //获得要转换的目标参数Java类型,如LocalDateTime等 JavaTypejavaType=getJavaType(type,contextClass); //调用本类的readJavaType方法 returnreadJavaType(javaType,inputMessage); } //AbstractJackson2HttpMessageConverter.java privateObjectreadJavaType(JavaTypejavaType,HttpInputMessageinputMessage)throwsIOException{ try{ if(inputMessageinstanceofMappingJacksonInputMessage){ Class>deserializationView=((MappingJacksonInputMessage)inputMessage).getDeserializationView(); if(deserializationView!=null){ returnthis.objectMapper.readerWithView(deserializationView).forType(javaType). readValue(inputMessage.getBody()); } } //调用jackson类库,将HTTP的json请求信息解析为需要的参数类型。至此,将json请求转换成目标Java类型 returnthis.objectMapper.readValue(inputMessage.getBody(),javaType); } ...... } 总结
controller方法的参数是通过不同的HandlerMethodArgumentResolver完成解析的。如果参数标注了@RequestBody注解,实际上是通过MappingJackson2HttpMessageConverter的ObjectMapper将传入json格式数据反序列化解析成目标类型的。如果标注了@RequestParam注解,是通过在应用初始化时注入到ConversionService的一个个Converter来实现的。其他的HandlerMethodArgumentResolver也是各有各的用处,大家可以再看看相关代码,以便加深理解。
到此这篇关于在SpringBoot应用中优雅的使用Date和LocalDateTime的教程详解的文章就介绍到这了,更多相关SpringBoot使用Date和LocalDateTime内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!