Jackson 在 Spring Boot 中错误地序列化了 ZonedDateTime
Jackson serializes a ZonedDateTime wrongly in Spring Boot
我有一个带有 Spring Boot 和 Jetty 的简单应用程序。我有一个简单的控制器返回一个具有 Java 8 ZonedDateTime
:
的对象
public class Device {
// ...
private ZonedDateTime lastUpdated;
public Device(String id, ZonedDateTime lastUpdated, int course, double latitude, double longitude) {
// ...
this.lastUpdated = lastUpdated;
// ...
}
public ZonedDateTime getLastUpdated() {
return lastUpdated;
}
}
在我的 RestController
中,我只有:
@RequestMapping("/devices/")
public @ResponseBody List<Device> index() {
List<Device> devices = new ArrayList<>();
devices.add(new Device("321421521", ZonedDateTime.now(), 0, 39.89011333, 24.438176666));
return devices;
}
我原以为 ZonedDateTime
会根据 ISO 格式进行格式化,但我得到的是 class 的整个 JSON 转储,如下所示:
"lastUpdated":{"offset":{"totalSeconds":7200,"id":"+02:00","rules":{"fixedOffset":true,"transitionRules":[],"transitions":[]}},"zone":{"id":"Europe/Berlin","rules":{"fixedOffset":false,"transitionRules":[{"month":"MARCH","timeDefinition":"UTC","standardOffset":{"totalSeconds":3600,"id":"+01:00","rules":{"fixedOffset":true,"transitionRules":[],"transitions":[]}},"offsetBefore":{"totalSeconds":3600,"id":"+01:00","rules":{"fixedOffset":true,"transitionRules":[],"transitions":[]}},"offsetAfter":{"totalSeconds":7200,"id":"+02:00", ...
我只有一个 spring-boot-starter-web
应用程序,使用 spring-boot-starter-jetty
并排除 spring-boot-starter-tomcat
。
为什么 Jackson 在 Spring Boot 中表现如此?
** 更新**
对于那些正在寻找如何解决这个问题的完整分步指南的人,我在提出问题后发现了这个:
http://lewandowski.io/2016/02/formatting-java-time-with-spring-boot-using-json/
有图书馆jackson-datatype-jsr310。试试吧。
此库涵盖了新的日期时间 API 并且还包括 ZonedDateTime
的序列化程序。
您只需添加 JavaTimeModule
:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
更新
将日期时间转换为 ISO-8601
string you should disable WRITE_DATES_AS_TIMESTAMPS
feature. You can easily do by either overriding ObjectMapper
bean or by using application properties:
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS = false
如果您不依赖 SpringBoot 的自动配置功能 - 您没有在配置文件中提供 spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS = false
属性 - 或者出于任何原因创建 ObjectMapper
手动实例。您可以通过编程方式禁用此功能,如下所示:
ObjectMapper m = new ObjectMapper();
m.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
这是给杰克逊的 2.8.7
上面已经提到了答案,但我认为它缺少一些信息。对于那些希望以多种形式(不仅仅是 ZonedDateTime)解析 Java 8 个时间戳的人。您需要在 POM 中使用最新版本的 jackson-datatype-jsr310
并注册以下模块:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
测试此代码
@Test
void testSeliarization() throws IOException {
String expectedJson = "{\"parseDate\":\"2018-12-04T18:47:38.927Z\"}";
MyPojo pojo = new MyPojo(ZonedDateTime.parse("2018-12-04T18:47:38.927Z"));
// serialization
assertThat(objectMapper.writeValueAsString(pojo)).isEqualTo(expectedJson);
// deserialization
assertThat(objectMapper.readValue(expectedJson, MyPojo.class)).isEqualTo(pojo);
}
请注意,您可以在 Spring 或 dropwizard 中全局配置对象映射器来实现此目的。我还没有找到一种干净的方法来在不注册自定义(反)序列化程序的情况下将其作为字段上的注释。
对于杰克逊 2.10
及以上,
parent pom.xml
<!-- https://github.com/FasterXML/jackson-bom -->
<dependencyManagement>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.10.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencyManagement>
模块pom.xml
<!-- https://github.com/FasterXML/jackson-modules-java8 -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
JsonMapper 创建,可能在你的 @Configuration
class
@Bean
public JsonMapper jsonMapper() {
return JsonMapper.builder()
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.addModule(new JavaTimeModule())
.build();
}
进一步阅读:
application.yml
中的设置spring.jackson.serialization.write-dates-as-timestamps=false
对我们的项目没有帮助。很可能是因为有其他库与 Jackson 一起工作:Swagger / OpenAPI / OpenAPI Generator。
有用的是将 RequestMappingHandlerAdapter
的 @EventListener
添加到 @SpringBootApplication
class。
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
@SpringBootApplication
public class SpringBootInitializer {
@Autowired
private RequestMappingHandlerAdapter handlerAdapter;
public static void main(String[] args) {
SpringApplication.run(SpringBootInitializer.class, args);
}
@EventListener
public void handleContextRefresh(ContextRefreshedEvent event) {
// see https://github.com/FasterXML/jackson-modules-java8/issues/11#issuecomment-913199874
// spring.jackson.serialization.write-dates-as-timestamps=false setting does not work in our configuration (probably because of Swagger / OpenAPI / OpenAPI Generator libraries used)
handlerAdapter
.getMessageConverters()
.forEach(c -> {
if (c instanceof MappingJackson2HttpMessageConverter jsonMessageConverter) {
ObjectMapper objectMapper = jsonMessageConverter.getObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
});
}
}
我在这里找到了这个解决方案 — https://github.com/FasterXML/jackson-modules-java8/issues/11#issuecomment-913199874。
我有一个带有 Spring Boot 和 Jetty 的简单应用程序。我有一个简单的控制器返回一个具有 Java 8 ZonedDateTime
:
public class Device {
// ...
private ZonedDateTime lastUpdated;
public Device(String id, ZonedDateTime lastUpdated, int course, double latitude, double longitude) {
// ...
this.lastUpdated = lastUpdated;
// ...
}
public ZonedDateTime getLastUpdated() {
return lastUpdated;
}
}
在我的 RestController
中,我只有:
@RequestMapping("/devices/")
public @ResponseBody List<Device> index() {
List<Device> devices = new ArrayList<>();
devices.add(new Device("321421521", ZonedDateTime.now(), 0, 39.89011333, 24.438176666));
return devices;
}
我原以为 ZonedDateTime
会根据 ISO 格式进行格式化,但我得到的是 class 的整个 JSON 转储,如下所示:
"lastUpdated":{"offset":{"totalSeconds":7200,"id":"+02:00","rules":{"fixedOffset":true,"transitionRules":[],"transitions":[]}},"zone":{"id":"Europe/Berlin","rules":{"fixedOffset":false,"transitionRules":[{"month":"MARCH","timeDefinition":"UTC","standardOffset":{"totalSeconds":3600,"id":"+01:00","rules":{"fixedOffset":true,"transitionRules":[],"transitions":[]}},"offsetBefore":{"totalSeconds":3600,"id":"+01:00","rules":{"fixedOffset":true,"transitionRules":[],"transitions":[]}},"offsetAfter":{"totalSeconds":7200,"id":"+02:00", ...
我只有一个 spring-boot-starter-web
应用程序,使用 spring-boot-starter-jetty
并排除 spring-boot-starter-tomcat
。
为什么 Jackson 在 Spring Boot 中表现如此?
** 更新**
对于那些正在寻找如何解决这个问题的完整分步指南的人,我在提出问题后发现了这个: http://lewandowski.io/2016/02/formatting-java-time-with-spring-boot-using-json/
有图书馆jackson-datatype-jsr310。试试吧。
此库涵盖了新的日期时间 API 并且还包括 ZonedDateTime
的序列化程序。
您只需添加 JavaTimeModule
:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
更新
将日期时间转换为 ISO-8601
string you should disable WRITE_DATES_AS_TIMESTAMPS
feature. You can easily do by either overriding ObjectMapper
bean or by using application properties:
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS = false
如果您不依赖 SpringBoot 的自动配置功能 - 您没有在配置文件中提供 spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS = false
属性 - 或者出于任何原因创建 ObjectMapper
手动实例。您可以通过编程方式禁用此功能,如下所示:
ObjectMapper m = new ObjectMapper();
m.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
这是给杰克逊的 2.8.7
上面已经提到了答案,但我认为它缺少一些信息。对于那些希望以多种形式(不仅仅是 ZonedDateTime)解析 Java 8 个时间戳的人。您需要在 POM 中使用最新版本的 jackson-datatype-jsr310
并注册以下模块:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
测试此代码
@Test
void testSeliarization() throws IOException {
String expectedJson = "{\"parseDate\":\"2018-12-04T18:47:38.927Z\"}";
MyPojo pojo = new MyPojo(ZonedDateTime.parse("2018-12-04T18:47:38.927Z"));
// serialization
assertThat(objectMapper.writeValueAsString(pojo)).isEqualTo(expectedJson);
// deserialization
assertThat(objectMapper.readValue(expectedJson, MyPojo.class)).isEqualTo(pojo);
}
请注意,您可以在 Spring 或 dropwizard 中全局配置对象映射器来实现此目的。我还没有找到一种干净的方法来在不注册自定义(反)序列化程序的情况下将其作为字段上的注释。
对于杰克逊 2.10
及以上,
parent pom.xml
<!-- https://github.com/FasterXML/jackson-bom -->
<dependencyManagement>
<dependency>
<groupId>com.fasterxml.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>2.10.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencyManagement>
模块pom.xml
<!-- https://github.com/FasterXML/jackson-modules-java8 -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
JsonMapper 创建,可能在你的 @Configuration
class
@Bean
public JsonMapper jsonMapper() {
return JsonMapper.builder()
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.addModule(new JavaTimeModule())
.build();
}
进一步阅读:
application.yml
中的设置spring.jackson.serialization.write-dates-as-timestamps=false
对我们的项目没有帮助。很可能是因为有其他库与 Jackson 一起工作:Swagger / OpenAPI / OpenAPI Generator。
有用的是将 RequestMappingHandlerAdapter
的 @EventListener
添加到 @SpringBootApplication
class。
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
@SpringBootApplication
public class SpringBootInitializer {
@Autowired
private RequestMappingHandlerAdapter handlerAdapter;
public static void main(String[] args) {
SpringApplication.run(SpringBootInitializer.class, args);
}
@EventListener
public void handleContextRefresh(ContextRefreshedEvent event) {
// see https://github.com/FasterXML/jackson-modules-java8/issues/11#issuecomment-913199874
// spring.jackson.serialization.write-dates-as-timestamps=false setting does not work in our configuration (probably because of Swagger / OpenAPI / OpenAPI Generator libraries used)
handlerAdapter
.getMessageConverters()
.forEach(c -> {
if (c instanceof MappingJackson2HttpMessageConverter jsonMessageConverter) {
ObjectMapper objectMapper = jsonMessageConverter.getObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
});
}
}
我在这里找到了这个解决方案 — https://github.com/FasterXML/jackson-modules-java8/issues/11#issuecomment-913199874。