一天没吃上饭就因为一个时区的bug,Jpa @CreatedDate 标签就是差8小时。

268 阅读4分钟

编写了一个@CreatedDate注解来获取当前时间。但是,在本地和开发服务器上的输出是不同的。那是为什么呢?

在我的开发服务器上,它输出Mon Jun 26 09:06:15 CST 2023,但在docker中,它打印出UTC时间。

这到底是为什么让我们一探究竟

首先大多数时间的获取用到的就是jdk都是要先获取时区在获取具体时间,具体时间我们只是相差8小时那么一定是时区出了问题,层层筛选翻到最后发现获取时区都是通过一个类来获取的。

就是java.util里的TimeZone

知道了这个我们就来看

JVM如何检测时区

/**
     * Returns the reference to the default TimeZone object. This
     * method doesn't create a clone.
     */
    static TimeZone getDefaultRef() {
        TimeZone defaultZone = defaultTimeZone;
        if (defaultZone == null) {
            // Need to initialize the default time zone.
            defaultZone = setDefaultZone();
            assert defaultZone != null;
        }
        // Don't clone here.
        return defaultZone;
    }

    private static synchronized TimeZone setDefaultZone() {
        TimeZone tz;
        // get the time zone ID from the system properties
        String zoneID = AccessController.doPrivileged(
                new GetPropertyAction("user.timezone"));

        // if the time zone ID is not set (yet), perform the
        // platform to Java time zone ID mapping.
        if (zoneID == null || zoneID.isEmpty()) {
            String javaHome = AccessController.doPrivileged(
                    new GetPropertyAction("java.home"));
            try {
                zoneID = getSystemTimeZoneID(javaHome);
                if (zoneID == null) {
                    zoneID = GMT_ID;
                }
            } catch (NullPointerException e) {
                zoneID = GMT_ID;
            }
        }

首先,核心方法就是setDefaultZone的方法,它将检查JVM是否具有user.timezone属性。如果没有,则会调用此本地方法getSystemTimeZoneID这个方法它实现在openjdk-jdk9u-backup-03-sep-2018/jdk/src/java.base/share/native/libjava/TimeZone.c at master · AdoptOpenJDK/openjdk-jdk9u-backup-03-sep-2018 · GitHub中.

JNIEXPORT jstring JNICALL
Java_java_util_TimeZone_getSystemTimeZoneID(JNIEnv *env, jclass ign,
                                            jstring java_home)
{
    const char *java_home_dir;
    char *javaTZ;
    jstring jstrJavaTZ = NULL;

    CHECK_NULL_RETURN(java_home, NULL);

    java_home_dir = JNU_GetStringPlatformChars(env, java_home, 0);
    CHECK_NULL_RETURN(java_home_dir, NULL);

    /*
     * Invoke platform dependent mapping function
     */
    javaTZ = findJavaTZ_md(java_home_dir);
    if (javaTZ != NULL) {
        jstrJavaTZ = JNU_NewStringPlatform(env, javaTZ);
        free((void *)javaTZ);
    }

    JNU_ReleaseStringPlatformChars(env, java_home, java_home_dir);
    return jstrJavaTZ;
}

主要逻辑在openjdk-jdk9u-backup-03-sep-2018/jdk/src/java.base/unix/native/libjava/TimeZone_md.c at 17007f6a09f553801fd424d3c71382717975f66d · AdoptOpenJDK/openjdk-jdk9u-backup-03-sep-2018 · GitHub**中。

static const char *ETC_TIMEZONE_FILE = "/etc/timezone";
static const char *ZONEINFO_DIR = "/usr/share/zoneinfo";
static const char *DEFAULT_ZONEINFO_FILE = "/etc/localtime";

getPlatformTimeZoneID()
{
    struct stat statbuf;
    char *tz = NULL;
    FILE *fp;
    int fd;
    char *buf;
    size_t size;
    int res;

#if defined(__linux__)
    /*
     * Try reading the /etc/timezone file for Debian distros. There's
     * no spec of the file format available. This parsing assumes that
     * there's one line of an Olson tzid followed by a '\n', no
     * leading or trailing spaces, no comments.
     */
     //最先查找的是"/etc/timezone"文件里面的时区
    if ((fp = fopen(ETC_TIMEZONE_FILE, "r")) != NULL) {
        char line[256];

        if (fgets(line, sizeof(line), fp) != NULL) {
            char *p = strchr(line, '\n');
            if (p != NULL) {
                *p = '\0';
            }
            if (strlen(line) > 0) {
                tz = strdup(line);
            }
        }
        (void) fclose(fp);
        if (tz != NULL) {
            return tz;
        }
    }
#endif /* defined(__linux__) */

    /*
     * Next, try /etc/localtime to find the zone ID.
     * timezone没有则尝试查找/etc/localtime,这里注意如果localtime与上面timezone时区不同,就有限获取timezone时区了。
     */
    RESTARTABLE(lstat(DEFAULT_ZONEINFO_FILE, &statbuf), res);
    if (res == -1) {
        return NULL;
    }

    /*
     *如果它是一个软链接(例如:/usr/share/zoneinfo/Asia/Shanghai),则返回路径的时区。
     *否则,将其与/usr/share/zoneinfo中的所有文件进行比较,如果找到,则返回时区。
     */

    if (S_ISLNK(statbuf.st_mode)) {
        char linkbuf[PATH_MAX+1];
        int len;

        if ((len = readlink(DEFAULT_ZONEINFO_FILE, linkbuf, sizeof(linkbuf)-1)) == -1) {
            jio_fprintf(stderr, (const char *) "can't get a symlink of %s\n",
                        DEFAULT_ZONEINFO_FILE);
            return NULL;
        }
        linkbuf[len] = '\0';
        tz = getZoneName(linkbuf);
        if (tz != NULL) {
            tz = strdup(tz);
            return tz;
        }
    }

    /*
     * If it's a regular file, we need to find out the same zoneinfo file
     * that has been copied as /etc/localtime.
     * If initial symbolic link resolution failed, we should treat target
     * file as a regular file.
     */
    RESTARTABLE(open(DEFAULT_ZONEINFO_FILE, O_RDONLY), fd);
    if (fd == -1) {
        return NULL;
    }

    RESTARTABLE(fstat(fd, &statbuf), res);
    if (res == -1) {
        (void) close(fd);
        return NULL;
    }
    size = (size_t) statbuf.st_size;
    buf = (char *) malloc(size);
    if (buf == NULL) {
        (void) close(fd);
        return NULL;
    }

    RESTARTABLE(read(fd, buf, size), res);
    if (res != (ssize_t) size) {
        (void) close(fd);
        free((void *) buf);
        return NULL;
    }
    (void) close(fd);

    tz = findZoneinfoFile(buf, size, ZONEINFO_DIR);
    free((void *) buf);
    return tz;
}

由于是C语言实现所以大概描述一下代码逻辑,以linux系统为例,其他系统逻辑类似但是本地文件不同略有差异。

整个方法是按以下步骤查找时区,一旦找到就立即返回时区。

1、查找TZ环境。

2、读取/etc/timezone。

3、读取/etc/localtime。

4、如果它是一个软链接(例如:/usr/share/zoneinfo/Asia/Shanghai),则返回路径的时区。否则,将其与/usr/share/zoneinfo中的所有文件进行比较,如果找到,则返回时区。

更改时区的方式有哪些?

可以使用以下命令列出Linux中可用的时区:timedatectl list-timezones

添加JVM参数

您可以添加JVM参数

-Duser.timezone = Asia/Shanghai

设置TZ环境变量

在.bashrc中添加

export TZ = Asia/Shanghai。

更改/etc/timezone

将其内容设置为

Asia/Shanghai

更改/etc/localtime将其链接到

/usr/share/zoneinfo/Asia/Shanghai

在Java程序中手动更改时区

在获取时间之前添加此行:

TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"))

通过代码设置JVM属性

System.setProperty("user.timezone", "Asia/Shanghai")

在日历中手动设置时区

Calendar.getInstance(TimeZone.getTimeZone("Asia/Shanghai"))

从第一段代码可以看出java 添加jvm运行参数的优先级最高,linux所以要注意获取时间顺序,遇到问题可按顺序排查。