Tomcat7.0源码分析——国际化

本文主要介绍一下Tomcat国际化相关的内容,用“国际化”这么高大上的名称我心里还是很忐忑的,如果对“国际化”理解有误还望各位看官指正。简单并狭隘一点地说:本文要讨论的所谓国际化就是Tomcat在它的整个生命周期中所输出的各种字符串能够根据不同的语言环境而输出不同的内容。我们先来直观的感受一下,Tomcat的国际化想要达到的效果是什么样的。

我们在启动Tomcat的时候控制台一般会输出一大堆的英文,如其中的一条:

Deployment of web application directory /home/abinge/src/apache-tomcat-7.0.77-src/webapps/ROOT has finished in 158 ms

不管我们的语言环境设置的是中文还是英文,在这里都是显示的是英文。为什么我们把语言环境设置成了中文却不会用中文显示呢?

这是因为Tomcat并没有提供对中文的支持,Tomcat自带的支持的语言包括英文(默认,即如果Tomcat没有提供对某种语言的支持,若系统环境设置成了该种语言,则默认使用英文)、西班牙文、法文和日文。

不过没有关系,我们自己添加一下对中文的支持,当然这里只是简单的支持一条字符串的中文化,要把所有用到的国际化字符串都添加中文支持是一项浩大的工程。

首先创建资源文件:LocalStrings_zh.properties,文件名中的zh表示对中文的支持,文件内容如下:

hostConfig.deployDir.finished=Web\u5e94\u7528\u76ee\u5f55{0}\u5728{1}\u6beb\u79d2\u5185\u90e8\u7f72\u5b8c\u6210

该文件中不支持直接写上中文字符串,而需要用中文转换成unicode码之后的格式,如何把我们想要的中文转成这种格式呢,可以使用jdk自带的native2ascii工具

abinge@abinge-ubuntu:~$ echo Web应用目录{0}{1}毫秒内部署完成 | native2ascii
Web\u5e94\u7528\u76ee\u5f55{0}\u5728{1}\u6beb\u79d2\u5185\u90e8\u7f72\u5b8c\u6210

然后将该资源放到org.apache.catalina.startup包下面,之所以要放到这个包下面是因为Tomcat是按照包来对这些国际化资源文件进行管理的,不同的包中的类所使用的这些国际化资源文件都在该类所在的包下面。

Tomcat启动过程中的类在org.apache.catalina.startup包下,因此要对Tomcat启动过程进行中文的支持,那么就需要把相应对中文支持的资源文件放在该包下面。我们可以看到,此时该包下面包含5个资源文件,前面四个文件是Tomcat本身就提供的

  • LocalStrings.properties
  • LocalStrings_es.properties
  • LocalStrings_fr.properties
  • LocalStrings_ja.properties
  • LocalStrings_zh.properties

第一个文件名中没有下划线,表示的是默认使用的语言,其余文件名中的es代表西班牙文,fr代表法文,ja代表日文,zh代表中文。这个时候我们再启动Tomcat的话,如果我们的语言环境设置成了中文,那么启动过程中就会出现如下信息:

Web应用目录/home/abinge/src/apache-tomcat-7.0.77-src/webapps/ROOT在158毫秒内部署完成

下面我们再来看看Tomcat是如何做到这一点的:

Tomcat使用org.apache.tomcat.util.res包下的StringManager类来进行国际化封装,如在org.apache.catalina.startup包下的HostConfig类中获取StringManager的实例代码如下:

protected static final StringManager sm =
    StringManager.getManager(Constants.Package);

每一个包下面都有一个Constants类,这些类中都有同一个静态常量Package,值为该类所在包的包名,如这里的这个Constants类中的内容如下:

public static final String Package = "org.apache.catalina.startup";

Tomcat是根据包来组织国际化资源文件的,因此可以解释之前为什么把我们自己的那个LocalStrings_zh.properties文件放在某个具体的包下面。那么Tomcat是如何使用StringManager对象的呢?如Tomcat的启动过程中可能会执行到如下语句:

log.info(sm.getString("hostConfig.deployDir.finished",
                    dir.getAbsolutePath(), Long.valueOf(System.currentTimeMillis() - startTime)));

会打印出来一条日志信息,信息的内容通过sm.getString()方法来获取,该方法的第一个参数传入的是一个key,这里是hostConfig.deployDir.finished,这个key也就是我们在国际化资源文件中所用到的那个key,对于不同语言的国际化资源文件,它们的key是必须要一致的,value是不同的语言所对应的文字,可以推断,Tomcat通过这种统一的方式来进行国际化,该方法内部会根据具体的语言环境来获取不同国际化资源文件中同一个key对应的不同值。

StringManager中提供了两种getString()方法:

  • public String getString(String key)
  • public String getString(final String key, final Object… args)

后面一个方法内部调用了前面一个方法,只不过在通过前面一个方法获取到String之后再通过java.text.MessageFormat来对字符串中的占位符({0},{1}这种格式)进行替换,用第二个可变参数args进行一一替换。可以理解为前一个方法用来获取那些没有占位符的字符串,后面一个方法还提供可变参数对字符串中的占位符进行替换。我们那个例子中就使用的是带占位符的字符串。

我们可以大致看一下后面一个getString方法的源代码:

public String getString(final String key, final Object... args) {
    String value = getString(key);
    if (value == null) {
        value = key;
    }

    MessageFormat mf = new MessageFormat(value);
    mf.setLocale(locale);
    return mf.format(args, new StringBuffer(), null).toString();
}

MessageFormat是jdk自带的方法,用来格式化字符串的,该类的具体使用在这里我们不做更深入的分析。

StringManager的使用我们已经知道了,再来看一下StringManager的实例化,StringManager通过一个静态方法getManager()来进行StringManager对象的获取,该方法有两个重载的方法如下:

public static final StringManager getManager(String packageName)
public static final StringManager getManager(Class clazz)

上面例子中使用的就是第一种,传入一个包名,其实也可以传入当前类的Class对象,即通过如下方式获取StringManager对象:

protected static final StringManager sm =
    StringManager.getManager(HostConfig.class);

第二种方式只不过在内部再通过clazz.getPackage().getName()间接拿到该类所在的包名。

这两个方法底层都会调用同一个getManager()方法:

public static final synchronized StringManager getManager(String packageName, Locale locale)

调用该方法时locale参数都是用的Locale.getDefault(),即都是使用默认的Locale,Locale对象代表具体的地理、政治、文化信息,简单地理解,通过该对象就能知道系统所使用的语言等相关信息,语言也是文化的一种嘛。

比如在我的机器上运行Locale.getDefault()的输出就是zh_CN,代表的是我的系统使用的是中文环境,下划线前面的zh代表语言(简体中文),后面的CN代表的是国家或地区(中国)。又如en_US中en代表的是语言(英文),国家是(美国)。这些都是国际标准中规定的,具体可参考ISO639和ISO3166。

getManager的具体实现代码我们有必要看一下:

public static final synchronized StringManager getManager(
        String packageName, Locale locale) {

    Map map = managers.get(packageName);
    if (map == null) {
        /*
         * Don't want the HashMap to be expanded beyond LOCALE_CACHE_SIZE.
         * Expansion occurs when size() exceeds capacity. Therefore keep
         * size at or below capacity.
         * removeEldestEntry() executes after insertion therefore the test
         * for removal needs to use one less than the maximum desired size
         *
         */
        map = new LinkedHashMap(LOCALE_CACHE_SIZE, 1, true) {
            private static final long serialVersionUID = 1L;
            @Override
            protected boolean removeEldestEntry(
                    Map.Entry eldest) {
                if (size() > (LOCALE_CACHE_SIZE - 1)) {
                    return true;
                }
                return false;
            }
        };
        managers.put(packageName, map);
    }

    StringManager mgr = map.get(locale);
    if (mgr == null) {
        mgr = new StringManager(packageName, locale);
        map.put(locale, mgr);
    }
    return mgr;
}

我们平时写的最多的单例模式中的getInstance()方法都是没有参数的,这里只不过是单例模式的一种变种,是针对于每一个包和每一种语言环境只有一个实例。Tomcat中的实现方式可以归纳如下:

使用两层Hash进行存储,外层的key为包名,内层的key为Locale实例。如果我们只需要根据包名进行实例的获取,那么就只需要一层map结构就够了。所有的StringManager对象都存在StringManager的一个成员变量managers中:

private static final Map> managers = new
    Hashtable>();

内层的Hash实现采用的是LinkedHashMap,该结构比HashMap支持更多的特性,比如能限制存储元素的个数,超过该数目再继续插入元素的时候会根据某种策略对map中的元素进行删除,如果想执行删除策略,需要重写removeEldestEntry方法,该方法默认的实现只返回false,即不会进行删除操作。这个类有点意思,值得深入研究一下。

在这里,对于同一个包不同locale对象的StringManager对象的数目不会超过LOCALE_CACHE_SIZE个,默认值是10

private static int LOCALE_CACHE_SIZE = 10;

最后通过packageName和locale对象调用StringManager的私有构造函数进行实例化:

private StringManager(String packageName, Locale locale) {
    String bundleName = packageName + ".LocalStrings";
    ResourceBundle bnd = null;
    try {
        bnd = ResourceBundle.getBundle(bundleName, locale);
    } catch (MissingResourceException ex) {
        // Try from the current loader (that's the case for trusted apps)
        // Should only be required if using a TC5 style classloader structure
        // where common != shared != server
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        if (cl != null) {
            try {
                bnd = ResourceBundle.getBundle(bundleName, locale, cl);
            } catch (MissingResourceException ex2) {
                // Ignore
            }
        }
    }
    bundle = bnd;
    // Get the actual locale, which may be different from the requested one
    if (bundle != null) {
        Locale bundleLocale = bundle.getLocale();
        if (bundleLocale.equals(Locale.ROOT)) {
            this.locale = Locale.ENGLISH;
        } else {
            this.locale = bundleLocale;
        }
    } else {
        this.locale = null;
    }
}

重点是这里的ResourceBundle类,该类是JDK提供的进行国际化操作的类,该类通过getBundle()静态方法获取对象,该方法需要指定bundle名称和Locale对象,可以简单的理解为,根据这两个参数就能知道需要使用的是哪个国际化资源文件,因此对国际化资源文件进行命名的时候必须要按照一定的规则进行。

这里的bundleName是根据包名和自定义名称"LocalStings"用点号拼接起来的,因此各个包下面的国际化资源文件名中都是以自定义名称LocalStrings开头的,针对于不同的Locale,文件名需要用下划线将不同的2个字符的语言码(如zh,en等)分隔开来,即如果Locale对象代表的是中文,则使用的是LocalStrings_zh.properties文件,以此类推,之后就可以使用ResourceBundle对象的getString(String key)方法来获取相应的值。

这里只是说了ResourceBundle类的最基本的用法。ResourceBundle的具体使用可以查看JDK源码或者文档进行了解,该类与Locale类一样都在java.util包下面,这里不再过多介绍。

现在在回过头来看一下StringManager类中的getString(String key)方法。想必大家已经能猜到了StringManager中的getString方法内部使用的就是ResourceBundle实例的getString方法:

public String getString(String key) {
    if (key == null){
        String msg = "key may not have a null value";
        throw new IllegalArgumentException(msg);
    }

    String str = null;

    try {
        // Avoid NPE if bundle is null and treat it like an MRE
        if (bundle != null) {
            str = bundle.getString(key);
        }
    } catch (MissingResourceException mre) {
        //bad: shouldn't mask an exception the following way:
        //   str = "[cannot find message associated with key '" + key +
        //         "' due to " + mre + "]";
        //     because it hides the fact that the String was missing
        //     from the calling code.
        //good: could just throw the exception (or wrap it in another)
        //      but that would probably cause much havoc on existing
        //      code.
        //better: consistent with container pattern to
        //      simply return null.  Calling code can then do
        //      a null check.
        str = null;
    }

    return str;
}

总之:Tomcat利用JDK提供的ResourceBundle类和Locale类来进行国际化的支持,具体实现中是通过StringManager类来进行封装,StringManager是按照包进行组织的,每个包都包含不同的StringManager对象。
相同包中针对不同的Locale也使用不同的StringManager对象,同一包中的StringManager对象个数默认不会超过10个,采用的是LinkedHashMap对同一个包下不同Locale的StringManager对象进行存储。

你可能感兴趣的:(Tomcat)