It's never easy to deal with time serialization in a Java project. On the one hand, we have to face a variety of time formats, such as year, month, hour, minute, second, millisecond, nanosecond, week, and annoying time zones. If you are not careful, you may get a lot of exceptions. On the other hand, Java providesDate
andLocalDateTime
The time types of the two versions correspond to different serialized configurations, and just figuring out these configurations is a troublesome. But most of the time, we have to deal with this bunch of configurations.
As a start, let's take a look at the available configurations and the corresponding effects.
Time string configuration
Prepare a simple interface to show the effect.
@Slf4j @RestController @RequestMapping("/boom") public class BoomController { @Operation(summary = "boom") @GetMapping public BoomData getBoomData() { return new BoomData(()); } @Operation(summary = "boom") @PostMapping public BoomData postBoomData(@RequestBody BoomData boomData) { ("boomData: {}", boomData); return boomData; } } @Data @NoArgsConstructor @AllArgsConstructor public class BoomData { private Date date; private LocalDateTime localDateTime; private LocalDate localDate; private LocalTime localTime; public BoomData(Clock clock) { = new Date(()); = (clock); = (clock); = (clock); } }
The above involves two types of time:
-
Date
Represents the old version date type, and similar onesCalendar
, has been with Java for a long time and has a wide range of usage. But relatively speaking, the design does not keep up with the times. For example, the variable value leads to thread unsafe, and the month starts from 0 is a bit abnormal. -
LocalDateTime
representThe new version of the package is introduced in JDK 8. The new time type solves the design flaws of the older version type, while adding rich APIs to improve ease of use.
There is a little difference between the two types in terms of recorded information:
Date
The time accuracy is milliseconds, and the internal is actually a long type timestamp. In addition, time zone information is also recorded, and theDate = timestamp + timezone
. If no time zone is provided, the system time zone is used by default.-
LocalDateTime
The time accuracy is nanoseconds, and 7 integers are used internally to record the time:- int year
- short month
- short day
- byte hour
- byte minute
- byte second
- int nano
Can be simply recorded as
LocalDateTime = year + month + day + hour + minute + second + nano
. (It should actually beLocalDateTime = LocalDate + LocalTime
,LocalDate = year + month + day
,LocalTime = hour + minute + second + nano
。)LocalDateTime has no time zone information, which is also the meaning of Local in the class name, which means using the local time zone. If you need time zone information, you can use
ZonedDateTime
type,ZonedDateTime = LocalDateTime + tz
。
After understanding the differences between the two version time types, then look at their serialization differences.
JSON Serialization
Call the GET interface to get the default serialization result.
{ "date": "2024-10-10T21:07:08.781+08:00", "localDateTime": "2024-10-10T21:07:08.781283", "localDate": "2024-10-10", "localTime": "21:07:08.781263" }
By default, the time field is serialized into a time string, but the format is different. Spring Boot uses Jackson to perform JSON serialization, and has different formatting rules for different time types:
-
Date
By default, formatted according to ISO standards -
LocalDateTime
Also processed according to ISO standards, accurate to microseconds, less time zone -
LocalDate
andLocalTime
Similar to LocalDateTime
The so-called ISO standard refers toISO 8601 Standard, an international standard that specializes in handling date and time formats. Combine the time and datesyyyy-MM-dd'T'HH:mm:
Format processing, such as2024-10-10T21:07:08.781+08:00
, where the letter T is the date and time separator, the date is represented as year-month-day, and the time is represented as time: minute: seconds. milliseconds. In the formatXXX
Refers to time zone information, for East 8, it is expressed as+08:00
。
By default, when calling the POST interface, it is also necessary to ensure that the JSON string in the body processes the time field in accordance with the format of ISO 8601 in order to be deserialized normally, otherwise Spring Boot will throw an exception. Of course, the requirements for the time format are not so strict. Time zones, microseconds, milliseconds, and seconds can be omitted, and they can be deserialized normally, but T cannot be omitted, and year, month, day, time and division cannot be omitted.
When the interface calls the unified standards on both ends, ISO 8601 does not perform badly, but it encounters domestic Internet preferencesyyyy-MM-dd HH:mm:ss
Format, you will get oneHttpMessageNotReadableException
, the JVM will prompt youJSON parse error: Cannot deserialize value of type XXX ...
。
If you want to joinyyyy-MM-dd HH:mm:ss
The easiest way to use it@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
. The @JsonFormat annotation is used to specify the sequence format of the time type, and is valid for both the Date type and the LocalDateTime type.
public class BoomData { @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date date; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime localDateTime; @JsonFormat(pattern = "yyyy-MM-dd") private LocalDate localDate; @JsonFormat(pattern = "HH:mm:ss") private LocalTime localTime; }
At this time, you can GET the time string that satisfies the format
{ "date": "2024-11-20 15:15:57", "localDateTime": "2024-11-20 23:15:57", "localDate": "2024-11-20", "localTime": "23:15:57" }
The POST request is also processed normally.
It seems that @JsonFormat has no bad effect. The problem is that it is a little bit cumbersome and each time field needs to be configured once. Fortunately, spring boot supports global settings of Jackson time serialization formats:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss # Global time format time-zone: GMT+8 # Specify the default time zone, unless the time field has specified the time zone, this time zone will be used during JSON serialization.
Fortunately, @JsonFormat has higher priority than global configuration, allowing us to implement certain requirements that require special formats.
It seems that only combination-format
and @JsonFormat, we can do everything. No one knows time serialization better than me!
Unfortunately,-format
The new version of the time type is not supported. Yes, in 2024, the distanceIt has been ten years since the package was released, and Spring's serialized configuration still does not support the LocalDateTime type. If you want to serialize the LocalDateTime type, the easiest way is to use @JsonFormat. Because @JsonFormat is an annotation provided by Jackson. Spring does nothing about it.
After complaining, consider how to configure the formatting rules of LocalDateTime globally. There are many solutions, the easiest one is to explicitly tell Jackson, LocalDateTime and other types to serialize and deserialize according to a certain format.
// Probably a class like JacksonConfig@Bean public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { return builder -> { // formatter DateTimeFormatter dateFormatter = ("yyyy-MM-dd"); DateTimeFormatter dateTimeFormatter = ("yyyy-MM-dd HH:mm:ss"); DateTimeFormatter timeFormatter = ("HH:mm:ss"); // deserializers (new LocalDateDeserializer(dateFormatter)); (new LocalDateTimeDeserializer(dateTimeFormatter)); (new LocalTimeDeserializer(timeFormatter)); // serializers (new LocalDateSerializer(dateFormatter)); (new LocalDateTimeSerializer(dateTimeFormatter)); (new LocalTimeSerializer(timeFormatter)); }; }
The above code builds different types for three typesDateTimeFormatter
(Format tool provided by the package, thread-safe), and then bind the serializer and the deserializer for each type.
Now the date type of the Local system is consistent with Date performance.
To sum up, when JSON serialization:
- If the Date type is used, you can use it
-format
Combination with @JsonFormat to adapt to different formatting requirements - If you use LocalDateTime and other types, you need to configure Jackson, bind the serializer and deserializer, and then combine it with @JsonFormat to do whatever you want
But it is not over yet, it is not the beginning of the end, it is just the end of the beginning~
Request parameters
In addition to JSON serialization, there is another scenario that also involves time serialization. That is the time field in the request parameter. The most common thing is that the Controller method is not useful.@RequestBody
Tagged object parameters, such as GET requests, such as form submission (application/x-www-form-urlencoded
) POST request.
For easy display, add a new interface method in the BoomController.
@GetMapping("query") public BoomData queryBoomData(BoomData boomData) { ("boomData: {}", boomData); return boomData; }
A more commonly used way of writing Query interface. Try calling it.
GET http://localhost:8080/boom/query?localDateTime=2024-10-10T21:07:08.781283&date=2024-10-10T21:07:08.781+08:00
An error is reported, field 'date': rejected value [2024-10-10T21:07:08.781+08:00].
Try again
GET http://localhost:8080/boom/query?localDateTime=2024-10-10T21:07:08.781283&date=2024-10-10 21:07:08
Or an error is reported, field 'date': rejected value [2024-10-10 21:07:08].
What format can prevent errors?
GET http://localhost:8080/boom/query?localDateTime=2024-10-10T21:07:08.781283&date=10/10/2024 21:07:08
That's right, use itdd/MM/yyyy
format. Because the request parameters are not belong to the JSON serialization tube, but are handled by Spring MVC. Spring MVC default Date type format isdd/MM/yyyy
。
It's easy to modify.@DateTimeFormat
, provided by Spring, specializes in processing time parameter formatting.
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date date; @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime localDateTime; @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate localDate; @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalTime localTime;
Now, the request is processed normallyhttp://localhost:8080/boom/query?localDateTime=2024-10-10 21:07:08&date=2024-10-10 21:07:08&localDate=2024-10-10&localTime=11:09:15
。
It's time to find global configuration again.
spring: mvc: format: date: yyyy-MM-dd HH:mm:ss # Valid for Date and LocalDate types, LocalDate ignores the time part time: HH:mm:ss # works for LocalTime and OffsetTime date-time: yyyy-MM-dd HH:mm:ss # LocalDateTime, OffsetDateTime, and ZonedDateTime
Just select on demand.
To summarize, for the time field in the GET request parameter and the time field in the form submit POST request, you can use it/time/date-time
to configure the global format.
- Only the Date type is used in the request, only the configuration is required
- If used
The types in the package need to be selected according to the type
For scenarios where global configuration is not used, use @DateTimeFormat to specify a separate time format.
Let's use timestamp
The above is the case where time is passed using a time string. Next, let’s discuss using the timestamp format.
Let’s first understand the concept of timestamps:
GMT time is used to represent the time zone. For example, GMT+8 refers to the time in the East 8th zone. GMT alone can also be regarded as GMT+0, which is the time zone 0, which is located in Greenwich, UK.
UTC time is the same concept as GMT and is also used to represent time zones, except that UTC is more precise. Similarly, UTC+8 can represent East 8, and UTC alone represents 0 time zone
Unix Epoch, a specific time point, 00:00:00 UTC(+0), January 1, 1970, which is the 0-time zone New Year's Day 1970. This time point is often used for the time starting point of a computer system, like 0 on the axis.
After guiding the above three concepts, the meaning of timestamps is easy to explain.The number of milliseconds that passes from the Unix period(or seconds, commonly used by computers). Think of time as a long axis, the position of 0 is the Unix century, and after that, every millisecond in the real world corresponds to a point in the timeline.
Time stamps are represented by integers, a long integer that has the function of a time string. Therefore, time stamps can also be used to pass time information.
If I swear that timestamps are better than time strings, it is definitely a very subjective judgment, but using timestamps in interfaces does have some shiny advantages.
- Time zone irrelevance, the value of the timestamp is fixed to UTC+0 time zone, no matter which time zone is located, the same time stamp, the same time stamp. In this way, the time zone can be considered only when displaying, and no time zone needs to be considered at other times.
- Small size, a long value is enough, which is shorter than the time string
- Good compatibility, no need to consider complex formatting rules
Some disadvantages that cannot be ignored:
- Poor readability, timestamps are not as intuitive as time strings, and require some auxiliary conversion tools, such as browser console
- Second-level timestamps and millisecond timestamps may be confused, so you must agree on it before use.
Using long-type timestamps does not require serialization considerations. Most platforms can properly handle long-type serialization. But sometimes, it is more convenient to use clear types such as Date and LocalDateTime in the code than long. So there may be a requirement: use time types in code and use timestamps when serializing. That is, use Date in the DTO class and long in the JSON string.
and using the time string type, this requirement is also divided into two situations:
- JSON serialization conversion
- Request parameter conversion
The two must be handled separately.
Timestamps in JSON serialization
Spring provides a configuration item that controls Jackson to process the time type as a timestamp when serialized.
-dates-as-timestamps=true
At this point, the date in the GET request becomes"date": 1728572627475
, the timestamp can be correctly identified when POST.
However, only Date has such generous treatment.The type of bag is still facing the dilemma of doing it yourself and getting enough food and clothing.
After turning on write-dates-as-timestamps, types such as LocalDateTime will be serialized into a shaping array (recall the simple formula of LocalDateTime).
{ "date": 1728572627475, "localDateTime": [ 2024, 10, 10, 23, 3, 47, 475519000 ], "localDate": [ 2024, 10, 10 ], "localTime": [ 23, 3, 47, 475564000 ] }
It cannot be said that there is a problem, after all, LocalDateTime is accurate to nanoseconds, and directly converted to millisecond timestamps will lose accuracy. In short, to achieve harmonious conversion, Jackson needs to be set up.
// Still somewhere like JacksonConfig@Bean public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() { return builder -> { // deserializers (new LocalDateDeserializer()); (new LocalDateTimeDeserializer()); // serializers (new LocalDateSerializer()); (new LocalDateTimeSerializer()); }; } public static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> { /** * If the handledType() method is not rewrite, an error will be reported * @return */ @Override public Class<LocalDateTime> handledType() { return ; } @Override public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (value != null) { ((()).toInstant().toEpochMilli()); } } } public static class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> { @Override public Class<?> handledType() { return ; } @Override public LocalDateTime deserialize(JsonParser parser, DeserializationContext deserializationContext) throws IOException { long timestamp = (); return (timestamp).atZone(()).toLocalDateTime(); } } public static class LocalDateSerializer extends JsonSerializer<LocalDate> { @Override public Class<LocalDate> handledType() { return ; } @Override public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider serializers) throws IOException { if (value != null) { ((()).toInstant().toEpochMilli()); } } } public static class LocalDateDeserializer extends JsonDeserializer<LocalDate> { @Override public Class<?> handledType() { return ; } @Override public LocalDate deserialize(JsonParser parser, DeserializationContext deserializationContext) throws IOException { long timestamp = (); return (timestamp).atZone(()).toLocalDate(); } }
Here we have carried out some blunt coercive measures, defined a series of Deserializers and Serializers, and implemented serialization rules between LocalDateTime and long.
LocalTime is not processed because the conversion of separate times to timestamps is not so suitable. The timestamp has a clear year, month and day. This part seems redundant for LocalTime, and the time is usually related to the time zone, so you should be more cautious when processing. You can choose according to your needs. If you explicitly need to use a timestamp to represent LocalTime, you can use a similar method to register Deserializer and Serializer.
The above is the configuration required to convert Date and LocalDateTime into timestamps when serializing JSON:
- If you only use Date, use the configuration items provided by Spring
-dates-as-timestamps=true
Just - If LocalDateTime is used, additional configuration is required, explicitly specifying Jackson to convert LocalDateTime to timestamp
Timestamp in request parameters
Using timestamps in request parameters is more complicated because there is no ready-made configuration like time strings, and conversion rules need to be implemented manually.
The Converter interface can be used to solve this problem.
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { (new LongStringToDateConverter()); (new LongStringToLocalDateTimeConverter()); (new LongStringToLocalDateConverter()); // (new LongStringToLocalTimeConverter()); // On demand } private static class LongStringToDateConverter implements Converter<String, Date> { @Override public Date convert(String source) { try { long timestamp = (source); return new Date(timestamp); } catch (NumberFormatException e) { return null; } } } private static class LongStringToLocalDateTimeConverter implements Converter<String, LocalDateTime> { @Override public LocalDateTime convert(String source) { try { long timestamp = (source); return (timestamp).atZone(()).toLocalDateTime(); } catch (NumberFormatException e) { return null; } } } private static class LongStringToLocalDateConverter implements Converter<String, LocalDate> { @Override public LocalDate convert(String source) { try { long timestamp = (source); return (timestamp).atZone(()).toLocalDate(); } catch (NumberFormatException e) { return null; } } } private static class LongStringToLocalTimeConverter implements Converter<String, LocalTime> { @Override public LocalTime convert(String source) { try { long timestamp = (source); return (timestamp).atZone(()).toLocalTime(); } catch (NumberFormatException e) { return null; } } } }
Note that Source is type String instead of Long, because Spring MVC treats all interface request parameter types as String, and then calls Converter to convert to other types. There are many built-in Converters, such as when converting to long type, the built-in StringToNumber conversion class is used. The LongStringToDateConverter we define is a parallel relationship.
The above is the processing required to convert Date and LocalDateTime into timestamps in the interface parameters: it is very simple, just register Converter.
Types in Swagger UI
When using SwaggerUI, the DTO field type is used by default as the request parameter type, that is, the receiving time string. After changing to timestamp during serialization, it also needs to be unified in the Swagger UI.
There are two ways to integrate Swagger in Java projects, Springdoc and Spring Fox. Springdoc update, the corresponding configuration is as follows:
@Bean public OpenAPI customOpenAPI() { // The key is to call this static method to replace () .replaceWithClass(, ) .replaceWithClass(, ) .replaceWithClass(, ); return new OpenAPI(); }
If you use Spring Fox, you need to use another configuration:
@Bean public Docket createRestApi() { return new Docket(DocumentationType.OAS_30) ... .build() // The point is this sentence .directModelSubstitute(, ); }
At this time, when debugging the interface on the Swagger UI page, the parameters of the time type are displayed as integers.
The Only Neat Thing to Do
To review, handling time field serialization in Spring Boot interface involves two scenarios:
- JSON Serialization
- Parameters in GET requests and form submission requests
The two situations should be set separately.
In terms of Java type selection, Spring supports Date types better than LocalDateTime, and has many built-in configurations, which can save a lot of trouble.
If you want to use LocalDateTime and other types, you need to specify the time format when serializing JSON, and also the time format in the request parameters. The former requires manual configuration, while the latter can use the configuration items provided by Spring.
If you want to pass data with a timestamp, you also need to set it separately. Specify the serializer and deserializer during JSON serialization, and bind the corresponding Converter implementation class in the request parameters. In addition, the type experience of unified Swagger UI is better.
The above is the detailed content of the method to correctly serialize time fields in the Spring Boot interface. For more information about Spring Boot serializing time fields, please pay attention to my other related articles!