我认为这并不容易完成。 Retrofit 似乎没有提供一种跟踪输入流的简单方法(我想到的最自然的地方是CallAdapter.Factory
但它不允许无效响应跟踪)。
基本上,非法响应转换应该在特定的转换器中检测到,该转换器的唯一责任是记录无效的有效负载。听起来很像装饰器设计模式。由于Java(不像Kotlin?)不支持装饰器作为一等公民,转发实现可以类似于Google Guava来实现Forwarding***
课程:
ForwardingInputStream.java
@SuppressWarnings("resource")
abstract class ForwardingInputStream
extends InputStream {
protected abstract InputStream inputStream();
// @formatter:off
@Override public int read() throws IOException { return inputStream().read(); }
// @formatter:on
// @formatter:off
@Override public int read(final byte[] b) throws IOException { return inputStream().read(b); }
@Override public int read(final byte[] b, final int off, final int len) throws IOException { return inputStream().read(b, off, len); }
@Override public long skip(final long n) throws IOException { return inputStream().skip(n); }
@Override public int available() throws IOException { return inputStream().available(); }
@Override public void close() throws IOException { inputStream().close(); }
@Override public void mark(final int readlimit) { inputStream().mark(readlimit); }
@Override public void reset() throws IOException { inputStream().reset(); }
@Override public boolean markSupported() { return inputStream().markSupported(); }
// @formatter:on
}
ForwardingResponseBody.java
@SuppressWarnings("resource")
abstract class ForwardingResponseBody
extends ResponseBody {
protected abstract ResponseBody responseBody();
// @formatter:off
@Override public MediaType contentType() { return responseBody().contentType(); }
@Override public long contentLength() { return responseBody().contentLength(); }
@Override public BufferedSource source() { return responseBody().source(); }
// @formatter:on
// @formatter:off
@Override public void close() { super.close(); }
// @formatter:on
}
ForwardingBufferedSource.java
abstract class ForwardingBufferedSource
implements BufferedSource {
protected abstract BufferedSource bufferedSource();
// @formatter:off
@Override public Buffer buffer() { return bufferedSource().buffer(); }
@Override public boolean exhausted() throws IOException { return bufferedSource().exhausted(); }
@Override public void require(final long byteCount) throws IOException { bufferedSource().require(byteCount); }
@Override public boolean request(final long byteCount) throws IOException { return bufferedSource().request(byteCount); }
@Override public byte readByte() throws IOException { return bufferedSource().readByte(); }
@Override public short readShort() throws IOException { return bufferedSource().readShort(); }
@Override public short readShortLe() throws IOException { return bufferedSource().readShortLe(); }
@Override public int readInt() throws IOException { return bufferedSource().readInt(); }
@Override public int readIntLe() throws IOException { return bufferedSource().readIntLe(); }
@Override public long readLong() throws IOException { return bufferedSource().readLong(); }
@Override public long readLongLe() throws IOException { return bufferedSource().readLongLe(); }
@Override public long readDecimalLong() throws IOException { return bufferedSource().readDecimalLong(); }
@Override public long readHexadecimalUnsignedLong() throws IOException { return bufferedSource().readHexadecimalUnsignedLong(); }
@Override public void skip(final long byteCount) throws IOException { bufferedSource().skip(byteCount); }
@Override public ByteString readByteString() throws IOException { return bufferedSource().readByteString(); }
@Override public ByteString readByteString(final long byteCount) throws IOException { return bufferedSource().readByteString(byteCount); }
@Override public int select(final Options options) throws IOException { return bufferedSource().select(options); }
@Override public byte[] readByteArray() throws IOException { return bufferedSource().readByteArray(); }
@Override public byte[] readByteArray(final long byteCount) throws IOException { return bufferedSource().readByteArray(byteCount); }
@Override public int read(final byte[] sink) throws IOException { return bufferedSource().read(sink); }
@Override public void readFully(final byte[] sink) throws IOException { bufferedSource().readFully(sink); }
@Override public int read(final byte[] sink, final int offset, final int byteCount) throws IOException { return bufferedSource().read(sink, offset, byteCount); }
@Override public void readFully(final Buffer sink, final long byteCount) throws IOException { bufferedSource().readFully(sink, byteCount); }
@Override public long readAll(final Sink sink) throws IOException { return bufferedSource().readAll(sink); }
@Override public String readUtf8() throws IOException { return bufferedSource().readUtf8(); }
@Override public String readUtf8(final long byteCount) throws IOException { return bufferedSource().readUtf8(byteCount); }
@Override public String readUtf8Line() throws IOException { return bufferedSource().readUtf8Line(); }
@Override public String readUtf8LineStrict() throws IOException { return bufferedSource().readUtf8LineStrict(); }
@Override public int readUtf8CodePoint() throws IOException { return bufferedSource().readUtf8CodePoint(); }
@Override public String readString(final Charset charset) throws IOException { return bufferedSource().readString(charset); }
@Override public String readString(final long byteCount, final Charset charset) throws IOException { return bufferedSource().readString(byteCount, charset); }
@Override public long indexOf(final byte b) throws IOException { return bufferedSource().indexOf(b); }
@Override public long indexOf(final byte b, final long fromIndex) throws IOException { return bufferedSource().indexOf(b, fromIndex); }
@Override public long indexOf(final ByteString bytes) throws IOException { return bufferedSource().indexOf(bytes); }
@Override public long indexOf(final ByteString bytes, final long fromIndex) throws IOException { return bufferedSource().indexOf(bytes, fromIndex); }
@Override public long indexOfElement(final ByteString targetBytes) throws IOException { return bufferedSource().indexOfElement(targetBytes); }
@Override public long indexOfElement(final ByteString targetBytes, final long fromIndex) throws IOException { return bufferedSource().indexOfElement(targetBytes, fromIndex); }
@Override public InputStream inputStream() { return bufferedSource().inputStream(); }
@Override public long read(final Buffer sink, final long byteCount) throws IOException { return bufferedSource().read(sink, byteCount); }
@Override public Timeout timeout() { return bufferedSource().timeout(); }
@Override public void close() throws IOException { bufferedSource().close(); }
// @formatter:on
}
简单的转发实现只是重写其父类的所有方法并将作业委托给委托对象。一旦扩展了转发类,一些父方法就可以再次被重写。
IConversionThrowableConsumer.java
这只是下面使用的监听器。
interface IConversionThrowableConsumer {
/**
* Instantiating {@link okhttp3.ResponseBody} can be not easy due to the way of how {@link okio.BufferedSource} is designed -- too heavy.
* Deconstructing its components to "atoms" with some lack of functionality may be acceptable.
* However, this consumer may need some improvements on demand.
*/
void accept(MediaType contentType, long contentLength, InputStream inputStream, Throwable ex)
throws IOException;
}
ErrorReportingConverterFactory.java
下一步是实现可以注入的错误报告转换器工厂Retrofit.Builder
并监听下游转换器中发生的任何错误。注意它是如何工作的:
- 对于每个响应转换器,都会注入一个中间转换器。它允许侦听下游转换器中的任何错误。
- 下游转换器获得不可关闭的资源以避免过早关闭底层 I/O 资源...
- 下游转换器进行转换,而中间转换器将实际输入流内容收集到缓冲区中,以便用输入流进行响应that may cause
GsonConverter
fail。这应该被认为是一个瓶颈,因为增长的缓冲区可能会很大(但是,它可能是有限的),当转换器请求时会复制其内部数组等等。
- If
IOException
or RuntimeException
发生这种情况时,中间转换器将缓冲的输入流内容和实际输入流连接起来,以便让消费者从一开始就接受输入流。
- 中间转换器负责关闭资源本身。
final class ErrorReportingConverterFactory
extends Factory {
private final IConversionThrowableConsumer consumer;
private ErrorReportingConverterFactory(final IConversionThrowableConsumer consumer) {
this.consumer = consumer;
}
static Factory getErrorReportingConverterFactory(final IConversionThrowableConsumer listener) {
return new ErrorReportingConverterFactory(listener);
}
@Override
public Converter<ResponseBody, ?> responseBodyConverter(final Type type, final Annotation[] annotations, final Retrofit retrofit) {
return (Converter<ResponseBody, Object>) responseBody -> {
final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
final InputStream realInputStream = responseBody.byteStream();
try {
final ForwardingResponseBody bufferingResponseBody = new BufferingNoCloseResponseBOdy(responseBody, byteArrayOutputStream);
final Converter<ResponseBody, Object> converter = retrofit.nextResponseBodyConverter(this, type, annotations);
return converter.convert(bufferingResponseBody);
} catch ( final RuntimeException | IOException ex ) {
final InputStream inputStream = concatInputStreams(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()), realInputStream);
consumer.accept(responseBody.contentType(), responseBody.contentLength(), inputStream, ex);
throw ex;
} finally {
responseBody.close();
}
};
}
private static class BufferingInputStream
extends ForwardingInputStream {
private final InputStream inputStream;
private final ByteArrayOutputStream byteArrayOutputStream;
private BufferingInputStream(final InputStream inputStream, final ByteArrayOutputStream byteArrayOutputStream) {
this.inputStream = inputStream;
this.byteArrayOutputStream = byteArrayOutputStream;
}
@Override
protected InputStream inputStream() {
return inputStream;
}
@Override
public int read()
throws IOException {
final int read = super.read();
if ( read != -1 ) {
byteArrayOutputStream.write(read);
}
return read;
}
@Override
public int read(final byte[] b)
throws IOException {
final int read = super.read(b);
if ( read != -1 ) {
byteArrayOutputStream.write(b, 0, read);
}
return read;
}
@Override
public int read(final byte[] b, final int off, final int len)
throws IOException {
final int read = super.read(b, off, len);
if ( read != -1 ) {
byteArrayOutputStream.write(b, off, read);
}
return read;
}
}
private static class BufferingNoCloseResponseBOdy
extends ForwardingResponseBody {
private final ResponseBody responseBody;
private final ByteArrayOutputStream byteArrayOutputStream;
private BufferingNoCloseResponseBOdy(final ResponseBody responseBody, final ByteArrayOutputStream byteArrayOutputStream) {
this.responseBody = responseBody;
this.byteArrayOutputStream = byteArrayOutputStream;
}
@Override
protected ResponseBody responseBody() {
return responseBody;
}
@Override
@SuppressWarnings("resource")
public BufferedSource source() {
final BufferedSource source = super.source();
return new ForwardingBufferedSource() {
@Override
protected BufferedSource bufferedSource() {
return source;
}
@Override
public InputStream inputStream() {
return new BufferingInputStream(super.inputStream(), byteArrayOutputStream);
}
};
}
/**
* Suppressing close due to automatic close in {@link ErrorReportingConverterFactory#responseBodyConverter(Type, Annotation[], Retrofit)}
*/
@Override
public void close() {
// do nothing
}
}
}
请注意,此实现大量使用转发类,并且仅覆盖必要的内容。
还有一些实用程序,例如连接输入流和使迭代器适应枚举。
迭代器枚举.java
final class IteratorEnumeration<T>
implements Enumeration<T> {
private final Iterator<? extends T> iterator;
private IteratorEnumeration(final Iterator<? extends T> iterator) {
this.iterator = iterator;
}
static <T> Enumeration<T> iteratorEnumeration(final Iterator<? extends T> iterator) {
return new IteratorEnumeration<>(iterator);
}
@Override
public boolean hasMoreElements() {
return iterator.hasNext();
}
@Override
public T nextElement() {
return iterator.next();
}
}
输入流.java
final class InputStreams {
private InputStreams() {
}
static InputStream concatInputStreams(final InputStream... inputStreams) {
return inputStreams.length == 2
? new SequenceInputStream(inputStreams[0], inputStreams[1])
: new SequenceInputStream(iteratorEnumeration((Iterator<? extends InputStream>) asList(inputStreams).iterator()));
}
}
OutputStreamConversionThrowableConsumer.java
简单的日志记录实现。
final class OutputStreamConversionThrowableConsumer
implements IConversionThrowableConsumer {
private static final int BUFFER_SIZE = 512;
private final PrintStream printStream;
private OutputStreamConversionThrowableConsumer(final PrintStream printStream) {
this.printStream = printStream;
}
static IConversionThrowableConsumer getOutputStreamConversionThrowableConsumer(final OutputStream outputStream) {
return new OutputStreamConversionThrowableConsumer(new PrintStream(outputStream));
}
static IConversionThrowableConsumer getSystemOutConversionThrowableConsumer() {
return getOutputStreamConversionThrowableConsumer(System.out);
}
static IConversionThrowableConsumer getSystemErrConversionThrowableConsumer() {
return getOutputStreamConversionThrowableConsumer(System.err);
}
@Override
public void accept(final MediaType contentType, final long contentLength, final InputStream inputStream, final Throwable ex)
throws IOException {
printStream.print("Content type: ");
printStream.println(contentType);
printStream.print("Content length: ");
printStream.println(contentLength);
printStream.print("Content: ");
final byte[] buffer = new byte[BUFFER_SIZE];
int read;
while ( (read = inputStream.read(buffer)) != -1 ) {
printStream.write(buffer, 0, read);
}
printStream.println();
}
}
把所有的放在一起
final Gson gson = new Gson();
final Retrofit retrofit = new Retrofit.Builder()
.baseUrl(...)
.addConverterFactory(getErrorReportingConverterFactory(getSystemOutConversionThrowableConsumer()))
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
final IWhateverService service = retrofit.create(IWhateverService.class);
final Call<...> call = service.getWhatever("test.json");
call.enqueue(new Callback<...>() {
@Override
public void onResponse(final Call<...> call, final Response<...> response) {
System.out.println(response.body());
}
@Override
public void onFailure(final Call<...> call, final Throwable throwable) {
throwable.printStackTrace(System.err);
}
});
注意ErrorReportingConverterFactory
必须在之前注册GsonConverterFactory
。假设对 JSON 的服务请求最终是非法的:
{"foo":1,###"bar":2}
在这种情况下,错误报告转换器将生成以下转储到标准输出:
Content type: application/json
Content length: -1
Content: {"foo":1,###"bar":2}
我不是 Log4j 方面的专家,并且无法找到一种有效的方法来获取输出流以将输入流重定向到。这是我发现的最接近的东西:
final class Log4jConversionThrowableConsumer
implements IConversionThrowableConsumer {
private static final int BUFFER_SIZE = 512;
private final Logger logger;
private Log4jConversionThrowableConsumer(final Logger logger) {
this.logger = logger;
}
static IConversionThrowableConsumer getLog4jConversionThrowableConsumer(final Logger logger) {
return new Log4jConversionThrowableConsumer(logger);
}
@Override
public void accept(final MediaType contentType, final long contentLength, final InputStream inputStream, final Throwable ex) {
try {
final StringBuilder builder = new StringBuilder(BUFFER_SIZE)
.append("Content type=")
.append(contentType)
.append("; Content length=")
.append(contentLength)
.append("; Input stream content=");
readInputStreamFirstChunk(builder, inputStream);
logger.error(builder.toString(), ex);
} catch ( final IOException ioex ) {
throw new RuntimeException(ioex);
}
}
private static void readInputStreamFirstChunk(final StringBuilder builder, final InputStream inputStream)
throws IOException {
final Reader reader = new InputStreamReader(inputStream);
final char[] buffer = new char[512];
final int read = reader.read(buffer);
if ( read >= 0 ) {
builder.append(buffer, 0, read);
}
}
}
不幸的是,收集整个字符串的成本可能很高,因此只需要前 512 个字节。这可能需要校准中间转换器中的加入流,以便将内容“向左”“移动”一点。