Tomcat + Play framework 讀不到POST的資料

最近在進行的play framework部署到tomcat上執行所遇到的問題

這裡記錄一下問題發生狀況

測試環境
- Scala 2.11
- Play 2.3.8 (https://www.playframework.com)
- tomcat 6
- play2war 1.3-beta3 (https://github.com/play2war/play2-war-plugin/)

部署方法:將play framework application打包成war之後放到tomcat上執行

開始只有tomcat+play的時候還執行的蠻順利的

tomcat -> play

之後加上tomcat的servlet filter之後出現play讀取不到post data的狀況

但是使用spring等卻不會有此問題

tomcat -> filter -> play

會加上filter是因為正式環境上通常會有很多前人留下的規則套用,像是SSO或是驗證登入等等

雖說play framework有提供自己的filter機制,但是畢竟很難在去自己再重新造輪子

所以必須使用前人的留下的servlet filter

幾經一番波折之後,在監聽封包的情況下確定POST DATA有送過來

那可能的問題就是出在servlet filter到play這段出了問題

一個簡單的示意圖如下(個人理解,不一定完全正確)

正常一個http request到了tomcat之後會變成HttpServletRequest並且自帶一個InputStream丟給servlet container如spring之類的

而play必須透過play2war這一層的包裝,除了一些基本的header資料之外剩下POST等透過InputStream讀取

但是一旦加了servlet filter的話,如下圖所示

如果filter有用到InputStream的話,如呼叫一些getParameterNames等函式

InputStream的資料就會變成HttpServletRequest的一部分,對servlet container沒有影響

但是play還在等待InputStream的資料被前面的filter吞掉了只剩下一個什麼都沒有的empty stream

因為這層關係讓play難以跟tomcat一般的filter共存

一個可行的方法是在一般filter之前加上一個wrapper filter對InputStream的資料進行cache

tomcat -> cache data filter -> other filter -> play

servlet filter在web.xml的設定是有順序的

透過warpper filter(圖中黃色部分)擋在最前面將資料cache起來除了供給其他filter使用,同時也不會影響到play對資料的讀取

cache的實作方式可以參考 https://github.com/ausaccessfed/applicationbase/blob/master/src/java/aaf/base/util/http/MultiReadHttpServletRequest.java

有了MultiReadHttpServletRequest物件就可以自己簡單實作一個warpper filter

MSRFilter.java
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.*;

import org.apache.commons.io.IOUtils;

class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
    private final byte[] body;

    public MultiReadHttpServletRequest(final HttpServletRequest httpServletRequest) throws IOException {
        super(httpServletRequest);
        // Read the request body and save it as a byte array

        body = IOUtils.toByteArray(super.getInputStream());
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return new ServletInputStreamImpl(new ByteArrayInputStream(body));
    }

    public byte[] getBodyBytes() {
        return body;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        String enc = getCharacterEncoding();
        if(enc == null) enc = "UTF-8";
        return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(body), enc));
    }

    private class ServletInputStreamImpl extends ServletInputStream {

        private final InputStream is;

        public ServletInputStreamImpl(final InputStream is) {
            this.is = is;
        }

        @Override
        public int read() throws IOException {
            return is.read();
        }

        @Override
        public boolean markSupported() {
            return false;
        }

        @Override
        public synchronized void mark(final int i) {
            throw new RuntimeException(new IOException("mark/reset not supported"));
        }

        @Override
        public synchronized void reset() throws IOException {
            throw new IOException("mark/reset not supported");
        }
    }
}

public final class MSRFilter implements Filter {

    public MSRFilter() {
    }

    @Override
    public void doFilter(final ServletRequest servletRequest,
            final ServletResponse servletResponse,
            final FilterChain filterChain) throws IOException,
            ServletException {
        MultiReadHttpServletRequest request = new MultiReadHttpServletRequest((HttpServletRequest) servletRequest);
        HttpServletResponse response = (HttpServletResponse) servletResponse;
        filterChain.doFilter(request, response);
    }

    @Override
    public void init(final FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
}

P.S. 上面的實作有bug,似乎會造成cookie或是其他filter的有意外狀況

comments powered by Disqus