Explore complex time zones in java

Explore complex time zones in java

Some common time (time zone) concepts

GMT

Also known as Greenwich Mean Time or Greenwich Mean Time, the old translation is Greenwich Mean Time.
GMT is a time calculation based on the rotation and revolution of the Earth. It refers to the standard time at the Royal Greenwich Observatory in the suburbs of London, England, because the prime meridian is defined on the longitude passing there.

UTC

The full name of UTC is Universal Time Coordinated, the Chinese name is Coordinated World Time, also known as World Unified Time, World Standard Time, and International Coordinated Time. It is the most important world time standard, based on atomic time seconds, and is as close to Greenwich Mean Time as possible.
UTC calculates time based on atomic clocks.

CST and CTT

Beijing Time, China Standard Time, China Standard Time, in terms of time zone division, belongs to the East Eighth District, 8 hours ahead of Coordinated Universal Time, and is recorded as UTC + 8.
However, the confusing thing about the abbreviation CST is that it can represent four different times at the same time:

  1. Central Standard Time (USA) UT-6:00
  2. Central Standard Time (Australia) UT + 9:30
  3. China Standard Time UT + 8:00
  4. Cuba Standard Time UT-4:00

So to avoid ambiguity, some places now use CTT to refer to China time zone.

ISO

ISO is a time format, the ISO 8601 version is currently used, the format is as follows;

YYYY-MM-DDThh:mm:ss[.mmm]TZD

Timestamp

The Chinese name of Timestamp is timestamp, which refers to the total number of seconds from 00:00:00 on January 1, 1970, Greenwich Mean Time to the present. Is an absolute time.

Asia/Shanghai

When Asia/Shanghai is a regional standard named after a region, it is called CST in China. This regional standard will be compatible with various time points in history.
China implemented daylight saving time from 1986 to 1991. The difference between summer and winter is 1 hour. Asia/Shanghai will be compatible with this time period.

Look at the time secrets in Java

The following test codes are all run with JDK17.

What do timestamps and Date look like

@Test
public void test1() {<!-- -->
    TimeZone timeZone = TimeZone.getDefault();
    long timestamp = System.currentTimeMillis();
    Date now = new Date();
    System.out.println("Current time zone:" + timeZone);
    System.out.println("Current timestamp:" + timestamp);
    System.out.println("Current Date:" + now);
    System.out.println("Comparison of timestamp and Date:" + (timestamp == now.getTime()));
}

The running results are as follows:

Current time zone: sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=31,lastRule=null]
Current timestamp:1691847447812
Current Date:Sat Aug 12 21:37:27 CST 2023
Timestamp and Date comparison: true

You can see that the current JVM default time zone is Asia/Shanghai, and the timestamp obtained from Date is consistent with the timestamp obtained from the system System.currentTimeMillis.
Look at the code for Date:

private transient long fastTime;

public Date() {<!-- -->
    this(System.currentTimeMillis());
}

public Date(long date) {<!-- -->
    fastTime = date;
}

public String toString() {<!-- -->
    // "EEE MMM dd HH:mm:ss zzz yyyy";
    BaseCalendar.Date date = normalize();
    StringBuilder sb = new StringBuilder(28);
    int index = date.getDayOfWeek();
    if (index == BaseCalendar.SUNDAY) {<!-- -->
        index = 8;
    }
    convertToAbbr(sb, wtb[index]).append(' '); // EEE
    convertToAbbr(sb, wtb[date.getMonth() - 1 + 2 + 7]).append(' '); // MMM
    CalendarUtils.sprintf0d(sb, date.getDayOfMonth(), 2).append(' '); // dd
  
    CalendarUtils.sprintf0d(sb, date.getHours(), 2).append(':'); // HH
    CalendarUtils.sprintf0d(sb, date.getMinutes(), 2).append(':'); // mm
    CalendarUtils.sprintf0d(sb, date.getSeconds(), 2).append(' '); // ss
    TimeZone zi = date.getZone();
    if (zi != null) {<!-- -->
        sb.append(zi.getDisplayName(date.isDaylightTime(), TimeZone.SHORT, Locale.US)); // zzz
    } else {<!-- -->
        sb.append("GMT");
    }
    sb.append(' ').append(date.getYear()); // yyyy
    return sb.toString();
}

Date stores a timestamp member variable, and the default construction method is the current timestamp. Since the computer is running very fast, the current timestamp obtained twice is consistent.
Let’s look at the toString method of Date. This method formats the Date object into a EEE MMM dd HH:mm:ss zzz yyyy string, which means in Chinese. week month
Days of the month hours minutes seconds time zone year
.
Why the printed Date time zone is CST, but the current time zone is Asia/Shanghai
Woolen cloth? In fact, a layer of conversion has been done here, which is the code zi.getDisplayName(date.isDaylightTime(), TimeZone.SHORT, Locale.US)
, only the displayed names are different, but they are actually the same.

What impact do different time zones have on Date

@Test
public void test2() {<!-- -->
    TimeZone timeZone = TimeZone.getDefault();
    System.out.println("The following is the default time zone:" + timeZone);
    System.out.println("Current Date:" + new Date());
    System.out.println("--------------");
  
    //Modify time zone to UTC
    TimeZone utcTimeZone = TimeZone.getTimeZone("UTC");
    TimeZone.setDefault(utcTimeZone);
    System.out.println("The following is the UTC time zone:" + utcTimeZone);
    System.out.println("Current UTC Date:" + new Date());
    System.out.println("--------------");
  
    //Modify the time zone to UTC + 8
    TimeZone utc8TimeZone = TimeZone.getTimeZone("UTC + 8");
    TimeZone.setDefault(utc8TimeZone);
    System.out.println("The following is UTC + 8 (wrong) time zone" + utc8TimeZone);
    System.out.println("Current UTC + 8 Date:" + new Date());
    System.out.println("--------------");
  
    //Modify time zone to GMT
    TimeZone gmtTimeZone = TimeZone.getTimeZone("GMT");
    TimeZone.setDefault(gmtTimeZone);
    System.out.println("The following is the GMT time zone:" + gmtTimeZone);
    System.out.println("Current GMT Date:" + new Date());
    System.out.println("--------------");
  
    //Modify time zone to GMT + 8
    TimeZone gmt8TimeZone = TimeZone.getTimeZone("GMT + 8");
    TimeZone.setDefault(gmtTimeZone);
    System.out.println("The following is the GMT + 8 time zone:" + gmt8TimeZone);
    System.out.println("Current GMT + 8 Date:" + new Date());
}

The result of running the above code is as follows:

The following is the default time zone: sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=31,lastRule=null]
Current Date:Sat Aug 12 22:21:48 CST 2023
---------------
The following is the UTC time zone: sun.util.calendar.ZoneInfo[id="UTC",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null]
Current UTC Date:Sat Aug 12 14:21:48 UTC 2023
---------------
Here is the UTC+8 (wrong) time zone: sun.util.calendar.ZoneInfo[id="GMT",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null]
Current UTC + 8 Date:Sat Aug 12 14:21:48 GMT 2023
---------------
The following is the GMT time zone: sun.util.calendar.ZoneInfo[id="GMT",offset=0,dstSavings=0,useDaylight=false,transitions=0,lastRule=null]
Current GMT Date:Sat Aug 12 14:21:48 GMT 2023
---------------
The following is the GMT + 8 time zone: sun.util.calendar.ZoneInfo[id="GMT + 08:00",offset=28800000,dstSavings=0,useDaylight=false,transitions=0,lastRule=null]
Current GMT + 8 Date:Sat Aug 12 22:21:48 GMT + 08:00 2023

This result is very interesting. Let’s first establish a few common sense or premises:

  1. The time in East Eighth District is 8 hours ahead of 0 time zone;
  2. Time is an absolute and relative concept. Absolute time is based on time zone 0. It has different names in different time zones, but it represents the same moment;

Then on this basis we analyze the above results:

  1. The default time zone is Asia/Shanghai, the current time is 2023-08-12 22:21:48, CST China time zone, Beijing time, no problem;
  2. Modify the time zone to UTC, and the current time is 2023-08-12 14:21:48. The UTC time zone is eight hours behind Beijing time, which is also correct;
  3. The modified time zone is UTC + 8, and the current time is 2023-08-12 14:21:48, GMT time zone, which is eight hours behind Beijing time. There is a problem here, why is it set?
    UTC+8, but the time zone is GMT? Make a note of this question and look at it later;
  4. Modify the time zone to GMT, and the current time is 2023-08-12 14:21:48. The GMT time zone is eight hours behind Beijing time, which is also correct;
  5. Modify the time zone to GMT + 8, the current time is 2023-08-12 22:21:48, GMT + 8 time zone, which is also correct;

So now let’s analyze why UTC + 8 is set, but the time zone is GMT. Take a look at the code:

private static TimeZone getTimeZone(String ID, boolean fallback) {<!-- -->
    TimeZone tz = ZoneInfo.getTimeZone(ID);
    if (tz == null) {<!-- -->
        tz = parseCustomTimeZone(ID);
        if (tz == null & amp; & amp; fallback) {<!-- -->
            tz = new ZoneInfo(GMT_ID, 0);
        }
    }
    return tz;
}
  1. The first step is to get the default time zone enumeration. Unfortunately, the default time zone enumeration does not have UTC + 8 at all. The default available time zone can be obtained through TimeZone.getAvailableIDs();
  2. The second step is to parse the custom time zone. Unfortunately, the custom time zone only supports the ones starting with GMT, so the default time zone GMT is returned;

Jackson time serialization blind spot

Prepare a java model class

@Getter
@Setter
public class Model {<!-- -->
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date created;
    private Date modified;
  
    private void setDate(Date date) {<!-- -->
        System.out.println("Set the time to:" + date);
        this.created = date;
        this.modified = date;
    }
}

This class has two member variables, one with the JsonFormat annotation and the other without. The time defaults to the current time.

Convert object to String
@Test
public void test1() throws JsonProcessingException {<!-- -->
    System.out.println("Current time zone:" + TimeZone.getDefault());
    Model model = new Model();
    ObjectMapper objectMapper = new ObjectMapper();
    System.out.println(objectMapper.writeValueAsString(model));
}

The running results are as follows:

Current time zone: sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=31,lastRule=null]
Set time: Sun Aug 13 11:15:14 CST 2023
Output result: {"created":"2023-08-13 03:15:14","modified":1691896514868}

Two problems can be seen from the output results:

  1. You can see that the current time zone is the East Eighth District, and the field created with the JsonFormat annotation is output according to the format, but the result is the time in the 0 time zone, not the time in the current time zone;
  2. Unannotated modified outputs a timestamp.

After some exploration, you can find a parameter timezone in the JsonFormat annotation, which is commented as follows:

TimeZone to use for serialization (if needed). Special value of DEFAULT_TIMEZONE can be used to mean "just use the default", where default is specified by the serialization context, which in turn defaults to system default (UTC) unless explicitly set to another timezone.

Translated, this parameter can specify the serialization time zone. If not specified, the default is UTC time zone (TimeZone in Jvm is not used here). This default value can be modified.

The second problem is that the modified field does not specify the serialization format, so the parameter SerializationFeature.WRITE_DATES_AS_TIMESTAMPS is added by default.
Date output is a timestamp. See the code for the DateTimeSerializerBase class
5cCZEC

oMsKHF
But Jackson can specify the default Date serialization format

Then try the modified code:

@Getter
@Setter
public class ModelA {<!-- -->
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT + 7")
    private Date created;
    private Date modified;
  
    private void setDate(Date date) {<!-- -->
        System.out.println("Set the time to:" + date);
        this.created = date;
        this.modified = date;
    }
}

@Test
public void test2() throws JsonProcessingException {<!-- -->
    System.out.println("Current time zone:" + TimeZone.getDefault());
    ModelA model = new ModelA();
    ObjectMapper objectMapper = new ObjectMapper();
    //Specify formatting style
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
    objectMapper.setDateFormat(dateFormat);
    //Set the time zone to Dongba District
    objectMapper.setTimeZone(TimeZone.getTimeZone("GMT + 8"));
    System.out.println("Output result:" + objectMapper.writeValueAsString(model));
    System.out.println("-------------");
    System.out.println("Set time zone to 0 time zone");
    //Set the time zone to 0 time zone
    TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
    objectMapper.setTimeZone(TimeZone.getTimeZone("GMT"));
    System.out.println("Output result:" + objectMapper.writeValueAsString(model));
}

Here, the serialization format of the created field is specified as yyyy-MM-dd HH:mm:ss, and the time zone is specified as GMT + 7 (East Seventh District, one hour earlier than Beijing time), and then already setup
The default serialization format of Date is yyyy/MM/dd HH:mm:ss. You can take a look at the result:

Current time zone: sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=31,lastRule=null]
Set time: Sun Aug 13 11:16:37 CST 2023
Output results: {"created":"2023-08-13 10:16:37","modified":"2023/08/13 11:16:37"}
-------------
Set time zone to 0 time zone
Output results: {"created":"2023-08-13 10:16:37","modified":"2023/08/13 03:16:37"}
  1. The current time zone is still the East Eighth District, and the time is 2023-08-13 11:16:37;
  2. The field with separately specified serialization format and time zone is one hour earlier than the current time, and the default time modified is output according to the default format and time zone;
  3. After setting the 0 time zone, the serialization of the annotated fields does not change. The serialization configuration is subject to the annotations, but modified is serialized in the 0 time zone;
Convert String to Object
@Test
public void test3() throws JsonProcessingException {<!-- -->
    System.out.println("Current time zone:" + TimeZone.getDefault());
    String json = "{"created":"2023-08-12 12:56:46","modified":"2023 /08/12 12:56:46"}";
    System.out.println("Original string:" + json);
    ObjectMapper objectMapper = new ObjectMapper();
    System.out.println("--------------");
    objectMapper.setDateFormat(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"));
    //Calculate as 0 time zone
    System.out.println("Set serialization by time zone 0");
    objectMapper.setTimeZone(TimeZone.getTimeZone("GMT"));
    Model model2 = objectMapper.readValue(json, Model.class);
    TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
    System.out.println("output created:" + model2.getCreated());
    System.out.println("output modified:" + model2.getModified());
    System.out.println("--------------");
    // Calculated according to East Eighth District
    System.out.println("Set serialization according to Dongba District");
    objectMapper.setTimeZone(TimeZone.getTimeZone("GMT + 8"));
    Model model = objectMapper.readValue(json, Model.class);
    TimeZone.setDefault(TimeZone.getTimeZone("GMT + 8"));
    System.out.println("output created:" + model.getCreated());
    System.out.println("output modified:" + model.getModified());
}

Define a string. The time in the string is just a symbol and does not have any concept of time zone. Therefore, when converted to Date, this concept will become a certain time in a certain time zone. The running results are as follows:

Current time zone: sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=31,lastRule=null]
Original string: {"created":"2023-08-12 12:56:46","modified":"2023/08/12 12:56:46"}
---------------
Set serialization by time zone 0
Output created:Sat Aug 12 12:56:46 GMT 2023
Output modified:Sat Aug 12 12:56:46 GMT 2023
---------------
Set serialization by East Eighth District
Output created:Sat Aug 12 20:56:46 GMT + 08:00 2023
Output modified:Sat Aug 12 12:56:46 GMT+08:00 2023

Analysis results:

  1. The current time zone is East Eighth District, created is annotated but does not specify a time zone, modified is unannotated, and the string time is 2023-08-12 12:56:46;
  2. Set the default deserialization time zone to 0 time zone. After converting to an object, it will all be 0 time zone 2023-08-12 12:56:46;
  3. Set the default deserialization time zone to Dongba District. After converting to an object, created is 2023-08-12 20:56:46 of Dongba District, which is 0
    The time zone is 2023-08-12 12:56:46. This time is obviously inconsistent with expectations. What we expect is 2023-08-12 12:56:46< in East Eighth District. /code>,*
    Let’s verify this issue with the next test case*. modified is 2023-08-12 12:56:46 in Dongba District, which is correct;

Change the above code and set up Dongba District serialization to create a new ObjectMapper:

System.out.println("Current time zone:" + TimeZone.getDefault());
String json = "{"created":"2023-08-12 12:56:46","modified":"2023 /08/12 12:56:46"}";
System.out.println("Original string:" + json);
System.out.println("--------------");
//Calculate according to 0 time zone
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setDateFormat(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"));
System.out.println("Set serialization by time zone 0");
objectMapper.setTimeZone(TimeZone.getTimeZone("GMT"));
Model model2 = objectMapper.readValue(json, Model.class);
TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
System.out.println("output created:" + model2.getCreated());
System.out.println("output modified:" + model2.getModified());
System.out.println("--------------");
// Calculated according to East Eighth District
System.out.println("Set serialization according to Dongba District");
ObjectMapper objectMapper2 = new ObjectMapper();
objectMapper2.setDateFormat(new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"));
objectMapper2.setTimeZone(TimeZone.getTimeZone("GMT + 8"));
Model model = objectMapper2.readValue(json, Model.class);
TimeZone.setDefault(TimeZone.getTimeZone("GMT + 8"));
System.out.println("output created:" + model.getCreated());
System.out.println("output modified:" + model.getModified());

The result is as follows:

Current time zone: sun.util.calendar.ZoneInfo[id="Asia/Shanghai",offset=28800000,dstSavings=0,useDaylight=false,transitions=31,lastRule=null]
Original string: {"created":"2023-08-12 12:56:46","modified":"2023/08/12 12:56:46"}
---------------
Set serialization by time zone 0
Output created:Sat Aug 12 12:56:46 GMT 2023
Output modified:Sat Aug 12 12:56:46 GMT 2023
---------------
Set serialization by East Eighth District
Output created:Sat Aug 12 12:56:46 GMT+08:00 2023
Output modified:Sat Aug 12 12:56:46 GMT+08:00 2023

Analyze the results:

  1. The current time zone is East Eighth District, created is annotated but does not specify a time zone, modified is unannotated, and the string time is 2023-08-12 12:56:46;
  2. Build an ObjectMapper and set the default deserialization time zone to 0 time zone. After being converted into an object, it will all be 0 time zone 2023-08-12 12:56:46;
  3. Build an ObjectMapper and set the default deserialization time zone to Dongba District. After converting to an object, it will all be Dongba District 2023-08-12 12:56:46, which is 0
    The time zone is 2023-08-12 04:56:46, so the problem occurs in that the annotated default time zone has not changed at all;

In fact, the reason why this line of code objectMapper.setTimeZone(TimeZone.getTimeZone("GMT")) does not take effect is because it is the same ObjectMapper
The deserialization of the same class is cached in the object, and setting the time zone cannot take effect on the annotated fields. At this point it is okay to deserialize the string to another class because there is no cache.

How to use DateTimeFormat in Spring

Many people don’t know what the difference is between the annotation DateTimeFormat in Spring and JsonFormat in Jackson, and when to use which one.

In fact, this problem is very simple. There are two ways for Spring to receive Http request parameters. One is based on the parameters on the form or url, that is, the params method, and the other is the body method.
Methods, these two methods determine how Spring deserializes input parameters.

Form or Param method

The code for this method can be viewed in org.springframework.beans.AbstractNestablePropertyAccessor
The principle of sight is to convert each field into a property of the model object. This is when DateTimeFormat takes effect.

Body mode

Because after Spring receives the Json Body, it is actually a string. At this time, it does not know which field should be mapped to the attribute of which model object, so it needs to use three parties.
Json serialization tool, such as Jackson or Fastjson. At this time, JsonFormat will take effect, and the configuration will return to Jackson ObjectMapper.
configured, but Spring serialization has its own ObjectMapper instance, which can be configured according to the configuration file.

In the same way, because the Body returned by http
It is text, so this method is to serialize the model object into a string. This is also done with the help of a serialization tool, so it also takes effect JsonFormat.

What is the most reasonable way to interact with the time field in the interface

In interface docking, there are often interface interactions with time fields. In many cases, we interact through strings, that is, formatted into 2022-03-09 00:13:00, but like this The string cannot identify the time in which time zone it is, that is, it cannot identify the absolute time.
In this case, both parties need to know the time zones of both parties, which is obviously unreasonable. There are several ways to solve interaction problems:

  1. The two parties agree on the time zone and interact with strings. This method is too costly, but there is no problem when used in China;
  2. Interact with timestamps, so that output and input are absolute times, and the receiver can convert the timestamp to its own service time zone for operation (for example, do not add annotations when serializing with Jackson).
    This method is not friendly enough for front-end interaction, the human eye cannot tell the time at a glance, and the readability is too weak. Another point to mention is that Spring will not serialize Date types into timestamps by default. See the specific code below. It modifies Jackson's behavior of serializing into timestamps by default;
    UQVEII
  3. It is represented by a string format with time zone, such as OffsetDateTime. This class maintains LocalDateTime and ZoneOffset, which can represent time and time zone. In this way, the time can be converted to 2023-08-18T10:43:00.591 + 08:00 through serialization.
    If the receiver is in time zone 0, this string can be deserialized into the time in time zone 0, such as 2023-08-18T02:43:00.591Z. This method is highly readable and can also mark the absoluteness of time. It is often used in internationalization.
syntaxbug.com © 2021 All Rights Reserved.