读取 JAX-RS body InputStream 两次
Read JAX-RS body InputStream twice
我有一个 JAX-RS 日志过滤器来记录请求和响应的详细信息,如下所示:
public class LoggingFilter implements ContainerRequestFilter, ContainerResponseFilter {
@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
...
String body = getBody(request);
...
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("request: {}", httpRequest);
}
}
}
getBody()
方法从 InputStream
读取主体内容,但我需要做一些技巧,因为我无法重置此流。如果没有这个小技巧,我的休息方法总是收到空的请求正文内容:
private String getBody(final ContainerRequestContext requestContext) {
try {
byte[] body = IOUtils.toByteArray(requestContext.getEntityStream());
InputStream stream = new ByteArrayInputStream(body);
requestContext.setEntityStream(stream);
return new String(body);
} catch (IOException e) {
return null;
}
}
有没有更好的阅读正文内容的方法?
EDIT 这是一个改进的版本,看起来更健壮并使用 JDK 类。只需在重用前调用 close()
。
public class CachingInputStream extends BufferedInputStream {
public CachingInputStream(InputStream source) {
super(new PostCloseProtection(source));
super.mark(Integer.MAX_VALUE);
}
@Override
public synchronized void close() throws IOException {
if (!((PostCloseProtection) in).decoratedClosed) {
in.close();
}
super.reset();
}
private static class PostCloseProtection extends InputStream {
private volatile boolean decoratedClosed = false;
private final InputStream source;
public PostCloseProtection(InputStream source) {
this.source = source;
}
@Override
public int read() throws IOException {
return decoratedClosed ? -1 : source.read();
}
@Override
public int read(byte[] b) throws IOException {
return decoratedClosed ? -1 : source.read(b);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return decoratedClosed ? -1 : source.read(b, off, len);
}
@Override
public long skip(long n) throws IOException {
return decoratedClosed ? 0 : source.skip(n);
}
@Override
public int available() throws IOException {
return source.available();
}
@Override
public void close() throws IOException {
decoratedClosed = true;
source.close();
}
@Override
public void mark(int readLimit) {
source.mark(readLimit);
}
@Override
public void reset() throws IOException {
source.reset();
}
@Override
public boolean markSupported() {
return source.markSupported();
}
}
}
这允许通过将 mark
调整为 Integer.MAXVALUE
来读取缓冲区中的整个流。这也确保源在第一次关闭时正确关闭以释放 OS 资源。
旧答案
因为您无法确定 InputStream
支持标记 (markSupported()
) 的实际执行情况。您最好首先缓存输入流本身。
例如 ContainerRequestFilter
:
@Component
@Provider
@PreMatching
@Priority(1)
public class ReadSomethingInPayloadFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext request) throws IOException {
CachingInputStream entityStream = new CachingInputStream(request.getEntityStream());
readPayload(entityStream);
request.setEntityStream(entityStream.getCachedInputStream());
}
}
缓存输入流是一种简单的输入流缓存方法,它与您的方法很相似:
class CachingInputStream extends InputStream {
public static final int END_STREAM = -1;
private final InputStream is;
private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
public CachingInputStream(InputStream is) {
this.is = is;
}
public InputStream getCachedInputStream() {
return new ByteArrayInputStream(baos.toByteArray());
}
@Override
public int read() throws IOException {
int result = is.read();
// Avoid rewriting the end char (-1) otherwise it will be considered as a real char.
if (result != END_STREAM)
baos.write(result);
return result;
}
@Override
public int available() throws IOException {
return is.available();
}
@Override
public void close() throws IOException {
is.close();
}
}
这个实现在很多方面都很幼稚,它可以在以下方面以及可能更多方面得到改进:
- 在原始流上检查
markSupported
- 不要使用堆来存储缓存的输入流,这样可以避免对GC造成压力
- 缓存目前是无限制的,这可能是一个很好的改进,至少使用与您的 http 服务器相同的限制。
我有一个 JAX-RS 日志过滤器来记录请求和响应的详细信息,如下所示:
public class LoggingFilter implements ContainerRequestFilter, ContainerResponseFilter {
@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
...
String body = getBody(request);
...
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("request: {}", httpRequest);
}
}
}
getBody()
方法从 InputStream
读取主体内容,但我需要做一些技巧,因为我无法重置此流。如果没有这个小技巧,我的休息方法总是收到空的请求正文内容:
private String getBody(final ContainerRequestContext requestContext) {
try {
byte[] body = IOUtils.toByteArray(requestContext.getEntityStream());
InputStream stream = new ByteArrayInputStream(body);
requestContext.setEntityStream(stream);
return new String(body);
} catch (IOException e) {
return null;
}
}
有没有更好的阅读正文内容的方法?
EDIT 这是一个改进的版本,看起来更健壮并使用 JDK 类。只需在重用前调用 close()
。
public class CachingInputStream extends BufferedInputStream {
public CachingInputStream(InputStream source) {
super(new PostCloseProtection(source));
super.mark(Integer.MAX_VALUE);
}
@Override
public synchronized void close() throws IOException {
if (!((PostCloseProtection) in).decoratedClosed) {
in.close();
}
super.reset();
}
private static class PostCloseProtection extends InputStream {
private volatile boolean decoratedClosed = false;
private final InputStream source;
public PostCloseProtection(InputStream source) {
this.source = source;
}
@Override
public int read() throws IOException {
return decoratedClosed ? -1 : source.read();
}
@Override
public int read(byte[] b) throws IOException {
return decoratedClosed ? -1 : source.read(b);
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
return decoratedClosed ? -1 : source.read(b, off, len);
}
@Override
public long skip(long n) throws IOException {
return decoratedClosed ? 0 : source.skip(n);
}
@Override
public int available() throws IOException {
return source.available();
}
@Override
public void close() throws IOException {
decoratedClosed = true;
source.close();
}
@Override
public void mark(int readLimit) {
source.mark(readLimit);
}
@Override
public void reset() throws IOException {
source.reset();
}
@Override
public boolean markSupported() {
return source.markSupported();
}
}
}
这允许通过将 mark
调整为 Integer.MAXVALUE
来读取缓冲区中的整个流。这也确保源在第一次关闭时正确关闭以释放 OS 资源。
旧答案
因为您无法确定 InputStream
支持标记 (markSupported()
) 的实际执行情况。您最好首先缓存输入流本身。
例如 ContainerRequestFilter
:
@Component
@Provider
@PreMatching
@Priority(1)
public class ReadSomethingInPayloadFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext request) throws IOException {
CachingInputStream entityStream = new CachingInputStream(request.getEntityStream());
readPayload(entityStream);
request.setEntityStream(entityStream.getCachedInputStream());
}
}
缓存输入流是一种简单的输入流缓存方法,它与您的方法很相似:
class CachingInputStream extends InputStream {
public static final int END_STREAM = -1;
private final InputStream is;
private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
public CachingInputStream(InputStream is) {
this.is = is;
}
public InputStream getCachedInputStream() {
return new ByteArrayInputStream(baos.toByteArray());
}
@Override
public int read() throws IOException {
int result = is.read();
// Avoid rewriting the end char (-1) otherwise it will be considered as a real char.
if (result != END_STREAM)
baos.write(result);
return result;
}
@Override
public int available() throws IOException {
return is.available();
}
@Override
public void close() throws IOException {
is.close();
}
}
这个实现在很多方面都很幼稚,它可以在以下方面以及可能更多方面得到改进:
- 在原始流上检查
markSupported
- 不要使用堆来存储缓存的输入流,这样可以避免对GC造成压力
- 缓存目前是无限制的,这可能是一个很好的改进,至少使用与您的 http 服务器相同的限制。