编写了一个@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;
}
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所以要注意获取时间顺序,遇到问题可按顺序排查。