====== SpringBoot and JSON ====== * [[springframework:springboot|SpringBoot]] 와 [[java:jackson|Java Jackson JSON Library]] JSON ===== 날짜 시간 처리 Date Time Format ===== * Java의 기존 ''java.util.Date''와 신규 Java 8 시간에 대한 처리 * ''java.util.Date''는 기본적으로 Unix timestamp(long 값)으로 표현된다. ==== 기본 설정 ==== * SpringBoot 2.x 는 starter 추가 하면 Jackson ''ObjectMapper'' 자동 생성 설정 api 'org.springframework.boot:spring-boot-starter-json' * 구버전 SpringBoot 에서는 의존성에 다음 추가해야 JSR310 날짜/시간 지원됨. 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 사용 ==== * 제일 좋은 것은 처음부터 ''WRITE_DATES_AS_TIMESTAMPS=false'' 상태로 운영하는 것이다. ''application.yml'' spring.jackson.serialization.write-dates-as-timestamps: false * SpringBoot 2.x 는 이값이 **기본 ''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 받던것을 올바로 처리하게 변경해야만 한다. * Formatter와 Jackson Serializers/Deserializers 에 대해서 테스트를 만들어 보증하는 것이 좋다. [[https://gist.github.com/kwon37xi/aa359e364b7e81f79085fb04cc710037|FormatterSerializerTestControllerTest.groovy]] ==== 모든 타입에 대해 일관성 있는 커스텀 형식 지원 ==== * 만약, 모든 날짜 객체에 대해 일관성있는 포맷을 지정하고자 한다면(ISO 포맷) 다음과 같이 한다. @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 사용시 Module 추가 ==== * ''Jackson2ObjectMapperBuilder'' 사용시 모듈을 추가하려면 ''modules'' 메소드 혹은 ''modulesToInstall''를 사용한다. * ''modules(...)'' : Spring과 Jackson default 모듈 탐색을 모두 취소하고 오직 이 메소드 인자로 전달된 모듈만 탑재 * ''modulesToInstall(...)'' : Spring(JSR-310 or jodatime 등 자동추가)과 Jackson default 모듈 탑재를 수행하고 그 뒤에 명시된 모듈을 추가 탑재 ==== @EnableWebMvc ==== ''@EnableWebMvc''와 ''WebMvcConfigurerAdapter''를 사용하는 순간 더이상 SpringBoot가 아니고 Spring 이기 때문에 위의 설정이 먹지 않게 된다. [[springframework:springboot:mvc|SpringBoot and Spring MVC]] 참조 [[https://github.com/spring-projects/spring-boot/issues/2116|Spring Boot Issue 2116]] 이때는 ''WebMvcConfigurerAdapter#configureMessageConverters''를 override 해야한다. @Configuration @EnableWebMvc public class WebConfig extends WebMvcConfigurerAdapter { @Override public void configureMessageConverters(List> 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)); } } ==== Controller Get Parameter 에 대한 포맷지원 ==== 이것은 Jackson JSON 직렬화와는 다른 문제이므로 포맷을 지정해야한다. @GetMapping("/test") public Result test(@RequestParam("datetime") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime datetime) { // ... } // 파라미터를 ?datetime=2018-07-11T20:22:55.123 형태로 호출 ==== @JsonComponent ==== * [[https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/jackson/JsonComponent.html|@JsonComponent]] 애노테이션을 통해서 Serializer와 Deserializer를 bean으로 등록하면 자동으로 Jackson ObjectMapper에 해당 컴포넌트를 Serializer와 Deserializer로 등록해준다. * [[https://www.baeldung.com/spring-boot-jsoncomponent|Using @JsonComponent in Spring Boot | Baeldung]] ==== WebFlux Jackson 설정 ==== * [[springframework:webflux|Spring WebFlux]] Jackson 설정 * ''@EnableWebFlux''는 SpringBoot에서는 해서는 안 된다. * https://stackoverflow.com/a/43241547/1051402 @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"); } } ==== 기타 참조 ==== * [[http://www.popit.kr/rest-api-%eb%82%a0%ec%a7%9c%ec%8b%9c%ea%b0%84-%ed%91%9c%ed%98%84-%ec%a0%95%ed%95%98%ea%b8%b0/|REST API 날짜/시간 표현 정하기]] * [[http://javacan.tistory.com/478|자바캔(Java Can Do IT) :: 스프링 부트 2.0과 1.5의 Jackson JSON 날짜 타입 포맷 설정]] * [[http://lewandowski.io/2016/02/formatting-java-time-with-spring-boot-using-json/|Formatting Java Time with Spring Boot using JSON | Craftsmanship Archives]] * [[https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/htmlsingle/#howto-customize-the-jackson-objectmapper|How to customize the Jackson ObjectMapper]] * [[https://dzone.com/articles/spring-web-service-response-filtering|Spring Web Service Response Filtering - DZone Java]] - 응답 Json Filter * [[https://www.baeldung.com/spring-boot-formatting-json-dates|Formatting JSON Dates in Spring Boot | Baeldung]]