Spring Boot 응용 프로그램 에서 Date 와 LocalDateTime 을 우아 하 게 사용 하 는 방법 에 대한 설명

자바 8 은 발 표 된 지 여러 해 가 되 었 지만 많은 사람들 이 개발 할 때DateSimpleDateFormat를 꾸준히 사용 하여 시간 조작 을 하고 있다.4567914)스 레 드 가 안전 한 것 이 아니 라SimpleDateFormat처리 시간 이 번 거 로 워 자바 8 은Date,LocalDateTime,LocalDate등 새로운 시간 조작 API 를 제공 했다.LocalTime이 든Date이 든 Spring Boot 응용 을 개발 할 때 모든 실체 류 의 날짜 필드 에LocalDate주 해 를 붙 여 전단 전송 값 과 날짜 필드 를 연결 시 키 고@DateTimeFormat주 해 를 붙 여 전단 으로 돌아 가 는 날짜 필드 형식 을 우리 가 원 하 는 시간 형식 으로 바 꿔 야 합 니 다.시간 과 날짜 유형 은 개발 에 사용 되 는 빈도 가 매우 높 습 니 다.필드 마다 이 두 개의 주 해 를 더 하면 매우 번 거 롭 습 니 다.전역 설정 의 처리 방식 이 있 습 니까?오늘 소개 해 드 리 겠 습 니 다.
주:본 고 는 Springboot 2.3.0 버 전 을 바탕 으로 합 니 다.
요청 방식 에 따라 서로 다른 설정 이 필요 합 니 다.다음은 JSON 방식 의 전 참 과 GET 요청 및 POST 폼 방식 의 전 참 두 가지 상황 으로 나 뉘 어 있 습 니 다.
JSON 방식 전 참
이 경우 유형 POST,Content-type 은 application/json 방식 의 요청 을 말한다.이러한 요청 에 대해 contrller 에@JsonFormat주 해 를 추가 하여 우리 가 요청 파 라 메 터 를 받 는 부분 변 수 를 표시 해 야 합 니 다.코드 는 다음 과 같 습 니 다.

@SpringBootApplication
@RestController
public class SpringbootDateLearningApplication {

 public static void main(String[] args) {
  SpringApplication.run(SpringbootDateLearningApplication.class, args);
 }
 
  /**
  * DateTime      
  */
 private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";

 /**
  * Date      
  */
 private static final String DEFAULT_DATE_PATTERN = "yyyy-MM-dd";

 /**
  * Time      
  */
 private static final String DEFAULT_TIME_PATTERN = "HH:mm:ss";

 public static class DateEntity {
  private LocalDate date;

  private LocalDateTime dateTime;

  private Date originalDate;

  public LocalDate getDate() {
   return date;
  }

  public void setDate(LocalDate date) {
   this.date = date;
  }

  public LocalDateTime getDateTime() {
   return dateTime;
  }

  public void setDateTime(LocalDateTime dateTime) {
   this.dateTime = dateTime;
  }

  public Date getOriginalDate() {
   return originalDate;
  }

  public void setOriginalDate(Date originalDate) {
   this.originalDate = originalDate;
  }

 }

 @RequestMapping("/date")
 public DateEntity getDate(@RequestBody DateEntity dateEntity) {
  return dateEntity;
 }
} 
기본 수신 과 반환 값 의 형식 이 모두@RequestBody이 라 고 가정 하면 다음 과 같은 몇 가지 방안 이 있 을 수 있 습 니 다.
application.yml 파일 설정
application.yml 파일 에 다음 과 같은 내용 을 설정 합 니 다.

spring:
 jackson:
 date-format: yyyy-MM-dd HH:mm:ss
 time-zone: GMT+8
소결:
  • Content-Type 은 application/json 의 POST 요청 을 지원 합 니 다.요청 매개 변수 문자열 과 되 돌아 오 는 형식 은 모두yyyy-MM-dd HH:mm:ss요청 매개 변수 가 다른 형식 이 라면yyyy-MM-dd HH:mm:ss문자열 은 400 Bad Request 이상 을 보고 합 니 다.
  • LocalDate 등 Java8 date API 는 지원 되 지 않 습 니 다.
  • Jackson 설정 추가
    
    /**
     * Jackson           ,    Post     json              json
     */
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
     return builder -> builder
       .serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
       .serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)))
       .serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)))
       .serializerByType(Date.class, new DateSerializer(false, new SimpleDateFormat(DEFAULT_DATETIME_PATTERN)))
       .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
       .deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)))
       .deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)))
       .deserializerByType(Date.class, new DateDeserializers.DateDeserializer(DateDeserializers.DateDeserializer.instance, new SimpleDateFormat(DEFAULT_DATETIME_PATTERN), DEFAULT_DATETIME_PATTERN))
       ;
    }
    소결:
  • Content-Type 은 application/json 의 POST 요청 을 지원 합 니 다.요청 매개 변수 문자열 과 되 돌아 오 는 형식 은 모두yyyy-MM-dd요청 매개 변수 가 다른 형식 이 라면yyyy-MM-dd HH:mm:ss문자열 은 400 Bad Request 이상 을 보고 합 니 다.
  • LocalDate 등 Java8 date API 를 지원 합 니 다.
  • PS:위의 방식 은 하나의yyyy-MM-ddBean 설정 을 통 해 이 루어 집 니 다.이 를 제외 하고 사용자 정의Jackson2ObjectMapperBuilderCustomizer를 통 해 이 루어 집 니 다.
    
    @Bean
    public MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter() {
     MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
     ObjectMapper objectMapper = new ObjectMapper();
     //     
     objectMapper.setTimeZone(TimeZone.getTimeZone("GMT+8:00"));
     //          
     objectMapper.setDateFormat(new SimpleDateFormat(DEFAULT_DATETIME_PATTERN));
    
     // Java8      
     JavaTimeModule javaTimeModule = new JavaTimeModule();
     javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)));
     javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)));
     javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)));
     javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)));
     javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)));
     javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)));
     objectMapper.registerModule(javaTimeModule);
    
     converter.setObjectMapper(objectMapper);
     return converter;
    }
    상기 몇 가지 방식 은 JSON 전 참 시의 전역 화 설정 을 실현 할 수 있 으 며,후 두 가지 코드 에 bean 을 추가 하 는 방식 을 추천 하 며,동시에 지원MappingJackson2HttpMessageConverterDate을 할 수 있 습 니 다.
    GET 요청 및 POST 폼 방식 참조
    이런 방식 은 위의 JSON 방식 과 스프링 부 트 에서 처리 하 는 방식 이 완전히 다르다.이전 JSON 방식 의 전 참 은LocalDate에서 jacksonHttpMessgeConverter을 통 해 http 요청 체 를 우리 가 contrller 에 쓴 매개 변수 대상 으로 바 꾸 었 는데 이런 방식 은ObjectMapper인터페이스(spring-core 에서 정 의 된 소스 유형(보통Converter을 목표 유형 으로 바 꾸 는 인터페이스)로 본질 적 인 차이 가 있다.
    사용자 정의 매개 변수 변환기(변환기)
    매개 변수 변환 기 를 사용자 정의 하여 위 에서 언급 한String인 터 페 이 스 를 실현 합 니 다.설정 류 에 다음 과 같은 몇 개의 bean 을 설정 합 니 다.예 는 다음 과 같 습 니 다.
    
    @Bean
    public Converter<String, Date> dateConverter() {
     return new Converter<>() {
      @Override
      public Date convert(String source) {
       SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
       try {
        return formatter.parse(source);
       } catch (Exception e) {
        throw new RuntimeException(String.format("Error parsing %s to Date", source));
       }
      }
     };
    }
    
    @Bean
    public Converter<String, LocalDate> localDateConverter() {
     return new Converter<>() {
      @Override
      public LocalDate convert(String source) {
       return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN));
      }
     };
    }
    
    @Bean
    public Converter<String, LocalDateTime> localDateTimeConverter() {
     return new Converter<>() {
      @Override
      public LocalDateTime convert(String source) {
       return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN));
      }
     };
    }
    동시에 controller 인 터 페 이 스 를 일부 파 라 메 터 를 추가 하면 인터페이스 에서 단독으로 변수 로 수신 하 는 것 도 정상적으로 전환 할 수 있다 는 것 을 알 수 있다.
    
    @RequestMapping("/date")
    public DateEntity getDate(
      LocalDate date,
      LocalDateTime dateTime,
      Date originalDate,
      DateEntity dateEntity) {
     System.out.printf("date=%s, dateTime=%s, originalDate=%s 
    ", date, dateTime, originalDate); return dateEntity; }
    소결:
  • GET 요청 및 POST 폼 방식 으로 요청 합 니 다.
  • LocalDate 등 Java8 date API 를 지원 합 니 다.
  • 사용org.springframework.core.convert.converter.Converter주해
    앞에서 언급 한 바 와 같이 GET 요청 및 POST 폼 방식 도@DateTimeFormat으로 처리 할 수 있 으 며,contrller 인터페이스 파라미터 나 실체 류 속성 에서 만 사용 할 수 있 습 니 다.예 를 들 어@DateTimeFormat.사용자 정의 매개 변수 변환기(Converter)를 사용 하면 Spring 은 이 방식 을 우선 사용 하여 처리 합 니 다.즉@DateTimeFormat(pattern = "yyyy-MM-dd") Date originalDate주석 이 적용 되 지 않 고 두 가지 방식 이 호 환 되 지 않 습 니 다.
    그렇다면 사용자 정의 매개 변수 변환 기 를 사용 했다 면 호 환@DateTimeFormat형식 으로 받 아들 이 시 겠 습 니까?우 리 는 앞의yyyy-MM-dd을 정규 일치 방식 으로 바 꿀 수 있다.이렇게 하면 좋 은 해결 방안 이 라 고 할 수 있다.예 를 들 어 다음 과 같다.
    
    /**
     *        
     */
    private static final String DATE_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])";
    
    /**
     *        
     */
    private static final String TIME_REGEX = "(20|21|22|23|[0-1]\\d):[0-5]\\d:[0-5]\\d";
    
    /**
     *           
     */
    private static final String DATE_TIME_REGEX = DATE_REGEX + "\\s" + TIME_REGEX;
    
    /**
     * 13         
     */
    private static final String TIME_STAMP_REGEX = "1\\d{12}";
    
    /**
     *         
     */
    private static final String YEAR_MONTH_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])";
    
    /**
     *      
     */
    private static final String YEAR_MONTH_PATTERN = "yyyy-MM";
    
    @Bean
    public Converter<String, Date> dateConverter() {
     return new Converter<String, Date>() {
      @SuppressWarnings("NullableProblems")
      @Override
      public Date convert(String source) {
       if (StrUtil.isEmpty(source)) {
        return null;
       }
       if (source.matches(TIME_STAMP_REGEX)) {
        return new Date(Long.parseLong(source));
       }
       DateFormat format;
       if (source.matches(DATE_TIME_REGEX)) {
        format = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
       } else if (source.matches(DATE_REGEX)) {
        format = new SimpleDateFormat(DEFAULT_DATE_FORMAT);
       } else if (source.matches(YEAR_MONTH_REGEX)) {
        format = new SimpleDateFormat(YEAR_MONTH_PATTERN);
       } else {
        throw new IllegalArgumentException();
       }
       try {
        return format.parse(source);
       } catch (ParseException e) {
        throw new RuntimeException(e);
       }
      }
     };
    }
    소결:
  • GET 요청 및 POST 폼 방식 으로 요청 하지만 사용 하 는 곳 마다dateConverter주 해 를 붙 여야 합 니 다.
  • 사용자 정의 매개 변수 변환기(Converter)와 호 환 되 지 않 습 니 다.
  • LocalDate 등 Java8 date API 를 지원 합 니 다.
  • 사용@DateTimeFormat협조@ControllerAdvice
    
    /*
     *      @ControllerAdvice
     */
    @ControllerAdvice
    @SpringBootApplication
    @RestController
    public class SpringbootDateLearningApplication {
     ...
     @InitBinder
     protected void initBinder(WebDataBinder binder) {
      binder.registerCustomEditor(LocalDate.class, new PropertyEditorSupport() {
       @Override
       public void setAsText(String text) throws IllegalArgumentException {
        setValue(LocalDate.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATE_PATTERN)));
       }
      });
      binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport() {
       @Override
       public void setAsText(String text) throws IllegalArgumentException {
        setValue(LocalDateTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)));
       }
      });
      binder.registerCustomEditor(LocalTime.class, new PropertyEditorSupport() {
       @Override
       public void setAsText(String text) throws IllegalArgumentException {
        setValue(LocalTime.parse(text, DateTimeFormatter.ofPattern(DEFAULT_TIME_PATTERN)));
       }
      });
      binder.registerCustomEditor(Date.class, new PropertyEditorSupport() {
       @Override
       public void setAsText(String text) throws IllegalArgumentException {
        SimpleDateFormat formatter = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
        try {
         setValue(formatter.parse(text));
        } catch (Exception e) {
         throw new RuntimeException(String.format("Error parsing %s to Date", text));
        }
       }
      });
     } 
     ...
    } 
    실제 응용 에서 우 리 는 위의 코드 를 부모 클래스 에 넣 고 모든 인 터 페 이 스 는 이 부모 클래스 를 계승 하여 전체적인 처리 효 과 를 얻 을 수 있다.원 리 는 AOP 와 유사 하 게 파라미터 가 handler 에 들 어가 기 전에 변환 할 때 우리 가 정의 한@initBinder을 사용 하여 처리 하 는 것 이다.
    소결:
  • GET 요청 및 POST 폼 방식 으로 요청 합 니 다.
  • LocalDate 등 Java8 date API 를 지원 합 니 다.
  • 부분 차별 화 처리
  • 앞의 전역 날짜 형식 에 따라 다음 과 같이 설정 되 어 있다 고 가정 합 니 다.PropertyEditorSupport그러나 특정한yyyy-MM-dd HH:mm:ss유형의 필드 는Date형식 으로 특수 처리 하여 받 거나 되 돌려 야 합 니 다.다음 과 같은 방안 을 선택 할 수 있 습 니 다.
    사용yyyy/MM/dd@DateTimeFormat주해
    
    @JsonFormat(pattern = "yyyy/MM/dd", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy/MM/dd HH:mm:ss")
    private Date originalDate;
    위 와 같이 필드 에@JsonFormat주 해 를 추가 할 수 있 고@DateTimeFormat주 해 를 추가 할 수 있 으 며,이 필드 의 수신 과 반환 날짜 형식 을 각각 지정 할 수 있 습 니 다.
    PS:@JsonFormat@JsonFormat주 해 는 모두 Spring Boot 가 제공 하 는 것 이 아니 라 Spring 응용 에서 도 사용 할 수 있 습 니 다.
    사용자 정의 매개 변수 변환기(Converter)를 사용 하면 Spring 은 이 방식 을 우선 사용 하여 처리 합 니 다.즉@DateTimeFormat주석 이 유효 하지 않 습 니 다.
    사용자 정의 직렬 화기 와 반 직렬 화기
    
    /**
     * {@link Date}     
     */
    public class DateJsonSerializer extends JsonSerializer<Date> {
     @Override
     public void serialize(Date date, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws
       IOException {
      SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
      jsonGenerator.writeString(dateFormat.format(date));
     }
    }
    
    /**
     * {@link Date}      
     */
    public class DateJsonDeserializer extends JsonDeserializer<Date> {
     @Override
     public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
      try {
       SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
       return dateFormat.parse(jsonParser.getText());
      } catch (ParseException e) {
       throw new IOException(e);
      }
     }
    }
    
    /**
     *     
     */
    @JsonSerialize(using = DateJsonSerializer.class)
    @JsonDeserialize(using = DateJsonDeserializer.class)
    private Date originalDate;
    위 에서 보 듯 이 필드 에서@DateTimeFormat@JsonSerialize주 해 를 사용 하여 직렬 화 와 반 직렬 화 를 지정 할 때 사용자 정의 직렬 화 기와 반 직렬 화 기 를 사용 할 수 있 습 니 다.
    마지막 으로 JSON 방식 과 GET 요청 및 POST 폼 방식 을 호 환 하 는 완전한 설정 을 다시 하 겠 습 니 다.
    
    @Configuration
    public class GlobalDateTimeConfig {
    
     /**
      *        
      */
     private static final String DATE_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[1-2][0-9]|3[0-1])";
    
     /**
      *        
      */
     private static final String TIME_REGEX = "(20|21|22|23|[0-1]\\d):[0-5]\\d:[0-5]\\d";
    
     /**
      *           
      */
     private static final String DATE_TIME_REGEX = DATE_REGEX + "\\s" + TIME_REGEX;
    
     /**
      * 13         
      */
     private static final String TIME_STAMP_REGEX = "1\\d{12}";
    
     /**
      *         
      */
     private static final String YEAR_MONTH_REGEX = "[1-9]\\d{3}-(0[1-9]|1[0-2])";
    
     /**
      *      
      */
     private static final String YEAR_MONTH_PATTERN = "yyyy-MM";
    
     /**
      * DateTime      
      */
     private static final String DEFAULT_DATETIME_PATTERN = "yyyy-MM-dd HH:mm:ss";
    
     /**
      * Date      
      */
     private static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    
     /**
      * Time      
      */
     private static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
    
     /**
      * LocalDate   ,    RequestParam PathVariable  
      */
     @Bean
     public Converter<String, LocalDate> localDateConverter() {
      return new Converter<String, LocalDate>() {
       @SuppressWarnings("NullableProblems")
       @Override
       public LocalDate convert(String source) {
        if (StringUtils.isEmpty(source)) {
         return null;
        }
        return LocalDate.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT));
       }
      };
     }
    
     /**
      * LocalDateTime   ,    RequestParam PathVariable  
      */
     @Bean
     public Converter<String, LocalDateTime> localDateTimeConverter() {
      return new Converter<String, LocalDateTime>() {
       @SuppressWarnings("NullableProblems")
       @Override
       public LocalDateTime convert(String source) {
        if (StringUtils.isEmpty(source)) {
         return null;
        }
        return LocalDateTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN));
       }
      };
     }
    
     /**
      * LocalDate   ,    RequestParam PathVariable  
      */
     @Bean
     public Converter<String, LocalTime> localTimeConverter() {
      return new Converter<String, LocalTime>() {
       @SuppressWarnings("NullableProblems")
       @Override
       public LocalTime convert(String source) {
        if (StringUtils.isEmpty(source)) {
         return null;
        }
        return LocalTime.parse(source, DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT));
       }
      };
     }
    
     /**
      * Date   ,    RequestParam PathVariable  
      */
     @Bean
     public Converter<String, Date> dateConverter() {
      return new Converter<String, Date>() {
       @SuppressWarnings("NullableProblems")
       @Override
       public Date convert(String source) {
        if (StringUtils.isEmpty(source)) {
         return null;
        }
        if (source.matches(TIME_STAMP_REGEX)) {
         return new Date(Long.parseLong(source));
        }
        DateFormat format;
        if (source.matches(DATE_TIME_REGEX)) {
         format = new SimpleDateFormat(DEFAULT_DATETIME_PATTERN);
        } else if (source.matches(DATE_REGEX)) {
         format = new SimpleDateFormat(DEFAULT_DATE_FORMAT);
        } else if (source.matches(YEAR_MONTH_REGEX)) {
         format = new SimpleDateFormat(YEAR_MONTH_PATTERN);
        } else {
         throw new IllegalArgumentException();
        }
        try {
         return format.parse(source);
        } catch (ParseException e) {
         throw new RuntimeException(e);
        }
       }
      };
     }
    
     /**
      * Json           ,    Post     json                 json
      */
     @Bean
     public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
      return builder -> builder.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
        .serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
        .serializerByType(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
        .serializerByType(Long.class, ToStringSerializer.instance)
        .deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATETIME_PATTERN)))
        .deserializerByType(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
        .deserializerByType(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
     }
    
    }
    소스 코드 분석
    전체 설정 을 어떻게 하 는 지 알 고 난 후에 우 리 는 debug 소스 코드 를 통 해 Spring MVC 가 어떻게 매개 변 수 를 연결 하 는 지 깊이 분석 할 것 입 니 다.
    여전히 위의 contrller 를 예 로 들 어 debug 를 진행 합 니 다.
    
    @RequestMapping("/date")
    public DateEntity getDate(
      LocalDate date,
      LocalDateTime dateTime,
      Date originalDate,
      DateEntity dateEntity) {
     System.out.printf("date=%s, dateTime=%s, originalDate=%s 
    ", date, dateTime, originalDate); return dateEntity; }
    다음은 요청 을 받 은 후 스 택 을 호출 하 는 관건 적 인 방법 입 니 다.
    
    // DispatcherServlet    
    doService:943, DispatcherServlet
    //     
    doDispatch:1040, DispatcherServlet
    //      (   、      、   )
    handle:87, AbstractHandlerMethodAdapter
    handleInternal:793, RequestMappingHandlerAdapter
    //            ,      
    invokeHandlerMethod:879, RequestMappingHandlerAdapter
    invokeAndHandle:105, ServletInvocableHandlerMethod 
    //     ,           
    invokeForRequest:134, InvocableHandlerMethod
    getMethodArgumentValues:167, InvocableHandlerMethod
    resolveArgument:121, HandlerMethodArgumentResolverComposite
    다음 에 우 리 는 관건 적 인@JsonDeserialize부분 부터 분석 하고 소스 코드 는 다음 과 같다.
    
    // InvocableHandlerMethod.java
    @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);
    }
    //     
    protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
      Object... providedArgs) throws Exception {
     //     handler method       ,       ,    、   
     MethodParameter[] parameters = getMethodParameters();
     if (ObjectUtils.isEmpty(parameters)) {
      return EMPTY_ARGS;
     }
     //         MethodParameter      
     Object[] args = new Object[parameters.length];
     for (int i = 0; i < parameters.length; i++) {
      MethodParameter parameter = parameters[i];
      parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
      args[i] = findProvidedArgument(parameter, providedArgs);
      if (args[i] != null) {
       continue;
      }
      // resolvers        ,HandlerMethodArgumentResolverComposite  ,      HandlerMethodArgumentResolver   。                         
      if (!this.resolvers.supportsParameter(parameter)) {
       throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
      }
      try {
       //   HandlerMethodArgumentResolverComposite     ,             
       args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
      }
      catch (Exception ex) {
     ......
      }
     }
     return args;
    }
    다음은invokeForRequest:134, InvocableHandlerMethod방법 소스 에 들 어가 야 합 니 다.
    
    // HandlerMethodArgumentResolverComposite.java
    @Override
    @Nullable
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
     //                    
     HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
     if (resolver == null) {
      throw new IllegalArgumentException("Unsupported parameter type [" +
        parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
     }
     //                   
     return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
    }
    //                 
    @Nullable
    private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
     //                          ,        
     HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
     if (result == null) {
      for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
       //     argumentResolvers  list              
       if (resolver.supportsParameter(parameter)) {
        result = resolver;
        this.argumentResolverCache.put(parameter, result);
        break;
       }
      }
     }
     return result;
    }
    HandlerMethodArgumentResolverComposite#resolveArgument에는 모두 26 개의 매개 변수 해석 기 가 있 습 니 다.다음은 흔히 볼 수 있 는 것 을 나열 합 니 다.
    
    this.argumentResolvers = {LinkedList@6072} size = 26
     0 = {RequestParamMethodArgumentResolver@6098} 
     1 = {RequestParamMapMethodArgumentResolver@6104} 
     2 = {PathVariableMethodArgumentResolver@6111} 
     3 = {PathVariableMapMethodArgumentResolver@6112} 
     ......
     7 = {RequestResponseBodyMethodProcessor@6116} 
     8 = {RequestPartMethodArgumentResolver@6117} 
     9 = {RequestHeaderMethodArgumentResolver@6118} 
     10 = {RequestHeaderMapMethodArgumentResolver@6119} 
     ......
     14 = {RequestAttributeMethodArgumentResolver@6123} 
     15 = {ServletRequestMethodArgumentResolver@6124} 
     ......
     24 = {RequestParamMethodArgumentResolver@6107} 
     25 = {ServletModelAttributeMethodProcessor@6133} 
    모든 매개 변수 해석 기 가argumentResolvers인 터 페 이 스 를 실현 했다.
    
    public interface HandlerMethodArgumentResolver {
    
     //                           
     boolean supportsParameter(MethodParameter parameter);
    
     //             
     @Nullable
     Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
     NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
    
    }
    여기 서 우 리 는 생각 을 정리 하고 방법 적 매개 변수 에 대한 분석 은 모두 하나하나 옮 겨 다 니 며 적당 한HandlerMethodArgumentResolver을 찾 아 완성 한 것 이다.예 를 들 어 매개 변수 에HandlerMethodArgumentResolver또는@RequestParam주석 이 표시 되 어 있 으 면 SpringMVC 는 서로 다른 매개 변수 해석 기로 해석 합 니 다.다음은 가장 자주 사용 하 는@RequestBody을 골 라 상세 한 분석 절 차 를 깊이 분석 하 겠 습 니 다.@PathVariable계승RequestParamMethodArgumentResolver,RequestParamMethodArgumentResolver인터페이스AbstractNamedValueMethodArgumentResolver방법 을 실현 했다.
    
    // AbstractNamedValueMethodArgumentResolver.java
    @Override
    @Nullable
    public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    
     //          ,         
     Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
     ......
     if (binderFactory != null) {
      //    DataBinder 
      WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name);
      try {
       //   DataBinder      ,    :   ,    ,    
       arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter);
      }
     ......
     }
    
     handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest);
    
     return arg;
    }
    
    // DataBinder.java
    @Override
    @Nullable
    public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
      @Nullable MethodParameter methodParam) throws TypeMismatchException {
     //      convertIfNecessary  ,        TypeConverterSupport
     return getTypeConverter().convertIfNecessary(value, requiredType, methodParam);
    }
    
    // TypeConverterSupport.java
    @Override
    @Nullable
    public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
      @Nullable MethodParameter methodParam) throws TypeMismatchException {
     //      convertIfNecessary  ,  MethodParameter        TypeDescriptor
     return convertIfNecessary(value, requiredType,
       (methodParam != null ? new TypeDescriptor(methodParam) : TypeDescriptor.valueOf(requiredType)));
    }
    // convertIfNecessary  
    @Nullable
    @Override
    public <T> T convertIfNecessary(@Nullable Object value, @Nullable Class<T> requiredType,
      @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException {
    
     Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate");
     try {
      //   TypeConverterDelegate convertIfNecessary  
      return this.typeConverterDelegate.convertIfNecessary(null, null, value, requiredType, typeDescriptor);
     }
     ......
    }
    다음은AbstractNamedValueMethodArgumentResolver의 소스 코드 로 들 어 갑 니 다.
    
    // TypeConverterDelegate.java
    @Nullable
    public <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue,
      @Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException {
    
     //                 PropertyEditor。         @ControllerAdvice  @initBinder     ,        ,      
     PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);
    
     ConversionFailedException conversionAttemptEx = null;
    
     //           ConversionService
     ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
     //     ,    PropertyEditor    ConversionService
     if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {
      TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
      if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
       try {
        // #1,              ,       
        return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
       }
       catch (ConversionFailedException ex) {
        // fallback to default conversion logic below
        conversionAttemptEx = ex;
       }
      }
     }
    
     Object convertedValue = newValue;
    
     //     ,   PropertyEditor   PropertyEditor
     if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) {
      ......
      //  editor     
      convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor);
     }
    
     boolean standardConversion = false;
    
     if (requiredType != null) {
      // Try to apply some standard type conversion rules if appropriate.
    
      if (convertedValue != null) {
       if (Object.class == requiredType) {
        return (T) convertedValue;
       }
       //      、         ,         ,    convertIfNecessary  ,       
       else if (requiredType.isArray()) {
        // Array required -> apply appropriate conversion of elements.
        if (convertedValue instanceof String && Enum.class.isAssignableFrom(requiredType.getComponentType())) {
         convertedValue = StringUtils.commaDelimitedListToStringArray((String) convertedValue);
        }
        return (T) convertToTypedArray(convertedValue, propertyName, requiredType.getComponentType());
       }
       else if (convertedValue instanceof Collection) {
        // Convert elements to target type, if determined.
        convertedValue = convertToTypedCollection(
          (Collection<?>) convertedValue, propertyName, requiredType, typeDescriptor);
        standardConversion = true;
       }
       else if (convertedValue instanceof Map) {
        // Convert keys and values to respective target type, if determined.
        convertedValue = convertToTypedMap(
          (Map<?, ?>) convertedValue, propertyName, requiredType, typeDescriptor);
        standardConversion = true;
       }
       if (convertedValue.getClass().isArray() && Array.getLength(convertedValue) == 1) {
        convertedValue = Array.get(convertedValue, 0);
        standardConversion = true;
       }
       if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) {
        // We can stringify any primitive value...
        return (T) convertedValue.toString();
       }
       else if (convertedValue instanceof String && !requiredType.isInstance(convertedValue)) {
     ......
       }
       else if (convertedValue instanceof Number && Number.class.isAssignableFrom(requiredType)) {
        convertedValue = NumberUtils.convertNumberToTargetClass(
          (Number) convertedValue, (Class<Number>) requiredType);
        standardConversion = true;
       }
      }
      else {
       // convertedValue == null,    
       if (requiredType == Optional.class) {
        convertedValue = Optional.empty();
       }
      }
    
     ......
     }
     //     
     if (conversionAttemptEx != null) {
      if (editor == null && !standardConversion && requiredType != null && Object.class != requiredType) {
       throw conversionAttemptEx;
      }
      logger.debug("Original ConversionService attempt failed - ignored since " +
        "PropertyEditor based conversion eventually succeeded", conversionAttemptEx);
     }
    
     return (T) convertedValue;
    }
    사용자 정의HandlerMethodArgumentResolver를 설정 하면\#1 의 분기 에 들 어가resolveArgument에서 유형 전환 을 합 니 다.하위 클래스TypeConverterDelegate를 예 로 들 면.
    
    // GenericConversionService.java
    @Override
    @Nullable
    public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
     ......
     //            conveter, LocalDateTime  ,         localDateTimeConverter
     GenericConverter converter = getConverter(sourceType, targetType);
     if (converter != null) {
      //            converter      。  ,              
      Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
      return handleResult(sourceType, targetType, result);
     }
     return handleConverterNotFound(source, sourceType, targetType);
    }
    이상 은 레이 블Converter주 해 를 처리 하 는 매개 변수ConversionService분석 절차 입 니 다.
    다음은 레이 블GenericConversionService주 해 를 처리 하 는 매개 변수@RequestParam의 분석 절 차 를 살 펴 보 겠 습 니 다.여전히RequestParamMethodArgumentResolver방법 으로 들 어 갑 니 다.
    
    // RequestResponseBodyMethodProcessor.java
    @Override
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
      NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
    
     parameter = parameter.nestedIfOptional();
     //           
     Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
     ......
     return adaptArgumentIfNecessary(arg, parameter);
    }
    
    @Override
    protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
      Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
    
     HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
     Assert.state(servletRequest != null, "No HttpServletRequest");
     ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);
     //     AbstractMessageConverterMethodArgumentResolver      
     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;
    }
    다음은 부류@RequestBody의 소스 코드 로 들 어 갑 니 다.
    
    // AbstractMessageConverterMethodArgumentResolver.java
    @Nullable
    protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
      Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
     ......
     EmptyBodyCheckingHttpInputMessage message;
     try {
      message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
     //   HttpMessageConverter
      for (HttpMessageConverter<?> converter : this.messageConverters) {
       Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
       GenericHttpMessageConverter<?> genericConverter =
         (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
       if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
         (targetClass != null && converter.canRead(targetClass, contentType))) {
        if (message.hasBody()) {
         HttpInputMessage msgToUse =
           getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
         //    MappingJackson2HttpMessageConverter    AbstractJackson2HttpMessageConverter read      ,
         body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
           ((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
         body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
        }
        else {
         body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
        }
        break;
       }
      }
     }
     ......
     return body;
    }
    
    // AbstractJackson2HttpMessageConverter.java
    @Override
    public Object read(Type type, @Nullable Class<?> contextClass, HttpInputMessage inputMessage)
      throws IOException, HttpMessageNotReadableException {
     //           Java  , LocalDateTime 
     JavaType javaType = getJavaType(type, contextClass);
     //      readJavaType  
     return readJavaType(javaType, inputMessage);
    }
    
    // AbstractJackson2HttpMessageConverter.java
    private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException {
     try {
      if (inputMessage instanceof MappingJacksonInputMessage) {
       Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView();
       if (deserializationView != null) {
        return this.objectMapper.readerWithView(deserializationView).forType(javaType).
          readValue(inputMessage.getBody());
       }
      }
      //   jackson  , HTTP json              。  , json       Java  
      return this.objectMapper.readValue(inputMessage.getBody(), javaType);
     }
     ......
    }
    총결산
    controller 방법의 매개 변 수 는 서로 다른RequestResponseBodyMethodProcessor을 통 해 해석 된다.매개 변수 가resolveArgument주 해 를 표시 하면 실제AbstractMessageConverterMethodArgumentResolverHandlerMethodArgumentResolver를 통 해 json 형식 데이터 의 반 직렬 화 를 목표 유형 으로 해석 합 니 다.@RequestBody주 해 를 표시 하면 응용 초기 화 시 주입MappingJackson2HttpMessageConverter의 하나ObjectMapper를 통 해 이 루어 집 니 다.다른@RequestParam도 각각 쓸모 가 있 으 니 관련 코드 를 다시 봐 서 깊이 이해 할 수 있 습 니 다.
    Spring Boot 애플 리 케 이 션 에서 Date 와 LocalDateTime 을 우아 하 게 사용 하 는 튜 토리 얼 에 대한 자세 한 설명 은 여기까지 입 니 다.더 많은 Spring Boot 사용 Date 와 LocalDateTime 내용 은 이전 글 을 검색 하거나 아래 글 을 계속 찾 아 보 세 요.앞으로 도 많은 응원 부 탁 드 리 겠 습 니 다!

    좋은 웹페이지 즐겨찾기