引入问题
有两个webapp:一个要求默认时区是东8区,也就是北京时间;一个要求默认时区是0时区。这两个webapp之间有内在业务上的关联,希望部署在同一个Tomcat中。
在Java中,默认时区是可以通过JVM启动参数-Duser.timezone=GMT+8
来设定的。如果Java应用启动时没有设置,则采用系统的默认时区。显然,默认时区是JVM隔离级别的。而同一个Tomcat实例中的webapp都运行在同一个JVM实例上,所以是共用同一个默认时区的。
所以就有了这么个问题:如何让处于同一个Tomcat的不同webapp使用各自独立的时区?
几个有问题的方案
stackoverflow上「How do I set the timezone in Tomcat for a single web app?」探讨了这个问题,但给出的几个答案都有些问题。
使用Filter
JDK 1.5中,关于TimeZone类的setDefault方法源码如下:
public static synchronized void setDefault(TimeZone zone) {
defaultZoneTL.set(zone);
}
defaultZoneTL中TL的意思是ThreadLocal,所以调用这个方法只对当前线程有效。由此,我们可以使用Filter,在业务开始前调用setDefault设置正确的时区,业务完成后再调用setDefault恢复到原先的时区。代码如下:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
TimeZone savedZone = TimeZone.getDefault();
TimeZone.setDefault(webappZone);
chain.doFilter(request, response);
TimeZone.setDefault(savedZone);
}
使用Filter的方式只针对JDK1.5有效,当JDK1.6和JDK1.7实现代码不同后,这种方式也就可能得到未知的结果了。
开启默认的SecurityManager
JDK 1.6中,关于TimeZone类的setDefault方法源码如下:
public static void setDefault(TimeZone zone) {
if (hasPermission()) {
synchronized (TimeZone.class) {
defaultTimeZone = zone;
defaultZoneTL.set(null);
}
} else {
defaultZoneTL.set(zone);
}
}
可以看到,设置默认时区时,需要判断是否拥有权限。如果没有权限,则行为跟JDK1.5一致,设置只对当前线程有效;如果拥有权限,则设置对全局的JVM有效。这里的权限相关代码如下:
private static boolean hasPermission() {
boolean hasPermission = true;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
try {
sm.checkPermission(new PropertyPermission("user.timezone", "write"));
} catch (SecurityException e) {
hasPermission = false;
}
}
return hasPermission;
}
这里提到了SecurityManager的概念。默认情况下代码中的sm为null,hasPermission()函数的返回值为true,所以setDefault()函数对全局的JVM造成影响,这不是我们希望的。
如果我们希望调用setDefault()函数后得到与JDK1.5的相同的效果,则必须配置Java的SecurityManager,并且权限检查必须抛出异常。为应用设置SecurityManager可以通过设置启动参数实现:-Djava.security.manager <class_name>
如果没有指定一个class,则Java将会使用默认的SecurityManager,这个安全策略是在$JAVA_HOME/jre /lib/security/java.policy
中定义的。经过测试,默认的安全策略不允许我们修改全局范围的默认时区,hasPermission()函数返回false。这样,我们就还可以采用Filter的方式来对不同webapp分时区了。
采用开启默认的SecurityManager,然后进行Filter的方式只对JDK1.5,JDK1.6有效,因为JDK1.7的实现又变了,所以这种方式对JDK1.7将造成不一样的结果。
使用定制的JavaAwtAccess
JDK 1.7中,关于TimeZone类的setDefault方法源码如下:
public static void setDefault(TimeZone zone) {
if (hasPermission()) {
synchronized (TimeZone.class) {
defaultTimeZone = zone;
setDefaultInAppContext(null);
}
} else {
setDefaultInAppContext(zone);
}
}
可以看到,JDK1.7和之前的JDK相比,已经不再采用ThreadLocal的方式来存储时区了。它用了一个AppContext的概念。setDefaultInAppContext()函数的源码如下:
private static void setDefaultInAppContext(TimeZone tz) {
JavaAWTAccess javaAWTAccess = SharedSecrets.getJavaAWTAccess();
if (javaAWTAccess == null) {
mainAppContextDefault = tz;
} else {
if (!javaAWTAccess.isDisposed()) {
javaAWTAccess.put(TimeZone.class, tz);
if (javaAWTAccess.isMainAppContext()) {
mainAppContextDefault = null;
}
}
}
}
这里用到了javaAWTAccess接口。如果运行时javaAWTAccess为null,那么设置时区就是改变mainAppContextDefault的值;如果运行时javaAWTAccess不为null,那么设置时区就是讲时区方位javaAWTAccess对象中去。进一步观察mainAppContextDefault,发现它是TimeZone类的静态私有变量,也是被全局的JVM共享的,跟设置默认JVM时区没有区别。
那么,就只有一种办法,就是实现一个javaAWTAccess的定制类,并在运行时,让其加载它。但是这种方式只适用于JDK 1.7。
总结
以上三种方案,分别针对JDK1.5,1.6,1.7,并不是通用的解决方案。而Java官方并没有在文档上对setDefault()这个函数做出具体实现上的说明。在这种情况下,针对一种具体实现而开发Java应用,就违背了Java标榜的write once, run anywhere的初衷。所以都是不推荐的。
最稳妥的方案还是:将对时区有不同要求的webapp放在不同的Tomcat实例下运行。