Contents
Overview
The original intention of writing this article is to understand how to serialize and deserialize the JSR 310 date and time system in Spring Boot2. There are two Spring MVC application scenarios:
- Use @RequestBody to obtain JSON parameters and encapsulate them into entity objects;
- Use @ResponseBody to convert the data returned to the front end into JSON data.
For some basic types of data such as Integer and String, Spring MVC can solve it through some built-in converters without users needing to care. However, for date and time types (such as LocalDateTime), due to the changeable formats, no built-in converters are available, so users need to do it themselves. It’s time to configure and process.
Reading this article assumes that the reader has a preliminary understanding of how to use Jackson.
test environment
This article uses Spring Boot2.6.6 version, and the locked Jackson version is as follows:
<jackson-bom.version> 2.13 .2 .20220328 </jackson-bom.version>
Jackson needs to introduce dependencies to handle JSR 310 date and time:
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version> 2.13 .2 </version> </dependency>
Spring Boot automatic configuration
In the spring-boot-autoconfigure package, Jackson is automatically configured:
package org.springframework.boot.autoconfigure.jackson; @Configuration(proxyBeanMethods = false) @ConditionalOnClass(ObjectMapper.class) public class JacksonAutoConfiguration { // Detailed code omitted }
There is a piece of code that configures ObjectMapper
@Bean @Primary @ConditionalOnMissingBean ObjectMapper jacksonObjectMapper (Jackson2ObjectMapperBuilder builder) { return builder.createXmlMapper( false ).build(); }
You can see that ObjectMapper is built by Jackson2ObjectMapperBuilder.
Further down you will see the following code:
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(Jackson2ObjectMapperBuilder.class) static class JacksonObjectMapperBuilderConfiguration { @Bean @Scope("prototype") @ConditionalOnMissingBean Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder (ApplicationContext applicationContext, List<Jackson2ObjectMapperBuilderCustomizer> customizers) { Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder (); builder.applicationContext(applicationContext); customize(builder, customizers); return builder; } private void customize (Jackson2ObjectMapperBuilder builder, List<Jackson2ObjectMapperBuilderCustomizer> customizers) { for (Jackson2ObjectMapperBuilderCustomizer customizer : customizers) { customizer.customize(builder); } } }
It is found that Jackson2ObjectMapperBuilder is created here, and the customize(builder, customizers) method is called, passing in Lis<Jackson2ObjectMapperBuilderCustomizer> to customize the ObjectMapper.
Jackson2ObjectMapperBuilderCustomizer is an interface with only one method. The source code is as follows:
@FunctionalInterface public interface Jackson2ObjectMapperBuilderCustomizer { /** * Customize the JacksonObjectMapperBuilder. * @param jacksonObjectMapperBuilder the JacksonObjectMapperBuilder to customize */ void customize (Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) ; }
To put it simply, Spring Boot will collect all the Jackson2ObjectMapperBuilderCustomizer implementation classes in the container and uniformly set the Jackson2ObjectMapperBuilder to achieve customized ObjectMapper. Therefore, if we want to customize the ObjectMapper, we only need to implement the Jackson2ObjectMapperBuilderCustomizer interface and register it with the container.
Custom Jackson configuration class
Without further ado, let’s go straight to the code:
@Component public class JacksonConfig implements Jackson2ObjectMapperBuilderCustomizer , Ordered { /** Default date and time format*/ private final String dateTimeFormat = "yyyy-MM-dd HH:mm:ss" ; /** Default date format*/ private final String dateFormat = "yyyy-MM-dd" ; /* *Default time format*/ private final String timeFormat = "HH:mm:ss" ; @Override public void customize (Jackson2ObjectMapperBuilder builder) { // Set the serialization and deserialization format of the java.util.Date time class builder.simpleDateFormat(dateTimeFormat); // JSR 310 date and time processing JavaTimeModule javaTimeModule = new JavaTimeModule (); DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(dateTimeFormat); javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer (dateTimeFormatter)); javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer (dateTimeFormatter)); DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern(dateFormat); javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer (dateFormatter)); javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer (dateFormatter)); DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern(timeFormat); javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer (timeFormatter)); javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer (timeFormatter)); builder.modules(javaTimeModule); // Globally convert the Long type to String to solve the problem of precision loss of the Long type passed to the front end after serialization builder.serializerByType(BigInteger.class, ToStringSerializer.instance); builder.serializerByType(Long.class,ToStringSerializer.instance); } @Override public int getOrder () { return 1 ; } }
This configuration class implements three personalized configurations:
- Set the serialization and deserialization format of the java.util.Date time class;
- JSR 310 date and time processing;
- Globally convert the Long type to String to solve the problem of missing precision of the Long type passed to the front end after serialization.
Of course, readers can continue to customize other configurations according to their own needs.
test
JSR 310 date and time is used here for testing.
Create entity class User
@Data @NoArgsConstructor @AllArgsConstructor public class User { private Long id; private String name; private LocalDate localDate; private LocalTime localTime; private LocalDateTime localDateTime; }
Create controller UserController
@RestController @RequestMapping("user") public class UserController { @PostMapping("test") public User test ( @RequestBody User user) { System.out.println(user.toString()); return user; } }
Front-end parameter passing
{ "id" : 184309536616640512 , "name" : "Bagua Program" , "localDate" : "2023-03-01" , "localTime" : "09:35:50" , "localDateTime" : "2023-03-01 09:35:50" }
Backend returns data
{ "id" : "184309536616640512" , "name" : "Basic Program" , "localDate" : "2023-03-01" , "localTime" : "09:35:50" , "localDateTime" : "2023-03 -01 09:35:50" }
You can see that whatever data is passed in to the front end will be returned to the back end. The only difference is that the id returned by the back end is a string, which can prevent precision loss problems in the front end (such as JavaScript).
At the same time, it also proves that LocalDateTime and other date and time types have returned normally after visiting the backend (not rejected, nor beaten and deformed by the backend, for example, turned into a timestamp and returned, causing my mother not to recognize it) .
Front-end confession rejected
If JacksonConfig is not configured, Spring MVC will report the following exception after trying the built-in converter to no avail:
JSON parse error: Cannot deserialize value of typejava.time.LocalDateTime
The data returned to the front end is as follows:
{ "timestamp" : "2023-03-01T09:53:02.158+00:00" , "status" : 400 , "error" : "Bad Request" , "path" : "/user/test" }
You know, rejected.
Summarize
Core class ObjectMapper
ObjectMapper is the most important class of the jackson-databind module. It completes almost all functions of data processing.
Although Spring MVC performs a series of dazzling operations when processing the JSON parameters passed by the front end, the operation is as fierce as a tiger, and ultimately relies on ObjectMapper to complete serialization and deserialization. Therefore, you only need to customize the ObjectMapper provided by Spring Boot by default.
Do not override default configuration
We implement the Jackson2ObjectMapperBuilderCustomizer interface and register it with the container for personalized customization. Spring Boot will not overwrite the default ObjectMapper configuration, but merge and enhance it. Specifically, it will be sorted according to the Order priority of the Jackson2ObjectMapperBuilderCustomizer implementation class, so the above JacksonConfig The configuration class also implements the Ordered interface.
The default Jackson2ObjectMapperBuilderCustomizerConfiguration priority is 0, so if we want to override the configuration, just set the priority greater than 0.
Note: In the SpringBoot2 environment, do not inject custom ObjectMapper objects into the container, as this will overwrite the original ObjectMapper configuration!
QueryString format parameters
It should be noted that Jackson cannot solve the problem of QueryString format parameters, because Spring uses the Converter type conversion mechanism for such parameters, which is another way to bind parameters (sorry, Jackson did not help on this way) .
A custom parameter type converter is needed to handle date and time types, which requires another article to be introduced.