TomcatのServletRequest#setCharacterEncoding()問題まとめ

1. 問題の概要

Tomcatの4.1.29以降の4.1.*系バージョン、ならびに5.0.16以降の5.0.*系バージョンでは、ServletRequestクラスのsetCharacterEncoding()メソッドの挙動が変更されています。POSTリクエストのbodyで渡されるパラメータのコード変換は(これまで通り)行われますが、GETリクエストのクエリーストリングで渡されるパラメータのコード変換は行われないようになりました。以前はメソッドによらず、パラメータのコード変換はsetCharacterEncoding()メソッドで行うことができたので、Tomcat 4.1.29/5.0.16以前のバージョンに慣れていると、思わぬ落とし穴にはまります。

2. 経緯

SunのServlet仕様のAPIドキュメントで、ServletRequest#setCharacterEncoding()メソッドの説明を読むと、

Overrides the name of the character encoding used in the body of this request. This method must be called prior to reading request parameters or reading input using getReader().

とあるので(赤字部分に注目)、実はこの変更は仕様変更ではなく、正しい仕様を満たすためのバグ修正なんです。ですので、今後は「setCharacterEncoding()ではクエリーストリングで渡されるパラメータのコード変換は行われない。それがServlet API 2.3の正しい仕様」という認識を持っておく必要があります。別の表現をすると「日本語の文字列をちゃんと処理しようと思ったら、setCharacterEncoding()一発だけではダメだよ」ってことです。

3. 対処方法

サーブレットフィルタを使用している場合は、フィルタ内で吸収してしまうのが一番手軽です。よく使われている(と思われる)、SetCharacterEncodingFilterクラスならば、doFilter()メソッドを以下のように書き換えれば良いでしょう。

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
        throws IOException, ServletException
    {
        // Conditionally select and set the character encoding to be used
        if (ignore || (request.getCharacterEncoding() == null))
        {
            String encoding = selectEncoding(request);
            if (encoding != null)
            {
                request.setCharacterEncoding(encoding);

                // HTTP-GETの場合、ServletRequest#setCharacterEncoding()
                // ではパラメータのコード変換は行われない。(Tomcat 4.1.29/5.1.16以降)
                // しかたがないので、自力で変換する。
                if (decodeHttpGetParameter)
                {
                    if ((request instanceof HttpServletRequest) &&
                        ("GET".equalsIgnoreCase(((HttpServletRequest)request).getMethod())))
                    {
                        Map parameterMap = request.getParameterMap();
                        if (parameterMap != null)
                        {
                            for (Iterator it = parameterMap.keySet().iterator(); it.hasNext(); )
                            {
                                String name = (String)it.next();
                                String[] values = request.getParameterValues(name);
                                if (values != null)
                                {
                                    for (int i = 0; i < values.length; i++)
                                    {
                                        values[i] = new String(values[i].getBytes("ISO-8859-1"), encoding);
                                    }
                                }
                            }
                        }
                    }
                }

            }
        }

        // Pass control on to the next filter
        chain.doFilter(request, response);
    }

doFilter()メソッド以外は省略しています。decodeHttpGetParameterはクラス変数で、フィルタの初期化パラメータを利用してtrueまたはfalseにセットされるようにしています。web.xmlの設定で、処理を切り替えられるようにするわけです。

フィルタを使用せず、サーブレット内で処理する場合は、

(自分で考えましょう)

4. Tomcat以外のServletコンテナの状況

1. Weblogic

WebLogic Server のI18n(国際化)では、「setCharacterEncoding()でOK」と書かれているように読めます。「setCharacterEncoding()は Servlet 2.3 の仕様に準拠しています」という記述とは矛盾していますね・・・。

2. WebSphere

Servlet2.3/JSP1.2/HTTP Session Management(pdf)
のp.30「リクエストの文字コード設定」で、「setCharacterEncoding()しましょうね」という記述。やっぱりServlet 2.3仕様を満たしていないことになるのかなあ。

3. Oracle Application Server

(未稿)

4. Jetty

(未稿)

5. あとなんかあったっけ?

ちゃんと実際に確認したわけではありません。Web上で拾える情報を元に推測で書いています。誰か教えて。