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 supportedyyyy-MM-dd HH:mm:ssIf the request parameter is in another format, e.gyyyy-MM-ddIf 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 supportedyyyy-MM-dd HH:mm:ssIf the request parameter is in another format, e.gyyyy-MM-ddIf 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@DateTimeFormatannotations

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@DateTimeFormatAnnotation.
  • Incompatible with Converter.
  • Java8 date apis such as LocalDate are supported.

use@ControllerAdviceCooperate 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@DateTimeFormatand@JsonFormatannotations

@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.