Springboot about the date and time format handling summary

Project is used as a data type DTO LocalDateTime series in time, but always being given the parameters received SpringMVC, type conversion to configure the global time, try the following approach.

Note: This article is based on Springboot2.x test, if you can not take effect may be due to low spring version. PS: If your Controller in LocalDate what type of parameter annotations (RequestParam, PathVariable, etc.) do not increase, but also to be wrong, because by default, this parameter is parsed using ModelAttributeMethodProcessorprocessed, and this processor through reflection out instantiate an object, then the object Convert individual parameters, but not LocalDate class constructor can not be instantiated therefore given reflection! ! !
This article quoted from source code analysis section using Spring in LocalDateTime, LocalDate parameters such as the reference , the original writing is very exciting, it is recommended to read carefully! ! !

to achieve goals

  • Date is transferred into the reference request String (specified format), supports get, post (content-type = application / json)
  • Return data into a specified date and time format character record type Date
  • Support Java8 date API, such LocalTimeas: , localDateandLocalDateTime

POST and GET request form datetime string format conversion

To distinguish between this situation and time as Json string, since the front end of the rear end turn json pojo bottom using Json serialization Jackson tool ( HttpMessgeConverter); and the time as a normal request parameter string passed, the conversion Converter, both of the approach is different.

Use custom parameter converter (Converter)

Achieve org.springframework.core.convert.converter.Converter, custom parameter converter, as follows:

@Configuration
public class DateConverterConfig {
    @Bean
    public Converter<String, LocalDate> localDateConverter() {
        return new Converter<>() {
            @Override
            public LocalDate convert(String source) {
                return LocalDate.parse(source, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
            }
        };
    }

    @Bean
    public Converter<String, LocalDateTime> localDateTimeConverter() {
        return new Converter<>() {
            @Override
            public LocalDateTime convert(String source) {
                return LocalDateTime.parse(source, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
            }
        };
    }

}

Comments: The above two bean will be injected into the parameters of the parser spring mvc (it seems to be called ParameterConversionService), when the incoming string to be converted to LocalDateTime class, spring will call the Converter to convert this into parameters.

May also be transmitted to the distal end of string matching the regular, such as yyyy-MM-dd HH: mm: ss, yyyy-MM-dd, HH: mm: ss, etc., to match. To accommodate a variety of scenarios.

@Component
public class DateConverter implements Converter<String, Date> {

    private static final String dateFormat = "yyyy-MM-dd HH:mm:ss";

    @Override
    public Date convert(String value) {
        /**
         * 可对value进行正则匹配,支持日期、时间等多种类型转换
         * @param value
         * @return
         */
        SimpleDateFormat formatter = new SimpleDateFormat(dateFormat);
        try {
            return formatter.parse(value);
        } catch (Exception e) {
            throw new RuntimeException(String.format("parser %s to Date fail", value));
        }
    }
}

Use Spring Annotations

Own annotations as spring @DateTimeFormat (pattern = "yyyy-MM-dd"), as follows:

@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date startDate;

If you use custom parameters reformer, Spring will give priority to using this approach to deal with, namely Spring annotation does not take effect.

Use ControllerAdvice with initBinder

@ControllerAdvice
public class GlobalExceptionHandler {

    @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("yyyy-MM-dd")));
            }
        });
        binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                setValue(LocalDateTime.parse(text, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
            }
        });
        binder.registerCustomEditor(LocalTime.class, new PropertyEditorSupport() {
            @Override
            public void setAsText(String text) throws IllegalArgumentException {
                setValue(LocalTime.parse(text, DateTimeFormatter.ofPattern("HH:mm:ss")));
            }
        });
    }
}

Can be seen from the name, this is the controller do circumcision (there is also a global exception catching), converted before entering the parameter handler; we convert the corresponding object.

JSON parameters and return values ​​into global process

Request type: post, content-type = application / json, with background @RequestBodyreception, and receives the default format of the return value:yyyy-MM-dd HH:mm:ss

Modify application.yml file

Add the following in application.propertities file:

spring:
    jackson:
        date-format: yyyy-MM-dd HH:mm:ss
        time-zone: GMT+8
  • Support (content-type = application / json ) request format yyyy-MM-dd HH:mm:ssstring with background @RequestBodyreception, and returns the value converted date yyyy-MM-dd HH:mm:ssformat String;
  • Not supported (content-type = application / json) in yyyy-MM-dd other types of character string into a request DATE;
  • It does not support java8日期 api;

Jackson's use of JSON serialization and de-serialization

@Configuration
public class JacksonConfig {

    /** 默认日期时间格式 */
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    /** 默认日期格式 */
    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    /** 默认时间格式 */
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    @Bean
    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        ObjectMapper objectMapper = new ObjectMapper();

        // 忽略json字符串中不识别的属性
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        // 忽略无法转换的对象 
        objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
        // PrettyPrinter 格式化输出
        objectMapper.configure(SerializationFeature.INDENT_OUTPUT, true);
        // NULL不参与序列化
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

        // 指定时区
        objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8:00"));
        // 日期类型字符串处理
        objectMapper.setDateFormat(new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT));

        // java8日期日期处理
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
        javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
        javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
        javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
        objectMapper.registerModule(javaTimeModule);

        converter.setObjectMapper(objectMapper);
        return converter;
    }
}

to sum up:

  • Support (content-type = application / json ) request format yyyy-MM-dd HH:mm:ssstring with background @RequestBodyreception, and returns the value Switch Date yyyy-MM-dd HH:mm:ssFormat String;
  • Support java8日期 api;
  • Not supported (content-type = application / json ) requests yyyy-MM-ddand other types of string to a Date;

The above two methods as JSON global processing parameters, use two recommended, especially for large-scale projects global provided in the base package.

JSON and return into the reference value of the local differences in treatment

Scene: If the overall processing time of the date format is: yyyy-MM-dd HH:mm:ssbut a field to receive or return date yyyy-MM-dd.

method one

Use springboot own annotations @JsonFormat(pattern = "yyyy-MM-dd"), as follows:

@JsonFormat(pattern = "yyyy-MM-dd", timezone="GMT+8")
private Date releaseDate;

Comments: springboot provided by default, powerful, meet common scenarios to use, and specify the time zone.

Second way

Date custom serialization and deserialization, as follows:

/**
 * 日期序列化
 */
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));
    }
}

/**
 * 日期反序列化
 */
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 RuntimeException(e);
        }
    }
}

/**
 * 使用方式
 */
@JsonSerialize(using = DateJsonSerializer.class)
@JsonDeserialize(using = DateJsonDeserializer.class)
private Date releaseDate;

Date and time format handling the complete configuration

@Configuration
public class DateHandlerConfig {

    /** 默认日期时间格式 */
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    /** 默认日期格式 */
    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    /** 默认时间格式 */
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    /**
     * LocalDate转换器,用于转换RequestParam和PathVariable参数
     */
    @Bean
    public Converter<String, LocalDate> localDateConverter() {
        return new Converter<>() {
            @Override
            public LocalDate convert(String source) {
                return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT));
            }
        };
    }

    /**
     * LocalDateTime转换器,用于转换RequestParam和PathVariable参数
     */
    @Bean
    public Converter<String, LocalDateTime> localDateTimeConverter() {
        return new Converter<>() {
            @Override
            public LocalDateTime convert(String source) {
                return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT));
            }
        };
    }

    /**
     * LocalTime转换器,用于转换RequestParam和PathVariable参数
     */
    @Bean
    public Converter<String, LocalTime> localTimeConverter() {
        return new Converter<>() {
            @Override
            public LocalTime convert(String source) {
                return LocalTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT));
            }
        };
    }

    /**
     * Date转换器,用于转换RequestParam和PathVariable参数
     */
    @Bean
    public Converter<String, Date> dateConverter() {
        return new Converter<>() {
            @Override
            public Date convert(String source) {
                SimpleDateFormat format = new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT);
                try {
                    return format.parse(source);
                } catch (ParseException e) {
                    throw new RuntimeException(e);
                }
            }
        };
    }


    /**
     * Json序列化和反序列化转换器,用于转换Post请求体中的json以及将我们的对象序列化为返回响应的json
     */
    @Bean
    public ObjectMapper objectMapper(){
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        objectMapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);

        //LocalDateTime系列序列化和反序列化模块,继承自jsr310,我们在这里修改了日期格式
        JavaTimeModule javaTimeModule = new JavaTimeModule();
        javaTimeModule.addSerializer(LocalDateTime.class,new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
        javaTimeModule.addSerializer(LocalDate.class,new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        javaTimeModule.addSerializer(LocalTime.class,new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
        javaTimeModule.addDeserializer(LocalDateTime.class,new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
        javaTimeModule.addDeserializer(LocalDate.class,new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
        javaTimeModule.addDeserializer(LocalTime.class,new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));


        //Date序列化和反序列化
        javaTimeModule.addSerializer(Date.class, new JsonSerializer<>() {
            @Override
            public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
                SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT);
                String formattedDate = formatter.format(date);
                jsonGenerator.writeString(formattedDate);
            }
        });
        javaTimeModule.addDeserializer(Date.class, new JsonDeserializer<>() {
            @Override
            public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
                SimpleDateFormat format = new SimpleDateFormat(DEFAULT_DATE_TIME_FORMAT);
                String date = jsonParser.getText();
                try {
                    return format.parse(date);
                } catch (ParseException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        objectMapper.registerModule(javaTimeModule);
        return objectMapper;
    }
}

Expansion Source: In-depth research data binding process SpringMVC

Next, enter debug mode to see how mvc our request to bind the parameter to the method of our controller layer into the Senate;

Write a simple controller, make a break and see method call stack:

    @GetMapping("/getDate")
    public LocalDateTime getDate(@RequestParam LocalDate date,
                                 @RequestParam LocalDateTime dateTime,
                                 @RequestParam Date originalDate) {
        System.out.println(date);
        System.out.println(dateTime);
        System.out.println(originalDate);
        return LocalDateTime.now();
    }

After the call interface, we look at some of the key methods stack method call:

//进入DispatcherServlet
doService:942, DispatcherServlet
//处理请求
doDispatch:1038, DispatcherServlet
//生成调用链(前处理、实际调用方法、后处理)
handle:87, AbstractHandlerMethodAdapter
//反射获取到实际调用方法,准备开始调用
invokeHandlerMethod:895, RequestMappingHandlerAdapter
invokeAndHandle:102, ServletInvocableHandlerMethod
//这里是关键,参数从这里开始获取到
invokeForRequest:142, InvocableHandlerMethod
doInvoke:215, InvocableHandlerMethod
//这个是Java reflect调用,因此一定是在这之前获取到的参数
invoke:566, Method

According to the above analysis, it was found invokeForRequest:142, InvocableHandlerMethodthe code used here is to get the actual parameter:

    @Nullable
    public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
            Object... providedArgs) throws Exception {
        //这个方法是获取参数的,在这里下个断
        Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
        if (logger.isTraceEnabled()) {
            logger.trace("Arguments: " + Arrays.toString(args));
        }
        //这里开始调用方法
        return doInvoke(args);
    }

This approach is to enter and see what to do:

protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
    //获取方法参数数组,包含了入参信息,比如类型、泛型等等
    MethodParameter[] parameters = getMethodParameters();
    //这个用来存放一会从request parameter转换的参数
    Object[] args = new Object[parameters.length];
    for (int i = 0; i < parameters.length; i++) {
      MethodParameter parameter = parameters[i];
      parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
      //这里看起来没啥卵用(providedArgs为空)
      args[i] = resolveProvidedArgument(parameter, providedArgs);
      //这里开始获取到方法实际调用的参数,步进
      if (this.argumentResolvers.supportsParameter(parameter)) {
        //从名字就看出来:参数解析器解析参数
        args[i] = this.argumentResolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
        continue;
      }
    }
    return args;
}

Enter resolveArgument a look:

public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                              NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
  //根据方法入参,获取对应的解析器
  HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
  //开始解析参数(把请求中的parameter转为方法的入参)
  return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}

Here for the corresponding parameter parser according to the parameters and see how the internal acquired:

//遍历,调用supportParameter方法,跟进看看
for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
  if (methodArgumentResolver.supportsParameter(parameter)) {
    result = methodArgumentResolver;
    this.argumentResolverCache.put(parameter, result);
    break;
  }
}

Here, in the argument parser Finding no suitable parser! So, what are the parameters parser it (when I tested have 26)? ? ? I have listed several important see, it is not very familiar! ! !

{RequestParamMethodArgumentResolver@7686} 
{PathVariableMethodArgumentResolver@8359} 
{RequestResponseBodyMethodProcessor@8366} 
{RequestPartMethodArgumentResolver@8367} 

We enter the most commonly used a parser to see his supportsParameter method is found to obtain the corresponding parameter annotation parser.

    public boolean supportsParameter(MethodParameter parameter) {
        //如果参数拥有注解@RequestParam,则走这个分支(知道为什么上文要对RequestParam和Json两种数据区别对待了把)
        if (parameter.hasParameterAnnotation(RequestParam.class)) {
            //这个似乎是对Optional类型的参数进行处理的
            if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
                RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
                return (requestParam != null && StringUtils.hasText(requestParam.name()));
            }
            else {
                return true;
            }
        }
        //......
    }

That is, for @RequestParamand @RequestBodyas well as @PathVariableparameter annotation, SpringMVC parser will use different parameters for data binding!
So, what are the three parser Converter analytical parameters to use it? We look at each Entry into the three parsers:
first look RequestParamMethodArgumentResolverfound WebDataBinder internal use data binding, use the bottom is ConversionService (that is, our local injection Converter)

WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
//通过DataBinder进行数据绑定的
arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
//跟进convertIfNecessary()
public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
                                @Nullable MethodParameter methodParam) throws TypeMismatchException {

  return getTypeConverter().convertIfNecessary(value, requiredType, methodParam);
}
//继续跟进,看到了把
ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {
  TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
  if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
    try {
      return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
    }
    catch (ConversionFailedException ex) {
      // fallback to default conversion logic below
      conversionAttemptEx = ex;
    }
  }
}

Then look RequestResponseBodyMethodProcessorfound HttpMessageConverter converter type is used:

//resolveArgument方法内部调用下面进行参数解析
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());

//step into readWithMessageConverters(),我们看到这里的Converter是HttpMessageConverter
for (HttpMessageConverter<?> converter : this.messageConverters) {
  Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
  GenericHttpMessageConverter<?> genericConverter =
    (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
  if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
      (targetClass != null && converter.canRead(targetClass, contentType))) {
    if (message.hasBody()) {
      HttpInputMessage msgToUse =
        getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
      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;
  }
}

Finally, a look PathVariableMethodArgumentResolverfound to be consistent and go RequestParam execution path (both of which are inherited from AbstractNamedValueMethodArgumentResolver parsers), so the code is not posted.

to sum up

If you want to convert parameter request came to us to specify the type, according to the Senate notes to distinguish:

  • If requestBody, then by arranging ObjectMapper (this thing will be injected into the HttpMessagConverter Jackson inside, i.e. MappingJackson2HttpMessageConverterin) to implement the serialization and deserialization Json data format;
  • If RequestParam or PathVariable type parameters, the parameter conversion achieved by arranging Converter (which will be injected into ConversionService the Converter).

Guess you like

Origin www.cnblogs.com/christopherchan/p/12404804.html