【问题标题】:Strange intermittent character encoding behaivor in Tomcat serverTomcat 服务器中奇怪的间歇性字符编码行为
【发布时间】:2012-12-21 11:44:14
【问题描述】:

在我们的生产环境中,我们时常会在Tomcat中遇到一个非常奇怪的编码问题。

我还不能确定问题发生在代码中的确切位置,但它涉及将非 ascii 字符替换为近似的 ascii 字符。

例如将字符 'å' 替换为 'a'。由于该站点是瑞典语,因此字符“å”、“ä”和“ö”很常见。但是由于某种原因,替换 'ö' 字符总是有效的,所以像“Köp inte grisen i säcken”这样的字符串变成了“Kop inte grisen i säcken”,即 'ä' 没有被替换,而 ' ö' 字符是。

关于这个问题的一些简单的事实:

  • 这种情况很少发生(我们注意到它 3-4 次,第一次可能是 1-2 年前)。

  • 重启有问题的服务器可以解决问题(直到下一次)。

  • 从未在超过一台前端服务器上同时发生过。

  • 它并不总是发生在同一个前端服务器上。

  • 不涉及前端的用户输入。

  • 所有前端服务器都连接到同一个 CMS 和 DB,相关配置相同。

  • 所有前端服务器都具有相同的相关配置(linux 配置、tomcat 配置、java 环境配置,如“file.encoding”等),并使用相同的脚本启动(全部根据托管/服务提供商)。

  • 所有前端服务器都使用相同的站点战争文件和相同的 jar 文件。

  • 出现此字符替换问题时,站点上没有其他编码问题。

  • 我们从未能够在任何其他环境中重现此问题。

由于 CMS 要求,我们使用 Tomcat 5.5 和 Java 5。

对于这种行为,我只能想到两个可能的原因:

  1. 托管服务提供商有时会以不同的方式启动/重新启动前端服务器,可能使用具有其他环境变量或其他文件访问权限的另一个用户帐户,或者可能使用不同于正常脚本的其他脚本。

  2. 在 Tomcat 或 webapp 启动期间运行的某些进程依赖于其他进程,有时(间歇性但很少)这两个(或更多)进程碰巧按导致此编码缺陷的顺序运行。

但是即使上面的 1 或 2 是这种情况,它仍然不能完全解释真正发生的事情。有什么确切的区别可以解释这一点?由于所有“file.encoding”、“file.encoding.pkg”、“sun.io.unicode.encoding”、“sun.jnu.encoding”和所有其他相关环境变量在所有前端机器上都匹配(使用目视验证调试页面,而问题正在发生)。

对于这种奇怪的间歇性行为,有人能想出一些合理的解释吗?简单地升级 Tomcat 和/或 Java 版本并不是一个真正相关的答案,因为我们真的不知道这是否会解决问题,它仍然不能解释问题是什么。我更感兴趣的是确切地了解问题是由什么引起的。

问候 /吉米

更新:

我想我找到了执行字符替换的代码。在初始化时(由第一次调用来进行替换)它构建一个 HashMap,并像这样填充它:

lookup.put(new Character('å'), "a");  

然后当它应该替换字符串的字符时,它会遍历每个字符,并以字符为键在哈希映射中查找每个字符,如果找到替换字符串,则使用它,否则原始使用字符。

这部分代码已经有 3 年多的历史了,并且是由一个早已不复存在的开发人员编写的。如果我今天重写这段代码,我会做一些完全不同的事情,这甚至可以解决问题。但它仍然无法准确解释发生了什么。有人能看到一些可能的解释吗?

【问题讨论】:

  • 听起来像是一个多线程问题,有些不是线程安全的操作。我见过类似的问题,每隔几周就会发生一次错误转换。原因始终是对共享数据的非线程安全访问。尝试给机器增加一些非常重的负载,强制进行大量并行字符转换,看看会发生什么。
  • 问题是一旦服务器进入这种麻烦的模式,所有这些操作都会以同样的错误方式结束,每次都以相同的方式结束,直到服务器重新启动。因此,如果多线程是原因,您认为它到底触发了什么?此外,让生产前端承受重负载会对网站访问者产生负面影响,这不是我们愿意做的事情。
  • 在不知道代码的情况下,几乎不可能说出来。尝试隔离转换例程并在繁重的多线程环境中运行它们。
  • 我刚刚用一些代码更新了这个问题。假期后可能有时间做一些本地多线程测试,尽管我仍然不相信这到底是怎么造成的。对此有何理论解释?

标签: java tomcat encoding character intermittent


【解决方案1】:

在进行替换之前,将输入标准化为正常的 Form C。

例如,ä 可以只有 1 个字符,U+00E4,也可以是两个字符,a (U+0061) 和组合分音符号 U+0308

如果您的替换只是查找组合表单,那么分解后的表单仍将保持为 \u0061\u0308,因为它们都不匹配 \u00e4

public static void main(String args[]) {
    String decomposed = "\u0061\u0308";
    String composed = "\u00e4";

    System.out.println(decomposed);
    System.out.println(composed);
    System.out.println(composed.equals(decomposed));
    System.out.println(Normalizer
            .normalize(decomposed, Normalizer.Form.NFC).equals(composed));

}

输出

ä
ä
false
true

【讨论】:

  • 好的,这很有趣。字符编码是信息的丛林。但是......它仍然不能解释为什么会间歇性地发生这种情况。即偶尔,其中一个前端服务器会以这种奇怪的“模式”结束,其中来自同一个 CMS 的相同字符串数据每次都被不同地处理,直到该服务器重新启动。
  • @user1921254 是的,但可以尝试一下。我无法真正远程诊断它……或者即使我身临其境,这仍然是一次冒险
  • 输入字符串中所有字符的 int 值的调试打印输出,以及发生此问题时的替换 HashMap,将有助于找出是否涉及组合字符和分解字符的混合,对吧?因为我已经在代码中添加了。
猜你喜欢
  • 2021-01-27
  • 1970-01-01
  • 2011-03-13
  • 2015-08-11
  • 2019-01-28
  • 1970-01-01
  • 2012-02-09
  • 1970-01-01
  • 2011-07-27
相关资源
最近更新 更多