Javaサーブレットフィルターの2つのhttp要求パラメーターにアクセスしようとしていますが、ここでは新しいことではありませんが、パラメーターが既に消費されていることに驚いています!このため、フィルターチェーンでは使用できなくなりました。
これは、パラメータがPOSTリクエストボディ(たとえば、フォーム送信)に含まれる場合にのみ発生するようです。
パラメーターを読み取って消費しない方法はありますか?
これまでのところ、私はこの参照のみを見つけました: request.getParameterを使用したサーブレットフィルターはフォームデータを失います 。
ありがとう!
余談ですが、この問題を解決する別の方法は、フィルターチェーンを使用せずに、おそらく解析された要求本文を操作できるアスペクトを使用して、独自のインターセプターコンポーネントを構築することです。また、リクエストInputStream
を独自のモデルオブジェクトに変換するのは一度だけなので、より効率的です。
ただし、特に要求がフィルターチェーンを通過するときに、要求本文を複数回読み取ることは依然として妥当だと思います。通常、HTTPレイヤーに保持したい特定の操作には、サービスコンポーネントから切り離されたフィルターチェーンを使用します。
Will Hartung で示唆されているように、HttpServletRequestWrapper
を拡張し、リクエストInputStream
を消費し、基本的にバイトをキャッシュすることでこれを達成しました。
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 {
/* Cache the inputstream in order to read it multiple times. For
* convenience, I use Apache.commons IOUtils
*/
cachedBytes = new ByteArrayOutputStream();
IOUtils.copy(super.getInputStream(), cachedBytes);
}
/* An inputstream which reads the cached request body */
public class CachedServletInputStream extends ServletInputStream {
private ByteArrayInputStream input;
public CachedServletInputStream() {
/* create a new input stream from the cached request body */
input = new ByteArrayInputStream(cachedBytes.toByteArray());
}
@Override
public int read() throws IOException {
return input.read();
}
}
}
これで、フィルターチェーンを通過する前に元の要求をラップすることにより、要求本文を複数回読み取ることができます。
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
/* wrap the request in order to read the inputstream multiple times */
MultiReadHttpServletRequest multiReadRequest = new MultiReadHttpServletRequest((HttpServletRequest) request);
/* here I read the inputstream and do my thing with it; when I pass the
* wrapped request through the filter chain, the rest of the filters, and
* request handlers may read the cached inputstream
*/
doMyThing(multiReadRequest.getInputStream());
//OR
anotherUsage(multiReadRequest.getReader());
chain.doFilter(multiReadRequest, response);
}
}
このソリューションでは、getParameterXXX
メソッドを使用してリクエスト本文を複数回読み取ることもできます。これは、基になる呼び出しがgetInputStream()
であるため、もちろんキャッシュされたリクエストInputStream
を読み取るためです。
編集
ServletInputStream
インターフェイスの新しいバージョンの場合。 isReady
、setReadListener
など、さらにいくつかのメソッドの実装を提供する必要があります。以下のコメントで提供されているように、これを参照してください question 。
遅れていることはわかっていますが、この質問は私にとっても関連性があり、このSOの投稿はGoogleのトップヒットの1つでした。他の誰かが数時間節約できることを期待して、私は先に進んで私の解決策を投稿します。
私の場合、私はすべての要求と応答を彼らの体で記録する必要がありました。 Spring Frameworkを使用すると、答えは実際には非常に単純で、単に ContentCachingRequestWrapper および ContentCachingResponseWrapper を使用します。
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import Java.io.IOException;
public class LoggingFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper((HttpServletResponse) response);
try {
chain.doFilter(requestWrapper, responseWrapper);
} finally {
String requestBody = new String(requestWrapper.getContentAsByteArray());
String responseBody = new String(responseWrapper.getContentAsByteArray());
// Do not forget this line after reading response content or actual response will be empty!
responseWrapper.copyBodyToResponse();
// Write request and response body, headers, timestamps etc. to log files
}
}
}
上記の答えは非常に役に立ちましたが、私の経験にはまだいくつかの問題がありました。 Tomcat 7サーブレット3.0では、getParamterおよびgetParamterValuesも上書きする必要がありました。ここでのソリューションには、get-queryパラメーターとpost-bodyの両方が含まれています。生文字列を簡単に取得できます。
他のソリューションと同様に、Apache commons-ioとGoogles Guavaを使用します。
このソリューションでは、getParameter *メソッドはIOExceptionをスローしませんが、super.getInputStream()を使用して(本体を取得する)IOExceptionをスローする場合があります。キャッチしてruntimeExceptionをスローします。それはとても素敵ではありません。
import com.google.common.collect.Iterables;
import com.google.common.collect.ObjectArrays;
import org.Apache.commons.io.IOUtils;
import org.Apache.http.NameValuePair;
import org.Apache.http.client.utils.URLEncodedUtils;
import org.Apache.http.entity.ContentType;
import Java.io.BufferedReader;
import Java.io.ByteArrayInputStream;
import Java.io.ByteArrayOutputStream;
import Java.io.IOException;
import Java.io.InputStreamReader;
import Java.io.UnsupportedEncodingException;
import Java.nio.charset.Charset;
import Java.util.Collections;
import Java.util.LinkedHashMap;
import Java.util.List;
import Java.util.Map;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
/**
* Purpose of this class is to make getParameter() return post data AND also be able to get entire
* body-string. In native implementation any of those two works, but not both together.
*/
public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
public static final String UTF8 = "UTF-8";
public static final Charset UTF8_CHARSET = Charset.forName(UTF8);
private ByteArrayOutputStream cachedBytes;
private Map<String, String[]> parameterMap;
public MultiReadHttpServletRequest(HttpServletRequest request) {
super(request);
}
public static void toMap(Iterable<NameValuePair> inputParams, Map<String, String[]> toMap) {
for (NameValuePair e : inputParams) {
String key = e.getName();
String value = e.getValue();
if (toMap.containsKey(key)) {
String[] newValue = ObjectArrays.concat(toMap.get(key), value);
toMap.remove(key);
toMap.put(key, newValue);
} else {
toMap.put(key, new String[]{value});
}
}
}
@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 {
/* Cache the inputStream in order to read it multiple times. For
* convenience, I use Apache.commons IOUtils
*/
cachedBytes = new ByteArrayOutputStream();
IOUtils.copy(super.getInputStream(), cachedBytes);
}
@Override
public String getParameter(String key) {
Map<String, String[]> parameterMap = getParameterMap();
String[] values = parameterMap.get(key);
return values != null && values.length > 0 ? values[0] : null;
}
@Override
public String[] getParameterValues(String key) {
Map<String, String[]> parameterMap = getParameterMap();
return parameterMap.get(key);
}
@Override
public Map<String, String[]> getParameterMap() {
if (parameterMap == null) {
Map<String, String[]> result = new LinkedHashMap<String, String[]>();
decode(getQueryString(), result);
decode(getPostBodyAsString(), result);
parameterMap = Collections.unmodifiableMap(result);
}
return parameterMap;
}
private void decode(String queryString, Map<String, String[]> result) {
if (queryString != null) toMap(decodeParams(queryString), result);
}
private Iterable<NameValuePair> decodeParams(String body) {
Iterable<NameValuePair> params = URLEncodedUtils.parse(body, UTF8_CHARSET);
try {
String cts = getContentType();
if (cts != null) {
ContentType ct = ContentType.parse(cts);
if (ct.getMimeType().equals(ContentType.APPLICATION_FORM_URLENCODED.getMimeType())) {
List<NameValuePair> postParams = URLEncodedUtils.parse(IOUtils.toString(getReader()), UTF8_CHARSET);
params = Iterables.concat(params, postParams);
}
}
} catch (IOException e) {
throw new IllegalStateException(e);
}
return params;
}
public String getPostBodyAsString() {
try {
if (cachedBytes == null) cacheInputStream();
return cachedBytes.toString(UTF8);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/* An inputStream which reads the cached request body */
public class CachedServletInputStream extends ServletInputStream {
private ByteArrayInputStream input;
public CachedServletInputStream() {
/* create a new input stream from the cached request body */
input = new ByteArrayInputStream(cachedBytes.toByteArray());
}
@Override
public int read() throws IOException {
return input.read();
}
}
@Override
public String toString() {
String query = dk.bnr.util.StringUtil.nullToEmpty(getQueryString());
StringBuilder sb = new StringBuilder();
sb.append("URL='").append(getRequestURI()).append(query.isEmpty() ? "" : "?" + query).append("', body='");
sb.append(getPostBodyAsString());
sb.append("'");
return sb.toString();
}
}
唯一の方法は、入力ストリーム全体をフィルタで自分で消費し、それから必要なものを取得し、読み取ったコンテンツの新しいInputStreamを作成し、そのInputStreamをServletRequestWrapper(またはHttpServletRequestWrapper)に入れることです。
欠点は、ペイロードを自分で解析する必要があることです。標準では、その機能を利用できません。
補遺-
先ほど言ったように、HttpServletRequestWrapperを調べる必要があります。
フィルターでは、FilterChain.doFilter(request、response)を呼び出して続行します。
単純なフィルターの場合、要求と応答はフィルターに渡されるものと同じです。そうである必要はありません。これらを独自の要求や応答に置き換えることができます。
HttpServletRequestWrapperは、これを容易にするために特別に設計されています。元の要求を渡すと、すべての呼び出しをインターセプトできます。これの独自のサブクラスを作成し、getInputStreamメソッドを独自のメソッドに置き換えます。元のリクエストの入力ストリームを変更することはできないため、代わりにこのラッパーを使用して独自の入力ストリームを返します。
最も単純なケースは、元の要求の入力ストリームをバイトバッファーに取り込み、必要な魔法を実行してから、そのバッファーから新しいByteArrayInputStreamを作成することです。これがラッパーで返され、FilterChain.doFilterメソッドに渡されます。
ServletInputStreamをサブクラス化し、ByteArrayInputStreamの別のラッパーを作成する必要がありますが、それも大したことではありません。
私も同じ問題を抱えており、以下のコードはよりシンプルで、私にとってはうまく機能していると思います
public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
private String _body;
public MultiReadHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
_body = "";
BufferedReader bufferedReader = request.getReader();
String line;
while ((line = bufferedReader.readLine()) != null){
_body += line;
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(_body.getBytes());
return new ServletInputStream() {
public int read() throws IOException {
return byteArrayInputStream.read();
}
};
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
フィルターJavaクラスで、
HttpServletRequest properRequest = ((HttpServletRequest) req);
MultiReadHttpServletRequest wrappedRequest = new MultiReadHttpServletRequest(properRequest);
req = wrappedRequest;
inputJson = IOUtils.toString(req.getReader());
System.out.println("body"+inputJson);
質問があれば教えてください
これは基本的にLathyの回答ですが、ServletInputStreamの新しい要件に合わせて更新されています。
つまり(ServletInputStreamの場合)、実装する必要があります:
public abstract boolean isFinished();
public abstract boolean isReady();
public abstract void setReadListener(ReadListener var1);
これは、編集されたLathyのオブジェクトです
import Java.io.BufferedReader;
import Java.io.IOException;
import Java.io.InputStreamReader;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class RequestWrapper extends HttpServletRequestWrapper {
private String _body;
public RequestWrapper(HttpServletRequest request) throws IOException {
super(request);
_body = "";
BufferedReader bufferedReader = request.getReader();
String line;
while ((line = bufferedReader.readLine()) != null){
_body += line;
}
}
@Override
public ServletInputStream getInputStream() throws IOException {
CustomServletInputStream kid = new CustomServletInputStream(_body.getBytes());
return kid;
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}
そしてどこか(??)でこれを見つけました(これは「余分な」メソッドを処理するファーストクラスです)。
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import Java.io.IOException;
import Java.io.UnsupportedEncodingException;
public class CustomServletInputStream extends ServletInputStream {
private byte[] myBytes;
private int lastIndexRetrieved = -1;
private ReadListener readListener = null;
public CustomServletInputStream(String s) {
try {
this.myBytes = s.getBytes("UTF-8");
} catch (UnsupportedEncodingException ex) {
throw new IllegalStateException("JVM did not support UTF-8", ex);
}
}
public CustomServletInputStream(byte[] inputBytes) {
this.myBytes = inputBytes;
}
@Override
public boolean isFinished() {
return (lastIndexRetrieved == myBytes.length - 1);
}
@Override
public boolean isReady() {
// This implementation will never block
// We also never need to call the readListener from this method, as this method will never return false
return isFinished();
}
@Override
public void setReadListener(ReadListener readListener) {
this.readListener = readListener;
if (!isFinished()) {
try {
readListener.onDataAvailable();
} catch (IOException e) {
readListener.onError(e);
}
} else {
try {
readListener.onAllDataRead();
} catch (IOException e) {
readListener.onError(e);
}
}
}
@Override
public int read() throws IOException {
int i;
if (!isFinished()) {
i = myBytes[lastIndexRetrieved + 1];
lastIndexRetrieved++;
if (isFinished() && (readListener != null)) {
try {
readListener.onAllDataRead();
} catch (IOException ex) {
readListener.onError(ex);
throw ex;
}
}
return i;
} else {
return -1;
}
}
};
最終的には、リクエストを記録しようとしていました。そして、上記のフランケンシュタインの作品は、私が以下を作成するのに役立ちました。
import Java.io.IOException;
import Java.io.UnsupportedEncodingException;
import Java.security.Principal;
import Java.util.Enumeration;
import Java.util.LinkedHashMap;
import Java.util.Map;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.Apache.commons.io.IOUtils;
//one or the other based on spring version
//import org.springframework.boot.autoconfigure.web.ErrorAttributes;
import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* A filter which logs web requests that lead to an error in the system.
*/
@Component
public class LogRequestFilter extends OncePerRequestFilter implements Ordered {
// I tried Apache.commons and slf4g loggers. (one or the other in these next 2 lines of declaration */
//private final static org.Apache.commons.logging.Log logger = org.Apache.commons.logging.LogFactory.getLog(LogRequestFilter.class);
private static final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(LogRequestFilter.class);
// put filter at the end of all other filters to make sure we are processing after all others
private int order = Ordered.LOWEST_PRECEDENCE - 8;
private ErrorAttributes errorAttributes;
@Override
public int getOrder() {
return order;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String temp = ""; /* for a breakpoint, remove for production/real code */
/* change to true for easy way to comment out this code, remove this if-check for production/real code */
if (false) {
filterChain.doFilter(request, response);
return;
}
/* make a "copy" to avoid issues with body-can-only-read-once issues */
RequestWrapper reqWrapper = new RequestWrapper(request);
int status = HttpStatus.INTERNAL_SERVER_ERROR.value();
// pass through filter chain to do the actual request handling
filterChain.doFilter(reqWrapper, response);
status = response.getStatus();
try {
Map<String, Object> traceMap = getTrace(reqWrapper, status);
// body can only be read after the actual request handling was done!
this.getBodyFromTheRequestCopy(reqWrapper, traceMap);
/* now do something with all the pieces of information gatherered */
this.logTrace(reqWrapper, traceMap);
} catch (Exception ex) {
logger.error("LogRequestFilter FAILED: " + ex.getMessage(), ex);
}
}
private void getBodyFromTheRequestCopy(RequestWrapper rw, Map<String, Object> trace) {
try {
if (rw != null) {
byte[] buf = IOUtils.toByteArray(rw.getInputStream());
//byte[] buf = rw.getInputStream();
if (buf.length > 0) {
String payloadSlimmed;
try {
String payload = new String(buf, 0, buf.length, rw.getCharacterEncoding());
payloadSlimmed = payload.trim().replaceAll(" +", " ");
} catch (UnsupportedEncodingException ex) {
payloadSlimmed = "[unknown]";
}
trace.put("body", payloadSlimmed);
}
}
} catch (IOException ioex) {
trace.put("body", "EXCEPTION: " + ioex.getMessage());
}
}
private void logTrace(HttpServletRequest request, Map<String, Object> trace) {
Object method = trace.get("method");
Object path = trace.get("path");
Object statusCode = trace.get("statusCode");
logger.info(String.format("%s %s produced an status code '%s'. Trace: '%s'", method, path, statusCode,
trace));
}
protected Map<String, Object> getTrace(HttpServletRequest request, int status) {
Throwable exception = (Throwable) request.getAttribute("javax.servlet.error.exception");
Principal principal = request.getUserPrincipal();
Map<String, Object> trace = new LinkedHashMap<String, Object>();
trace.put("method", request.getMethod());
trace.put("path", request.getRequestURI());
if (null != principal) {
trace.put("principal", principal.getName());
}
trace.put("query", request.getQueryString());
trace.put("statusCode", status);
Enumeration headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String key = (String) headerNames.nextElement();
String value = request.getHeader(key);
trace.put("header:" + key, value);
}
if (exception != null && this.errorAttributes != null) {
trace.put("error", this.errorAttributes
.getErrorAttributes((WebRequest) new ServletRequestAttributes(request), true));
}
return trace;
}
}
このコードを一粒ずつ取ってください。
最も重要な「テスト」は、POSTがペイロードで機能するかどうかです。これが「二重読み取り」の問題を明らかにするものです。
擬似サンプルコード
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("myroute")
public class MyController {
@RequestMapping(method = RequestMethod.POST, produces = "application/json")
@ResponseBody
public String getSomethingExample(@RequestBody MyCustomObject input) {
String returnValue = "";
return returnValue;
}
}
テストしたいだけなら、「MyCustomObject」をプレーンな「Object」に置き換えることができます。
この答えは、いくつかの異なるSOFの投稿と例からフランケンスタインされています。しかし、すべてをまとめるのに時間がかかりました。
私の前にLathyの答えに賛成してください。私はこれなしではここまで得られなかったでしょう。
以下は、この作業中に私が得た例外の1つまたはいくつかです。
このリクエストに対してgetReader()がすでに呼び出されています
私が「借りた」場所のいくつかがここにあるように見えます:
http://slackspace.de/articles/log-request-body-with-spring-boot/
https://howtodoinjava.com/servlets/httpservletrequestwrapper-example-read-request-body/
https://www.oodlestechnologies.com/blogs/How-to-create-duplicate-object-of-httpServletRequest-object
SpringはAbstractRequestLoggingFilter
でこれをサポートしています:
@Bean
public Filter loggingFilter(){
final AbstractRequestLoggingFilter filter = new AbstractRequestLoggingFilter() {
@Override
protected void beforeRequest(final HttpServletRequest request, final String message) {
}
@Override
protected void afterRequest(final HttpServletRequest request, final String message) {
}
};
filter.setIncludePayload(true);
filter.setIncludeQueryString(false);
filter.setMaxPayloadLength(1000000);
return filter;
}
残念ながら、リクエストからペイロードを直接読み取ることはできませんが、Stringメッセージパラメータにはペイロードが含まれるため、次のようにそこから取得できます。
String body = message.substring(message.indexOf("{"), message.lastIndexOf("]"));
私の場合、getInputStream()
の上書きだけではうまくいきませんでした。私のサーバー実装は、このメソッドを呼び出さずにパラメーターを解析するようです。他の方法は見つかりませんでしたが、4つすべてのgetParameter *メソッドも再実装しました。以下はgetParameterMap
のコードです(Apache HttpクライアントとGoogle Guavaライブラリが使用されています):
@Override
public Map<String, String[]> getParameterMap() {
Iterable<NameValuePair> params = URLEncodedUtils.parse(getQueryString(), NullUtils.UTF8);
try {
String cts = getContentType();
if (cts != null) {
ContentType ct = ContentType.parse(cts);
if (ct.getMimeType().equals(ContentType.APPLICATION_FORM_URLENCODED.getMimeType())) {
List<NameValuePair> postParams = URLEncodedUtils.parse(IOUtils.toString(getReader()), NullUtils.UTF8);
params = Iterables.concat(params, postParams);
}
}
} catch (IOException e) {
throw new IllegalStateException(e);
}
Map<String, String[]> result = toMap(params);
return result;
}
public static Map<String, String[]> toMap(Iterable<NameValuePair> body) {
Map<String, String[]> result = new LinkedHashMap<>();
for (NameValuePair e : body) {
String key = e.getName();
String value = e.getValue();
if (result.containsKey(key)) {
String[] newValue = ObjectArrays.concat(result.get(key), value);
result.remove(key);
result.put(key, newValue);
} else {
result.put(key, new String[] {value});
}
}
return result;
}
Spring AbstractRequestLoggingFilterを見て(または使用して)
要求を制御できる場合は、コンテンツタイプをbinary/octet-streamに設定できます。これにより、入力ストリームを消費せずにパラメーターを照会できます。
ただし、これは一部のアプリケーションサーバーに固有の場合があります。 Tomcatをテストしただけですが、jettyは https://stackoverflow.com/a/11434646/9571 に従って同じように動作するようです。