【Java】HttpServletRequest/ResponseのWrapperクラスを実装する

備忘録です。SpringBootなどのFWに頼る場合、2025年現在でこういうコードを書くことは殆どない気がしますが念の為。

素のJava ServletにてWeb開発を行っていて、RequestのURIなどを加工する必要があるときなどの話です。

HttpServletRequest/ResponseのWrapper

Java Servletでの開発を行う際、リクエスト、およびレスポンスの実態は HttpServletRequest / HttpServletResponse のクラスがそれぞれ担います。

このクラスを拡張するための仕組みとして、HttpServletRequestWrapperHttpServletResponseWrapper というクラスが用意されており、これらを継承して独自クラスを作ることが可能です。

例えば、HttpServletResponseWrapper を継承することで、レスポンス本文やヘッダを横取り・加工することができます。 同様に HttpServletRequestWrapper を継承することで、リクエストのパラメータやパス情報などを差し替えることも可能です。


Filter でのラップ構成

Java Servletでは、HttpServletRequestWrapper を使うと HttpServletRequest のメソッドをオーバーライドして振る舞いを上書きできます。

これを活用し、Filterの先頭で HttpServletRequestWrapper を差し込み、 アプリ全体に「/project を取り除いた値」を見せるようにしました。

public class LogicalContextPathRequestWrapper extends HttpServletRequestWrapper {
    private static final String PHYSICAL_CONTEXT = "/project";

    public LogicalContextPathRequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public String getContextPath() {
        String ctx = super.getContextPath(); // "/project"
        if (ctx.startsWith(PHYSICAL_CONTEXT)) {
            return ctx.substring(PHYSICAL_CONTEXT.length()); // → ""
        }
        return ctx;
    }

    @Override
    public String getRequestURI() {
        String uri = super.getRequestURI();
        if (uri.startsWith(PHYSICAL_CONTEXT)) {
            return uri.substring(PHYSICAL_CONTEXT.length());
        }
        return uri;
    }
}
public class CommonWrappingFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        HttpServletRequest httpReq = (HttpServletRequest) request;
        HttpServletResponse httpRes = (HttpServletResponse) response;

        // 必要に応じて Response も wrap できる
        chain.doFilter(new LogicalContextPathRequestWrapper(httpReq), httpRes);
    }
}
<!-- web.xml -->
<filter>
  <filter-name>commonWrappingFilter</filter-name>
  <filter-class>com.example.CommonWrappingFilter</filter-class>
</filter>

<filter-mapping>
  <filter-name>commonWrappingFilter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

これにより、既存コードは**getContextPath() を今まで通り "" として扱える**ようになり、 フロント()から見えるURL構造と齟齬が出なくなりました。


補足:設計上の注意点

  • Wrapper を入れると、実際のリクエスト実装クラス(例:RequestFacade)ではなくなるため、 既存コードで具体クラスにキャストしている箇所があると ClassCastException になります。 → 正しくは HttpServletRequest インタフェースで扱うべきです。
  • 他のFilterでも別のWrapperを重ねることがあるため、このFilterをチェーンの先頭に置くようにします。 (多重ラップ時は一番外側のオーバーライドだけが有効になるため)
  • 多重ラップ環境では、instanceofgetRequest() を使って内側のWrapperを探索するのが一般的です。

多重ラップされた中から特定のWrapperを探す

Wrapperを継承したクラスに、独自のメソッドをもたせた場合、そのメソッドを呼び出すときに直接キャストをすると、ClassCastExceptionが起きる場合があります。

これは、AWrapperとBWrapperでwrapされたとき、最後にwrapしたクラスがどちらかによって、インスタンスの実体が異なるからです。

そのため、下記のようなコードで、実際に使用したいクラスをWrapperを探索する必要があります。

@SuppressWarnings("unchecked")
public static <T> T findWrappedRequest(HttpServletRequest request, Class<T> targetType) {
    HttpServletRequest current = request;
    while (current instanceof HttpServletRequestWrapper) {
        if (targetType.isInstance(current)) {
            return (T) current;
        }
        current = (HttpServletRequest) ((HttpServletRequestWrapper) current).getRequest();
    }
    return null;
}

使い方:

LogicalContextPathRequestWrapper wrapper =
    findWrappedRequest(request, LogicalContextPathRequestWrapper.class);

if (wrapper != null) {
    String logical = wrapper.getContextPath();
}
  • これにより、入れ子の中から目的の型だけを安全に取り出すことができます
  • 同様のロジックで HttpServletResponseWrapper 版を作ることも可能です

まとめ

  • HttpServletRequestWrapperHttpServletResponseWrapper は、Servletの動作を安全に上書きできる仕組み
  • Filter で全リクエストに共通適用する構成にすれば、既存コードを壊さず横断的に機能追加できる
  • 今回は getContextPath() を補正するために導入したが、他にもログ追跡やヘッダ強制追加など幅広く応用できる
  • 多重ラップ環境では、instanceof+unwrap探索の仕組みを用意しておくと後々の保守性が高まる