如何让Tomcat中的webapp使用不同的时区

来源:互联网 发布:侣皓吉吉和盛一伦 知乎 编辑:程序博客网 时间:2024/05/21 10:18

引入问题

有两个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实例下运行。

0 0