サーブレットリクエストに対してgetReader()を複数回呼ぶには

こんにちは、さるまりんです。

Springで開発中、サーブレットのフィルターでPOSTされた内容を覗き見したいことがありまして、フィルタ内でgetReader()を呼び出しました。

すると

getReader() has already been called for this request

「すでにこのリクエストに対してgetReader()が呼ばれてますよ」のエラー。

複数回呼べないんですね・・・

すでに読み込まれているから利用不可。考えてみれば当然です。

ではどうするか。

どうするかいろいろ調べてみるとこんな方法がstackoverflowにありました。

Http Servlet request lose params from POST body after read it once

なくなってしまうのであればキャッシュしておこう。

ラッパーを使いリクエストをキャッシュする仕組みを利用すればできるようです。

public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
    private ByteArrayOutputStream cachedBytes;
    public MultiReadHttpServletRequest(HttpServletRequest request) {
        super(request);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (cachedBytes == null)
            cacheInputStream();

             return new CachedServletInputStream();
        }

    @Override
    public BufferedReader getReader() throws IOException{
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    private void cacheInputStream() throws IOException {
        // 複数回読めるようにInputStreamをキャッシュします。
        cachedBytes = new ByteArrayOutputStream();
        IOUtils.copy(super.getInputStream(), cachedBytes);
    }

    // キャッシュされたリクエストBodyを読むInputStream
    public class CachedServletInputStream extends ServletInputStream {
        private ByteArrayInputStream input;

        public CachedServletInputStream() {
            // キャッシュされたリクエストBodyからInputStreamを作ります
            input = new ByteArrayInputStream(cachedBytes.toByteArray());
        }

        @Override
        public int read() throws IOException {
            return input.read();
        }
	    
        @Override
        public boolean isFinished() {
            return input.available() == 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener listener) {
            throw new RuntimeException("未実装です");
        }
    }
}

このMultiReadHttpServletRequestを使うとフィルター内でrequest.getReader()を呼び出した際のエラーが回避できました。

フィルター内はこんなコードです。

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

    MultiReadHttpServletRequest multiReadRequest = new MultiReadHttpServletRequest((HttpServletRequest) request);

    // リクエストbodyを文字列として取得する
    String requestBody = multiReadRequest.getReader().lines().collect(Collectors.joining(System.lineSeparator()));

    ...

    // chainのfilterを呼び出す
    chain.doFilter(multiReadRequest, response);
}

他にもいろんな方法があるんだと思いますが、ほとんど既存のコードを変えずに問題を解決できました。詳しい方の知識に感謝しています。

読んでくださってありがとうございました。

それではまた!