设置时区的 SimpleDateFormat 获得正确的值但区域错误

Posted

技术标签:

【中文标题】设置时区的 SimpleDateFormat 获得正确的值但区域错误【英文标题】:SimpleDateFormat with Timezone set gets correct value but wrong zone 【发布时间】:2019-04-18 17:28:45 【问题描述】:

我在 Spring 应用程序中进行了一个简单的测试,它的默认时区设置为 UTC

@SpringBootApplication
public class MemberIntegrationApp 

    @Autowired
    private TimeZoneProperties timeZoneProperties;

    @PostConstruct
    void started() 
        TimeZone.setDefault(TimeZone.getTimeZone(timeZoneProperties.getAppDefault()));  // which is UTC
    

    public static void main(String[] args) 
        SpringApplication.run(MemberIntegrationApp.class, args);
    


而且,这个简单的测试:(测试类用@SpringBootTest注释来加载主类中的配置,@SpringRunner也被应用

/**
 * Test the effect of setting timezone
 */
@Test
public void testTimezoneSettingOnSimpleDateFormat() throws ParseException 
    SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String d = "2018-08-08 12:34:56";
    log.info("Trying to parse the date string: ", d);
    Date result = f.parse(d);
    log.info("The result should be 12:34 UTC: ", result);

    f.setTimeZone(TimeZone.getTimeZone("UTC"));
    result = f.parse(d);
    log.info("The result should be 12:34 UTC: ", result);

    f.setTimeZone(TimeZone.getTimeZone("Europe/Madrid"));
    result = f.parse(d);
    log.info("The result should be 10:34 CEST: ", result);
    log.info("Now the offset(depre): ", result.getTimezoneOffset());

我有输出:

Trying to parse the date string: 2018-08-08 12:34:56
The result should be 12:34 UTC: Wed Aug 08 12:34:56 UTC 2018
The result should be 12:34 UTC: Wed Aug 08 12:34:56 UTC 2018
The result should be 10:34 CEST: Wed Aug 08 10:34:56 UTC 2018
Now the offset(depre): 0

现在,为什么第四行的值正确,但时区错误?应该是Europe/Madrid。而偏移量(在 Java 8 中已弃用,好吧,我可以原谅),它应该是 +0200,而不是 0。

这是UTC,因为在log.info() 中转换为字符串时,slf4j 会干扰????要不然是啥?我不这么认为,因为System.out.println() 也给了我UTC。

我知道我应该使用OffsetDateTime,但它是遗留的,我们目前无法将所有日期字段更改为该字段。我想知道为什么Java解析错误。

Timezone.getDefault()用SimpleDateFormat解析时有什么作用? f.getTimezone() 是什么?他们似乎在流程的不同部分采取行动.....

我问这个问题,因为杰克逊内部使用SimpleDateFormat 来处理日期字符串/格式化日期。 ObjectMapper 上的配置是否会影响映射器使用的 SimpleDateFormat

【问题讨论】:

我建议你避免使用SimpleDateFormat 类。它不仅过时了,而且出了名的麻烦。今天我们在java.time, the modern Java date and time API 中做得更好。一方面,java.time 在时区之间的转换方面不那么令人困惑。 是的……但不是我,而是 Spring/Jackson……你知道的。 FasterXML/jackson-modules-java8? 【参考方案1】:
public static Instant getInstantNow() 
        Clock utcClock = Clock.systemUTC();
        //ZoneId myTZ = ZoneId.of("Brazil/East");       
        return Instant.now(utcClock).minusSeconds(10800);   
        //Instant in = Instant.now(utcClock);
        //return in.atZone(myTZ);   
    

    public static LocalDateTime getLocalDateTimeNow() 
        ZonedDateTime nowBrasil = ZonedDateTime.now(ZoneId.of("Brazil/East"));
        return LocalDateTime.from(nowBrasil);
    

    public static LocalDate getLocalDateNow() 
        ZonedDateTime nowBrasil = ZonedDateTime.now(ZoneId.of("Brazil/East"));
        return LocalDate.from(nowBrasil);
    

【讨论】:

所以你建议使用 Java 8 Time。完全正确。我想我可能有一天会改变......但Spring似乎在杰克逊使用SimpleDateFormatter,这是我在这里模拟的目的......我不确定......【参考方案2】:

感谢您的回复;在 OP 中,我想到了这一行,但写错了:

 log.info("The result should be 14:34 CEST: ", result);

我认为这就像“所以我希望它是马德里,所以输出是马德里时区”,但恰恰相反:

格式化程序的时区将是输入日期/字符串的时区,而默认时区(如果不更改,则为JVM的时区,如果更改,则Timezone.getDefault()的值将是输出结果( date/string) 的时区。根据这两个,格式化程序将进行转换。

而且,Spring/Jackson 内部使用SimpleDateFormat 进行 JSON/Object 序列化/反序列化,所以这也是 Spring 的规则

而且,正如我测试的那样,spring.jackson.time-zonemapper.setTimezone() 将被字段上的JsonFormat(timezone = "xxx") 覆盖。也就是说spring.jackson.time-zone更通用,适用于需要“输入”时区的所有Date字段,JsonFormat(timezone = "xxx")更具体,覆盖前者。我猜spring.jackson.dateformat@JsonFormat(pattern = "xx")有相同的关系,但我没有测试。

图形化:

我写这个测试来证明这一点:

/**
 * Test the effect of setting timezone on a @link SimpleDateFormat. Apparently,
 * <code>f.setTimezone()</code> sets the input timezone, and default timezone sets
 * the output timezone.
 *
 */
@Test
public void testTimezoneSettingOnSimpleDateFormat() throws ParseException 
    /* *********** test parsing *********** */
    log.info("********** test parsing **********");
    SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String d = "2018-08-08 12:34:56";

    log.info("Trying to parse the date string: ", d);
    Date result = f.parse(d);
    log.info("The result should be 12:34 UTC: ", result);

    f.setTimeZone(TimeZone.getTimeZone("UTC"));
    result = f.parse(d);
    log.info("The result should be 12:34 UTC: ", result);

    f.setTimeZone(TimeZone.getTimeZone("Europe/Madrid"));
    result = f.parse(d);
    log.info("The result should be 10:34 UTC: ", result);

    /* ********** test formatting ********** */
    log.info("********** test formatting **********");
    // given
    SimpleDateFormat f2 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX");
    // construct a date to represent this moment
    OffsetDateTime now = OffsetDateTime.of(2018, 11, 16, 10, 22, 22, 0, ZoneOffset.of("+0100"));
    TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")); // GMT+8, so Madrid+7
    // when you construct a date without timezone, it will be the timezone of system/default!
    Date nowDate = new Date(now.toEpochSecond() * 1000);
    log.info("The constructed date is: ", nowDate); // Fri Nov 16 17:22:22 CST 2018

    // now formatter timezone is Madrid
    f2.setTimeZone(TimeZone.getTimeZone("Europe/Madrid"));
    // now default timezone is Asia/Shanghai

    // when
    String result2 = f2.format(nowDate);

    // then
    log.info("The result should be 10:22: ", result2); // 2018-11-16T10:22:22+01:00


    log.info("Conclusion: the formatter's timezone sets the timezone of input; the application/default " +
            "timezone sets the timezone of output. ");

【讨论】:

【参考方案3】:

我不认为这是一个错误,而是对线条的误解:

    f.setTimeZone(TimeZone.getTimeZone("Europe/Madrid"));
    result = f.parse(d);
    log.info("The result should be 10:34 CEST: ", result);

什么意思?

您首先设置一个时区,告诉解析器您将要解析欧洲/马德里区的时间。

然后你显示它。它无法猜测您想要它在哪个时区,因此它会以默认时区显示它,在您的情况下为 UTC。


注意:

实际上是 UTC 时间 10:34,而马德里是 12:34,而不是相反。 Date.getTimezoneOffset() 是 offset between UTC and the default timezone (因此在您的情况下为 0),与您用于配置解析器的时区无关。此外,它自 java 1.1 以来已被弃用,您不应该再使用它了。

要显示不同时区的日期值,可以使用SimpleDateFormat.format(),例如:

    f.setTimeZone(TimeZone.getTimeZone("UTC"));
    log.info("UTC ", f.format(new Date()));
    f.setTimeZone(TimeZone.getTimeZone("Europe/Madrid"));
    log.info("Europe/Madrid ", f.format(new Date()));

【讨论】:

是的,我错了,你明白了;所以这就像input(as f.getTimezone()) ----&gt; f ----&gt;date(as default timezone),而不是相反。就是在解析的时候f.setTimezone()建立字符串的输入时区,默认时区建立输出日期时区。那么,在 Spring Boot 中这样做的时候,是一样的吗?格式化怎么样? 那么,在第一种情况下,我们知道如果没有设置时区,格式化程序会将UTC作为预设,对吧? 是的。要输出不同时区的日期,请参阅我的更新 要理解的关键点是Date 确实是一个时间点,因此没有固有的时区或偏移量。只有当您想解析或打印日期时,您才需要想知道您在哪个区域 对,Date 是一个时间点,它的 getTimezoneOffset() 方法自 Java 1.1 以来已被弃用,带有指向 Calendar 的指针,这是 Java 8 之前的正确工具 java.time API…

以上是关于设置时区的 SimpleDateFormat 获得正确的值但区域错误的主要内容,如果未能解决你的问题,请参考以下文章

Calendar日期类详解SimpleDateFormat时区Date夏令时常用方法,日期差获取当前时间

SimpleDateFormat 解析丢失时区 [重复]

如果手机时区已更改,SimpleDateFormat setTimeZone无效

SimpleDateFormat 与时区

Java SimpleDateFormat时间解析时区问题

遇到的问题---java---使用SimpleDateFormat进行时区加8小时后出现24点的数据,导致时间异常无法入库