Java8 has been around for years, but many people still insist on using Date and SimpleDateFormat for time manipulation during development. SimpleDateFormat is not thread-safe, and Date handling time is cumbersome, so Java8 offers new time manipulation apis such as LocalDateTime, LocalDate, and LocalTime. Regardless of Date or LocalDate, it is often necessary to add @dateTimeFormat annotation to the Date field of each entity class when developing Spring Boot applications to receive front-end values and Date field binding. Add the @jsonFormat annotation to format the date field back to the time format we want. Time and date types are used very frequently in development, and it would be tedious to annotate each field with these two types. Is there a way to handle global Settings? Today I will introduce it to you.
Note: this article is based on Springboot2.3.0.
You need to perform different configurations according to different request modes. In the following sections, parameters are sent in JSON mode and in GET request and POST form mode.
The parameter is transmitted in JSON mode
This case refers to the POST Type, and the Content-Type is an application/ JSON request. For this type of request, the controller needs to annotate the @requestBody annotation to the local variable we use to receive the request parameters as follows:
@SpringBootApplication
@RestController
public class SpringbootDateLearningApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootDateLearningApplication.class, args); } / * ** DateTime format string* / private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; / * ** Date Formatting string* / private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd"; / * ** Time Format string* / 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(a) { return date; } public void setDate(LocalDate date) { this.date = date; } public LocalDateTime getDateTime(a) { return dateTime; } public void setDateTime(LocalDateTime dateTime) { this.dateTime = dateTime; } public Date getOriginalDate(a) { return originalDate; } public void setOriginalDate(Date originalDate) { this.originalDate = originalDate; } } @RequestMapping("/date") public DateEntity getDate(@RequestBody DateEntity dateEntity) { return dateEntity; } } Copy the code
Assuming that the default received and returned values are in the format YYYY-MM-DD HH: MM: SS, the following scenarios are possible.
Configure the application.yml file
In the application.yml file, configure the following:
spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
Copy the code
Summary:
- Content-type is an application/ JSON POST request, the request parameter string and the return format are supported
yyyy-MM-dd HH:mm:ss
If the request parameter is in another format, e.gyyyy-MM-dd
If the value is a string, 400 Bad Request is reported. - Java8 date apis such as LocalDate are not supported.
Adding Jackson Configuration
/ * ** Jackson serialization and deserialization converters for converting JSON in the body of a Post request and serializing objects to JSON that returns a response* /
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(a) {
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)) ; } Copy the code
Summary:
- Content-type is an application/ JSON POST request, the request parameter string and the return format are supported
yyyy-MM-dd HH:mm:ss
If the request parameter is in another format, e.gyyyy-MM-dd
If the value is a string, 400 Bad Request is reported. - Java8 date apis such as LocalDate are supported.
PS: the above methods are performed by configuring a Jackson2ObjectMapperBuilderCustomizerBean, in addition to this, also can be done through a MappingJackson2HttpMessageConverter custom.
@Bean
public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter(a) {
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
// Specify the time zone
objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8:00")); // Date type string processing objectMapper.setDateFormat(new SimpleDateFormat(DEFAULT_DATETIME_PATTERN)); Java8 date date processing 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; } Copy the code
The preceding methods can realize the global configuration of JSON parameter transmission. It is recommended to add the bean configuration method in the last two codes, which can support both Date and LocalDate.
GET request and POST form
This method is completely different from the JSON method above in Spring Boot. The previous JSON method of passing parameters is to convert the HTTP request body in HttpMessgeConverter via Jackson’s ObjectMapper to the parameter object we wrote in controller. This approach uses the Converter interface (defined in Spring-Core for converting a source type (usually a String) to a target type), which is essentially different.
Custom Parameter Converter (Converter)
A custom converter parameters, to achieve the above mentioned org. Springframework. Core. The convert. The converter. The converter interface, the following bean in the configuration class configuration, the sample is as follows:
@Bean
public Converter<String, Date> dateConverter(a) {
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(a) { return new Converter<>() { @Override public LocalDate convert(String source) { return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)); } }; } @Bean public Converter<String, LocalDateTime> localDateTimeConverter(a) { return new Converter<>() { @Override public LocalDateTime convert(String source) { return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)); } }; } Copy the code
At the same time, add some parameters to the controller interface, it can be found that the interface can be converted by using variables alone.
@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; } Copy the code
Summary:
- GET request and POST form request.
- Java8 date apis such as LocalDate are supported.
use@DateTimeFormat
annotations
As mentioned earlier, GET requests and POST forms can also be handled using @dateTimeFormat, either in controller interface parameters or in entity class attributes. For example, @datetimeformat (pattern = “YYYY-MM-DD “) Date originalDate. Note that if you are using a Custom parameter Converter, Spring will preferentially use the @dateTimeFormat annotation. The two methods are incompatible.
What if we use a custom parameter converter, but still want to accept it in YYYY-MM-DD format? A good solution would be to change the dateConverter to regular matching, as shown in the following example.
/ * ** Date regular expression* /
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])";
/ * ** Time regular expression* / private static final String TIME_REGEX = "(20|21|22|23|[0-1]\\d):[0-5]\\d:[0-5]\\d"; / * ** Date and time regular expressions* / private static final String DATE_TIME_REGEX = DATE_REGEX + "\\s" + TIME_REGEX; / * ** 13-bit timestamp regular expression* / private static final String TIME_STAMP_REGEX = "1\\d{12}"; / * ** Year and month regular expressions* / private static final String YEAR_MONTH_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])"; / * ** Year and month formats* / private static final String YEAR_MONTH_PATTERN = "yyyy-MM"; @Bean public Converter<String, Date> dateConverter(a) { 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); } } }; } Copy the code
Summary:
- GET request and POST form request, but need to be added in each place used
@DateTimeFormat
Annotation. - Incompatible with Converter.
- Java8 date apis such as LocalDate are supported.
use@ControllerAdvice
Cooperate with@initBinder
/ ** Add @controllerAdvice to the class* /
@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)); } } }); } .} Copy the code
In practice, we could put the above code into a parent class that all interfaces inherit from, achieving global processing. The principle is that, similar to AOP, we define PropertyEditorSupport to handle arguments when they are converted before they enter the handler.
Summary:
- GET request and POST form request.
- Java8 date apis such as LocalDate are supported.
Local differentiation treatment
Assume that the global Date format is YYYY-MM-DD HH: MM :ss, but a field of the Date type needs to be sent in yyyy/MM/ DD format. The following schemes are available.
use@DateTimeFormat
and@JsonFormat
annotations
@JsonFormat(pattern = "yyyy/MM/dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy/MM/dd HH:mm:ss")
private Date originalDate;
Copy the code
As shown above, you can add @DateTimeFormat and @JsonFormat annotations to the field to specify the received and returned date formats for the field separately.
PS: The @jsonFormat and @DateTimeFormat annotations are not provided by Spring Boot and can be used in Spring applications.
Again, if you are using a Custom parameter Converter, Spring will preferentially use this method, i.e. the @dateTimeFormat annotation will not take effect.
Custom serializers and deserializers
/ * * * {@linkDate} serializer* /
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)); } } / * * * {@linkDate} deserializer* / 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); } } } / * ** Mode of use* / @JsonSerialize(using = DateJsonSerializer.class) @JsonDeserialize(using = DateJsonDeserializer.class) private Date originalDate; Copy the code
As shown above, we can use @jsonSerialize and @Jsondeserialize annotations on the fields to specify our custom serializers and deserializers for serialization and deserialization.
Finally, a complete configuration that is compatible with JSON and GET requests and POST forms.
@Configuration
public class GlobalDateTimeConfig {
/ * ** Date regular expression* / 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])"; / * ** Time regular expression* / private static final String TIME_REGEX = "(20|21|22|23|[0-1]\\d):[0-5]\\d:[0-5]\\d"; / * ** Date and time regular expressions* / private static final String DATE_TIME_REGEX = DATE_REGEX + "\\s" + TIME_REGEX; / * ** 13-bit timestamp regular expression* / private static final String TIME_STAMP_REGEX = "1\\d{12}"; / * ** Year and month regular expressions* / private static final String YEAR_MONTH_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])"; / * ** Year and month formats* / private static final String YEAR_MONTH_PATTERN = "yyyy-MM"; / * ** DateTime format string* / private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss"; / * ** Date Formatting string* / private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd"; / * ** Time Format string* / private static final String DEFAULT_TIME_FORMAT = "HH:mm:ss"; / * ** LocalDate converter to convert RequestParam and PathVariable parameters* / @Bean public Converter<String, LocalDate> localDateConverter(a) { 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 converter used to convert RequestParam and PathVariable parameters* / @Bean public Converter<String, LocalDateTime> localDateTimeConverter(a) { 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 converter to convert RequestParam and PathVariable parameters* / @Bean public Converter<String, LocalTime> localTimeConverter(a) { 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 converter, used to convert RequestParam and PathVariable parameters* / @Bean public Converter<String, Date> dateConverter(a) { 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 serialization and deserialization converters for converting Json from the body of the Post request and serializing our object to the Json that returns the response* / @Bean public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer(a) { 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))); } } Copy the code
Source analysis
Now that we know how to do global Settings, let’s take a closer look at how Spring MVC does parameter binding using debug source code.
The above controller is used as an example for debugging.
@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; } Copy the code
Here are some key methods in the method call stack after the request is received:
// DispatcherServlet handles requests
doService:943, DispatcherServlet
// Process the request
doDispatch:1040, DispatcherServlet
// Generate call chain (preprocessing, actual calling method, postprocessing)
handle:87, AbstractHandlerMethodAdapter handleInternal:793, RequestMappingHandlerAdapter // Reflection gets the actual calling method and is ready to start calling invokeHandlerMethod:879, RequestMappingHandlerAdapter invokeAndHandle:105, ServletInvocableHandlerMethod // Critical step, from which request parameters are processed invokeForRequest:134, InvocableHandlerMethod getMethodArgumentValues:167, InvocableHandlerMethod resolveArgument:121, HandlerMethodArgumentResolverComposite Copy the code
Now we start from the key invokeForRequest:134, InvocableHandlerMethod, the source code is as follows
// InvocableHandlerMethod.java
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// The value of the converted parameter is returned
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); if (logger.isTraceEnabled()) { logger.trace("Arguments: " + Arrays.toString(args)); } // reflection calls to actually start executing the method return doInvoke(args); } // Implement it protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception { // Get the current handler method's method parameters array, encapsulating input information such as type, generics, etc MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } // This array is used to store the converted result from 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 is to define a member variable, HandlerMethodArgumentResolverComposite type, is a collection of all kinds of HandlerMethodArgumentResolver. See if there is a parameter handler that supports the parameters of the current method if (!this.resolvers.supportsParameter(parameter)) { throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver")); } try { / / call HandlerMethodArgumentResolverComposite to process parameters, the following will focus on the internal logic args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); } catch (Exception ex) { . } } return args; } Copy the code
Need to enter below HandlerMethodArgumentResolverComposite# resolveArgument method source code.
// HandlerMethodArgumentResolverComposite.java
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// Get the parameter parser that matches the current method's parameters HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); if (resolver == null) { throw new IllegalArgumentException("Unsupported parameter type [" + parameter.getParameterType().getName() + "]. supportsParameter should be called first."); } // Call the real parameter parser to process the parameters and return return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); } // Gets a parameter parser that matches the parameters of the current method @Nullable private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { // If there is a parameter parser in the cache that matches the parameters of the current method, it is not available for the first time HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); if (result == null) { for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { ArgumentResolvers // Walk through the argument parsers in the list to see if they are supported if (resolver.supportsParameter(parameter)) { result = resolver; this.argumentResolverCache.put(parameter, result); break; } } } return result; } Copy the code
ArgumentResolvers there are 26 argument parsers in total. The common ones are listed below.
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} Copy the code
All parameters of the parser implements the interface HandlerMethodArgumentResolver.
public interface HandlerMethodArgumentResolver {
// This is used to determine whether the current parameter parser supports the given method arguments
boolean supportsParameter(MethodParameter parameter);
// Parse the given method argument and return @Nullable Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; } Copy the code
Here we organize ideas, on the interpretation of the method parameters are done through each traversal find suitable HandlerMethodArgumentResolver. For example, if a parameter is annotated with @requestParam or @RequestBody or @PathVariable, SpringMVC will parse it with a different parameter parser. Pick one of the most commonly used RequestParamMethodArgumentResolver below to deeply analyze the details of the analytical process.
RequestParamMethodArgumentResolver inherited from AbstractNamedValueMethodArgumentResolver, The method to realize the interface HandlerMethodArgumentResolver resolveArgument AbstractNamedValueMethodArgumentResolver.
// AbstractNamedValueMethodArgumentResolver.java
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
// Parse out the raw value passed as an argument to the method below Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); . if(binderFactory ! =null) { / / create DataBinder WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); try { // Bind parameters with DataBinder, parameter list: raw value, target type, method parameters 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 { // Call the convertIfNecessary method of the subclass, whose implementation is 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 { // call the overloaded convertIfNecessary method, using MethodParameter to construct the TypeDescriptor TypeDescriptor return convertIfNecessary(value, requiredType, (methodParam ! =null ? new TypeDescriptor(methodParam) : TypeDescriptor.valueOf(requiredType))); } / / convertIfNecessary method @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 { // Call the convertIfNecessary method of TypeConverterDelegate return this.typeConverterDelegate.convertIfNecessary(null.null, value, requiredType, typeDescriptor); } .} Copy the code
Next, enter the source code for the 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 {
// Find if there is a custom PropertyEditor that fits the requirement type. Remember the section on using @ControllerAdvice with @initBinder above, if configured that way, you'll find it here PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName); ConversionFailedException conversionAttemptEx = null; // ConversionService is found ConversionService conversionService = this.propertyEditorRegistry.getConversionService(); // Critical judgment, use ConversionService if there is no PropertyEditor if (editor == null&& conversionService ! =null&& newValue ! =null&& typeDescriptor ! =null) { TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue); if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) { try { The type conversion service returns when the conversion is complete, as explained below return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor); } catch (ConversionFailedException ex) { // fallback to default conversion logic below conversionAttemptEx = ex; } } } Object convertedValue = newValue; // Use PropertyEditor if PropertyEditor is available if(editor ! =null|| (requiredType ! =null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) { . // The transformation is done by editor convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor); } boolean standardConversion = false; if(requiredType ! =null) { // Try to apply some standard type conversion rules if appropriate. if(convertedValue ! =null) { if (Object.class == requiredType) { return (T) convertedValue; } ConvertIfNecessary is called recursively, and the result of the processing is collected 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 instanceofString && ! 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, null processing if (requiredType == Optional.class) { convertedValue = Optional.empty(); } } . } // Exception handling 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; } Copy the code
If we had configured a custom Converter, we would go into the #1 branch and have ConversionService perform the type conversion, using GenericConversionService as an example.
// GenericConversionService.java
@Override
@Nullable
public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
. // Finding a matching conveter from the cache, in the case of LocalDateTime, will find our custom localDateTimeConverter GenericConverter converter = getConverter(sourceType, targetType); if(converter ! =null) { // Complete the conversion by calling the real Converter through the utility method. At this point, the conversion from the source type to the target type is complete Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType); return handleResult(sourceType, targetType, result); } return handleConverterNotFound(source, sourceType, targetType); } Copy the code
Above is the handling mark @ RequestParam annotations RequestParamMethodArgumentResolver parsing process parameters.
Look at the below handle mark @ RequestBody to annotate RequestResponseBodyMethodProcessor analytic process, the parameters of the still cut from resolveArgument method.
// RequestResponseBodyMethodProcessor.java
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
parameter = parameter.nestedIfOptional(); // Finish parsing the parameters here 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); / / call the superclass AbstractMessageConverterMethodArgumentResolver complete argument parsing 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; } Copy the code
Below into the parent class AbstractMessageConverterMethodArgumentResolver source.
// AbstractMessageConverterMethodArgumentResolver.java
@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
. EmptyBodyCheckingHttpInputMessage message; try { message = new EmptyBodyCheckingHttpInputMessage(inputMessage); / / traverse HttpMessageConverter for(HttpMessageConverter<? > converter :this.messageConverters) { Class<HttpMessageConverter<? >> converterType = (Class<HttpMessageConverter<? >>) converter.getClass();GenericHttpMessageConverter<? > genericConverter = (converter instanceofGenericHttpMessageConverter ? (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); / / by actual MappingJackson2HttpMessageConverter calls the superclass AbstractJackson2HttpMessageConverter read method to finish, 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 { // Get the Java type of the target parameter to convert, such as LocalDateTime JavaType javaType = getJavaType(type, contextClass); // Call the readJavaType method of this class 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()); } } // Call the Jackson library to parse the HTTP JSON request information into the required parameter types. At this point, the JSON request is converted to the target Java type return this.objectMapper.readValue(inputMessage.getBody(), javaType); } .} Copy the code
conclusion
The parameters of the controller method is done by different HandlerMethodArgumentResolver parsing. If the parameter annotation @ RequestBody annotations, is actually through MappingJackson2HttpMessageConverter ObjectMapper incoming deserialization parsing json data into the target type. If the @requestParam annotation is annotated, it is done by injecting each Converter into The ConversionService during application initialization. Other HandlerMethodArgumentResolver is also each have each use, you can see the code again, in order to deepen the understanding.