====== 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]]