第一章:Servlet的“隐形契约”——Tomcat的自动配置

大多数开发者习惯了Spring Boot的自动装配,却忘了原生Servlet规范本身就有一套强大的国际化机制。这套机制在Tomcat中被完美实现,无需任何Java代码,仅靠命名规则就能生效。

核心原理:
Tomcat利用 HttpServletRequest 的 getLocale() 和 getLocales() 方法,自动感知HTTP请求头中的 Accept-Language 字段。配合Java的 ResourceBundle,它能自动寻找匹配的语言包。

1.1 资源文件的“魔法命名”

在 src/main/resources 目录下,我们需要建立一套特定的命名规则:

// 文件名:messages.properties (默认基础文件,通常为英语)
// 内容:
greeting=Hello
error.database=Database connection failed
welcome.message=Welcome, {0}! Today is {1}.

// 文件名:messages_zh_CN.properties (中文简体)
// 内容:
greeting=你好
error.database=数据库连接失败
welcome.message=欢迎,{0}!今天是{1}。

// 文件名:messages_fr_FR.properties (法语)
// 内容:
greeting=Bonjour
error.database=Échec de la connexion à la base de données
welcome.message=Bienvenue, {0} ! Aujourd’hui est {1}.

注意: 基础文件(默认)必须存在,否则当遇到不支持的语言时,系统会抛出 MissingResourceException。

1.2 JSP中的“一键魔法”

在JSP页面中,我们不需要写任何Java脚本(Scriptlet)。只需使用JSTL(JSP Standard Tag Library)的 标签,Tomcat就会自动完成一切。

        "/>

深度解析:
标签内部调用了 ResourceBundle.getBundle()。Tomcat的 ApplicationContext 会拦截这个调用,并根据当前线程绑定的 Locale(由 HttpServletRequest 注入)去加载对应的属性文件。这期间没有任何硬编码的 if-else 判断语言,完全是基于Java的SPI(Service Provider Interface)机制。

第二章:字符编码的“生死防线”

老王提到的“乱码”问题,通常不是i18n逻辑的错,而是字符集(Charset)没配置好。Tomcat有一套全局的编码过滤机制。

2.1 server.xml 的“全局锁”

在 conf/server.xml 中,我们需要为Connector显式指定 URIEncoding:

URIEncoding="UTF-8"


useBodyEncodingForURI="false"

connectionTimeout="20000"
redirectPort="8443" />

2.2 Filter的“请求拦截”

虽然Tomcat可以处理GET参数的解码,但对于POST请求的Body,我们需要一个过滤器来设置 request.setCharacterEncoding。

@WebFilter(“/*”)
public class EncodingFilter implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {
    
    HttpServletRequest req = (HttpServletRequest) request;
    HttpServletResponse resp = (HttpServletResponse) response;

    // 【关键步骤】
    // 必须在获取任何参数之前调用
    // 否则Tomcat会使用默认编码(ISO-8859-1)解析一次,再设UTF-8就晚了
    req.setCharacterEncoding("UTF-8");
    
    // 设置响应编码,告诉浏览器用UTF-8解析
    resp.setCharacterEncoding("UTF-8");
    resp.setContentType("text/html;charset=UTF-8");

    // 放行
    chain.doFilter(req, resp);
}

}

注意: setCharacterEncoding 方法只有在 getReader() 或 getParameter() 调用之前执行才有效。这也是为什么它必须放在过滤器链的第一个位置。

第三章:LocaleResolver的“人工干预”

Tomcat的自动机制依赖于HTTP头。但在某些场景下(如用户在网页上手动点击“切换为中文”),我们需要覆盖浏览器的默认设置。

3.1 CookieLocaleResolver(如果使用Spring)

虽然标题是Tomcat原生,但考虑到大多数Java Web项目会引入Spring,我们可以利用Tomcat的 Filter 机制结合Spring的解析器:

// 这是一个自定义的Locale解析Filter
public class CustomLocaleFilter implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException {

    HttpServletRequest req = (HttpServletRequest) request;
    HttpServletResponse resp = (HttpServletResponse) response;

    // 1. 先检查URL参数中是否有语言标记 (e.g., ?lang=zh_CN)
    String langParam = req.getParameter("lang");
    Locale targetLocale = null;

    if (langParam != null && !langParam.isEmpty()) {
        String[] parts = langParam.split("_");
        if (parts.length == 2) {
            targetLocale = new Locale(parts[0], parts[1]);
        } else {
            targetLocale = new Locale(langParam);
        }

        // 2. 将选择的语言存入Cookie,下次直接读取
        // 这样就覆盖了浏览器的Accept-Language
        Cookie localeCookie = new Cookie("LOCALE", langParam);
        localeCookie.setMaxAge(60 * 60 * 24 * 30); // 30天
        localeCookie.setPath("/");
        resp.addCookie(localeCookie);
    } 
    else 
    {
        // 3. 如果URL没有参数,检查Cookie
        Cookie[] cookies = req.getCookies();
        if (cookies != null) {
            for (Cookie cookie : cookies) {
                if ("LOCALE".equals(cookie.getName())) {
                    String localeStr = cookie.getValue();
                    String[] parts = localeStr.split("_");
                    if (parts.length == 2) {
                        targetLocale = new Locale(parts[0], parts[1]);
                    } else {
                        targetLocale = new Locale(localeStr);
                    }
                    break;
                }
            }
        }
    }

    // 4. 如果检测到了目标语言,使用Decorator模式包装Request
    // 强行修改其getLocale()的返回值
    if (targetLocale != null) {
        LocaleRequestWrapper wrapper = new LocaleRequestWrapper(req, targetLocale);
        chain.doFilter(wrapper, response);
        return;
    }

    // 5. 否则,使用Tomcat默认的Locale(基于HTTP头)
    chain.doFilter(request, response);
}

}

// 请求包装器:覆盖getLocale方法
class LocaleRequestWrapper extends HttpServletRequestWrapper {
private Locale locale;

public LocaleRequestWrapper(HttpServletRequest request, Locale locale) {
    super(request);
    this.locale = locale;
}

@Override
public Locale getLocale() {
    return locale;
}

@Override
public Enumeration getLocales() {
    // 返回单个元素的枚举
    return Collections.enumeration(Arrays.asList(locale));
}

}

深度解析:
通过 HttpServletRequestWrapper,我们欺骗了后续的处理链(包括JSTL的 标签)。当JSTL调用 request.getLocale() 时,它拿到的是我们设定的 Locale,而不是浏览器发来的。这样就实现了“点击按钮切换语言”的功能,而底层的i18n逻辑完全不需要修改。

第四章:日期与数字的“本地化格式”

国际化不仅仅是文字翻译,还包括数字和日期的格式。Tomcat配合JSTL可以自动处理这些细节。

1,234.56
de_DE -> 1.234,56
zh_CN -> 1,234.56 (通常遵循国际惯例)
–>

$1,234.56 -->
¥1,234 -->

Jan 1, 2023 -->
2023年1月1日 -->
" type=“date”/>

终章:破案与救赎

我让老王在客户的法语浏览器环境中,打开我们的测试页面。页面上不再是乱码的“échec”,而是清晰的“Échec de la connexion”。

“搞定了?”老王的声音颤抖着。

“是的。”

“你改了多少代码?加了多少配置类?”

“一行代码都没改。我只是在 web.xml 里加了一个编码过滤器,并且确保 messages_fr_FR.properties 文件的编码是UTF-8且已部署。”

总结:
Tomcat的“一键魔法”并非真正的魔法,而是约定优于配置(Convention over Configuration)的胜利。
命名约定:basename_language_country.properties 让 ResourceBundle 自动定位。
协议约定:Accept-Language HTTP头直接映射到 Locale。
容器约定:JSTL 标签自动消费这些约定。

只要守住 UTF-8 这条底线,Java的i18n其实可以像呼吸一样自然。这,就是Tomcat赋予Java Web开发的“零配置”魅力。

更多推荐