java.util.Date
와 신규 Java 8 시간에 대한 처리java.util.Date
는 기본적으로 Unix timestamp(long 값)으로 표현된다.ObjectMapper
자동 생성 설정api 'org.springframework.boot:spring-boot-starter-json'
compile('com.fasterxml.jackson.datatype:jackson-datatype-jsr310') compile('com.fasterxml.jackson.datatype:jackson-datatype-jdk8')
@GetMapping("/dateTest") public MyClock dateTest2() { return new MyClock(); } @Getter public static class MyClock { private Date date; private LocalDateTime localDateTime; private LocalDate localDate; private LocalTime localTime; private OffsetDateTime offsetDateTime; private ZonedDateTime zonedDateTime; public MyClock() { Instant instant = Instant.now(); date = Date.from(instant); localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault()); localTime = localDateTime.toLocalTime(); localDate = localDateTime.toLocalDate(); offsetDateTime = OffsetDateTime.ofInstant(instant, ZoneId.systemDefault()); zonedDateTime = ZonedDateTime.ofInstant(instant, ZoneId.systemDefault()); } }
WRITE_DATES_AS_TIMESTAMPS=false
상태로 운영하는 것이다. application.yml
spring.jackson.serialization.write-dates-as-timestamps: false
false
이고 Java 8 date/time 에 대해 자동으로 ISO 포맷을 사용하게 한다. simpleDateFormat
만 따로 ISO와 동일하게 설정해주면 된다.simpleDateFormat(), dateFormat()
을 설정하면 ObjectMapper
가 non-thread-safe 하게 돼 버린다. 하지말고, java.util.Date
도 사용하지 말 것.java.util.Date
,java.time.*
모두 ISO 혹은 그 유사 포맷으로 직렬화된다.{ "date":"2018-07-18T06:16:33.647+0000", ## java.util.Date가 약간 다르게 출력됨. "localDateTime":"2018-07-18T15:16:33.647", "localDate":"2018-07-18", "localTime":"15:16:33.647", "offsetDateTime":"2018-07-18T15:16:33.647+09:00", "zonedDateTime":"2018-07-18T15:16:33.647+09:00" }
WRITE_DATES_AS_TIMESTAMPS
옵션을 변경할 경우 기존에 작동하던 java.util.Date
의 포맷이 변경되므로 프로젝트가 이미 운영중일 때는 java.util.Date
를 받는 모든 부분에 대해 기존 unix timestamp 받던것을 올바로 처리하게 변경해야만 한다.@SpringBootApplication public class DemoApplication implements Jackson2ObjectMapperBuilderCustomizer { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } // TODO : 아래 예에서는 deserializer가 빠져있다!! deserializer도 추가해줘야 일관성있게 된다. // ISO 포맷을 사용할경우 SpringBoot 2 에서 기본적으로 ISO Format으로 세팅하기 때문에 굳이 복잡하게 설정할 필요없이 // java.util.Date 에 대해서만 일관성있게 설정한다. @Override public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) { LocalDateTimeSerializer localDateTimeSerializer = new LocalDateTimeSerializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME); // LocalDateSerializer localDateSerializer = new LocalDateSerializer(DateTimeFormatter.ISO_LOCAL_DATE); // LocalTimeSerializer localTimeSerializer = new LocalTimeSerializer(DateTimeFormatter.ISO_LOCAL_TIME); // OffsetDateTimeSerializer offsetDateTimeSerializer = new CustomOffsetDateTimeSerializer(); // ZonedDateTimeSerializer zonedDateTimeSerializer = new CustomZonedDateTimeSerializer(); jacksonObjectMapperBuilder .timeZone(TimeZone.getDefault()) // 올바른 타임존을 설정해야 offset/zoned datetime이 올바로 설정됨. .locale(Locale.getDefault()) .simpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); // .serializerByType(LocalDateTime.class, localDateTimeSerializer) // .serializerByType(LocalDate.class, localDateSerializer) // .serializerByType(LocalTime.class, localTimeSerializer) // .serializerByType(OffsetDateTime.class, offsetDateTimeSerializer) // .serializerByType(ZonedDateTime.class, zonedDateTimeSerializer); } // 불필요 public static class CustomOffsetDateTimeSerializer extends OffsetDateTimeSerializer { protected CustomOffsetDateTimeSerializer() { super(OffsetDateTimeSerializer.INSTANCE, false, DateTimeFormatter.ISO_OFFSET_DATE_TIME); } } // 불필요 public static class CustomZonedDateTimeSerializer extends ZonedDateTimeSerializer { public CustomZonedDateTimeSerializer() { // ISO_OFFSET_DATE_TIME 로 바꾸면 OffsetDateTime과 동일하게 출력됨. super(ZonedDateTimeSerializer.INSTANCE, false, DateTimeFormatter.ISO_DATE_TIME, true); } } }
{ "date":"2018-07-18T15:11:49.693+09:00", "localDateTime":"2018-07-18T15:11:49.693", "localDate":"2018-07-18", "localTime":"15:11:49.693", "offsetDateTime":"2018-07-18T15:11:49.693+09:00", "zonedDateTime":"2018-07-18T15:11:49.693+09:00" }
DateTimeForamtter.ISO_ZONED_DATE_TIME
은 표준이 아니다. jackson은 ZonedDateTime
에 대해 기본으로 ISO_OFF_SET_DATE_TIME
을 사용한다.Jackson2ObjectMapperBuilder
사용시 모듈을 추가하려면 modules
메소드 혹은 modulesToInstall
를 사용한다.modules(…)
: Spring과 Jackson default 모듈 탐색을 모두 취소하고 오직 이 메소드 인자로 전달된 모듈만 탑재modulesToInstall(…)
: Spring(JSR-310 or jodatime 등 자동추가)과 Jackson default 모듈 탑재를 수행하고 그 뒤에 명시된 모듈을 추가 탑재
@EnableWebMvc
와 WebMvcConfigurerAdapter
를 사용하는 순간 더이상 SpringBoot가 아니고 Spring 이기 때문에 위의 설정이 먹지 않게 된다. SpringBoot and Spring MVC 참조 Spring Boot Issue 2116
이때는 WebMvcConfigurerAdapter#configureMessageConverters
를 override 해야한다.
@Configuration @EnableWebMvc public class WebConfig extends WebMvcConfigurerAdapter { @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { super.configureMessageConverters(converters); converters.add(new MappingJackson2HttpMessageConverter(jackson2ObjectMapperBuilder().build())); } @Bean public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() { return new Jackson2ObjectMapperBuilder() .failOnUnknownProperties(false) // SpringBoot default .featuresToDisable(MapperFeature.DEFAULT_VIEW_INCLUSION) // SpringBoot default .featuresToEnable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) // SpringBoot default .serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) .serializerByType(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ISO_DATE)); } }
이것은 Jackson JSON 직렬화와는 다른 문제이므로 포맷을 지정해야한다.
@GetMapping("/test") public Result test(@RequestParam("datetime") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime datetime) { // ... } // 파라미터를 ?datetime=2018-07-11T20:22:55.123 형태로 호출
@EnableWebFlux
는 SpringBoot에서는 해서는 안 된다.@Configuration public class Config implements WebFluxConfigurer, Jackson2ObjectMapperBuilderCustomizer { @Override public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) { jacksonObjectMapperBuilder .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) .timeZone(TimeZone.getDefault()) .locale(Locale.getDefault()) .simpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX"); } }