记录一次跌宕起伏的bug查找事件,对于解决获取当前事件相差8小时,觉得很有参考价值。最近接手一个项目,将本地服务器上的网站代码,放到了阿里云上,发现一个问题,获取当前时间不正确,始终相差8个小时,而在本机调试,以及内部服务器上都没有此问题。因为是同一套代码,基本排除是代码问题,应该就是环境导致。获取当前时间的代码很简单:
Date date = new Date();
String dateTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss",Locale.CHINA).format(date);
然后将该dateTime输出到前端html页面显示出来,发现输出的是UTC时间,晚了8个小时,而不是北京时间。奇葩的是,上面第2行设置了位置为Locale.CHINA,居然还出现了问题,什么情况?然后网上各种搜索,发现有很多解决方案都无效。
1.初探:有说添加System.setProperty("user.timezone","GMT+8");就可以了,试了试没用,毕竟这行代码起的作用,跟上面locale.CHINA一样
2.查看系统环境:由于本机虽然装的是windows 7系统,差别也不大,但内部服务器和阿里云都是同一版本的windows sever系统,基本排除操作系统环境问题
3.有趣的现象:将上面代码用记事本保存为java文件,在cmd控制台编译为class文件后,运行代码,发现没有问题,获取的当前时间就是北京时间。论坛有人说可能时区没设对,其实这种可能不大,即便我操作系统时区不对,上面的代码时区也设置成了中国,理论上是不会出现这个问题的。当然,为避免万一,我查看了系统时间
也就是时区设置是对的。同时,又添加了两行代码,用了代码来验证时区,所以现在代码:
import java.util.*;
import java.text.SimpleDateFormat;
public class HelloWorld {
public static void main(String[] args){
Date date = new Date();
String dateTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss",Locale.CHINA).format(date);
System.out.println(dateTime);
String zone = System.getProperty("user.timezone");
System.out.println("User Timezone: " + zone);
}
}
运行后输出结果为:
2017-09-14 14:52:23
User Timezone: Asia/Shanghai
也是对的,排除时区设置导致。论坛上有说不该用Date,应该用Calendar就可以了。于是添加Calendar获取当前时间,所以现在代码:
import java.util.*;
import java.text.SimpleDateFormat;
public class HelloWorld {
public static void main(String[] args){
Date date = new Date();
String dateTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss",Locale.CHINA).format(date);
System.out.println(dateTime);
Calendar calendar = Calendar.getInstance();
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss",Locale.CHINA);
String dateStr = df.format(calendar.getTime());
System.out.println(dateStr);
String zone = System.getProperty("user.timezone");
System.out.println("User Timezone: " + zone);
}
}
也是对的,Calendar使用效果相同,更加排除了是时区设置问题。还有说jdk1.5的问题,不过我用的是jdk1.8排除。这里发现一个现象,上面执行效果都正确,但是实在控制台下指定的,而不同于文章最开始处运行在tomacat下出错。说明很可能是tomacat环境下,出现了问题。
4.寻求官方技术支持:我联系了阿里云技术支持,可能没描述清楚,不是很理解我的实际情况。开始说可能是第三方软件适配阿里云问题,然后转给了第三方。小哥很耐心的联系到我,经过沟通后,解决不了。之后让小哥转了回去,让阿里云工程师来解决。沟通之后,大致是问我系统时区设置是否正确,是否更改了时区,我更改时区后调试,还是没有解决。这时,阿里云售后工程师建议给他授权,进去系统查看我的war包,想来是看源代码吧,想到是公司项目,我回绝了。其实阿里云工程师说的还是很接近问题的根源了,这里后面再说,也就是说直接性的售后,还是没有解决,因为这不是设置一下什么功能就能解决的。
5.尝试修改tomocat配置:既然由第3点大致确定是tomocat问题,那我们尝试修改tomocat配置。这里参考了网上的资源,有说修改tomocat的java配置和catalina.bat文件的,链接http://blog.csdn.net/davidxj/article/details/6109055;还有说修改tomocat配置文件的,链接http://jingyan.baidu.com/article/fdbd4277c281cdb89e3f48b0.html;还有说修改catalina.sh文件将时区+8;还有说修改spring配置的,链接http://www.zhonghcc.com/2016/06/308和http://www.jianshu.com/p/ed7620c7cb1d;还有application.properties文件里面添加 spring.jackson.time-zone=GMT+8的;实际操作后,还是没起作用,当前时间通过html页面显示出来还是UTC时间,不是北京时间,结果还是相差8小时。
6.定结论:至此,虽然知道可能是环境问题,但最终放弃了通过设置环境,来解决此问题。因为阿里云服务器实际就是一个虚拟机,tomocat获取当前时间,是获取的实体机硬件时间,即BIOS时间,因为阿里云为了同步,肯定是将布置在全球的服务器统一设置为UTC时间。至于我错误地将此bug归咎于阿里云服务器,毕竟tomocat运行在实体PC上是没有此问题的,而且我依次用tomocat6.0,7.5,8.5三个版本都试了一下,阿里云上都存在此问题。
7.不明所以:其实,我还进行过这样的操作,将时区和时间,在后台获取后都通过前端html显示出来:
Date date = new Date();
String dateTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss",Locale.CHINA).format(date);
Calendar calendar = Calendar.getInstance();
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss",Locale.CHINA);
String dateStr = df.format(calendar.getTime());
String zone = System.getProperty("user.timezone");
效果是两个时间dateTime和dateStr都是UTC时间,但是时区zone却是Asia/Shanghai,时间和时区直接矛盾,这个现象确实诡异!即时间取的是BIOS时间,时区取的却是系统时区。这里顺便说下,tomocat获取的时区诗是以启动时为准,启动后更改系统时区,代码重新获取时区是不会发生变化的。补充一下,我用的java spring mvc。
最后的解决方式:在程序内部,添加设置时区代码:
TimeZone.setDefault(TimeZone.getTimeZone("GMT+8"));
8.灵光乍现:至此该问题得到解决,但还是似懂非懂。后面一次偶然的机会,调试同在tomocat部署下的另一个项目,发现奇迹,他妈的竟然将时区设置成了标准时间!因此导致我这个项目获取时间时,已经被前一个项目把时区更改了,以至于获取当前时间相差8个小时。而我调试时由于只针对于本项目,看不出问题,也找不到更改了时区的代码。但这样第7点中,未调用TimeZone.setDatault()方法前打印时区就有问题了,得到的不是另一个项目设置的时区。
9.时区根源:在网上查找资料,得知
如果user.timezone没有定义,它就会尝试从user.country和java.home (System)属性来得到ID。 如果它没有成功找到一个时间区域ID,它就会使用一个"fallback" 的GMT值。换句话说, 如果它没有计算出你的时间区域ID,它将使用GMT作为你默认的时间区域。这里由于我本项目没有定义user.timezone,所以实际是从java.home得到的,就是Asia/Shanghai,虽然另一个项目设置TimeZone.setDatault()方法设置了时区为标准时区。
10.更进一步:既然是时区问题,试着用System.setProperty("user.timezone","GMT+8");这种方式在项目内部设置了时区,发现竟然没有用,原来在J2SE中,大多数日期和时间相关的类都包含时间区域信息,包括那些格式类,如java.text.DateFormat, 它们都会被JVM的默认时间区域所影响。由于另一个项目时通过TimeZone.setDatault()方法设置的默认时区,会影响到这个项目,因为默认时区优先级高,会优先被获取到,而不会获取本项目System.setProperty()方法设置的时区,参考链接http://log-cd.iteye.com/blog/368238。
总结:这是个很曲折的bug调试过程,可能你看的有点绕,但是如果细究下来,上面应该将绝大部分可能的原因都考虑到了,并且实测验证过。所以最后建议,可以的话不要在代码里设置时区,说不定设置了后,影响到一起发布的其它项目,让它们都获取jvm的时区吧。你至多更改jvm的时区就可以了,要不然保险起见,得在每个项目中,代码都设置为同一个时区。因为你本项目设置时区的方式,可能会影响到其他项目,例如上面说到的TimeZone.setDatault()方法设置时区。反过来,如果时区不对,排除上面提到的通常原因,也有可能不是本项目问题,而是部署在同一个tomocat上,另一个引发的问题,例如我上面遇到的就是这种情况。
以上由于也是收集并列举了网上各种情况和解决方式,相信慢慢细心地看完,对java时区的了解,应该是比较充分了。