我使用retrofit
进行http调用gson
作为转换器。
在某些情况下,当gson尝试将响应转换为对象时,我会抛出异常,我想知道在这种情况下实际响应是什么。
例如:
这是我收到的异常消息:Expected a string but was BEGIN_OBJECT at line 1 column 26 path $[0].date
执行调用的代码如下:
Gson gson = gsonBuilder.create();
Retrofit retrofit = (new retrofit2.Retrofit.Builder()).baseUrl(baseUrl).addConverterFactory(GsonConverterFactory.create(gson)).client(httpClient).build();
MyService service = retrofit.create(clazz);
...
Response<T> response = service.call().execute();
当这段代码抛出异常时,我想以某种方式记录原始响应体。我怎样才能做到这一点?
答案 0 :(得分:1)
我认为不能轻易完成。改造似乎没有提供一种跟踪输入流的简单方法(我认为最自然的地方是CallAdapter.Factory
,但它不允许无效的响应跟踪。)
基本上,应该在特定的转换器中检测非法响应转换,该转换器的唯一责任是记录无效的有效负载。听起来很像装饰设计模式。既然Java(不像Kotlin?)不支持装饰者作为一等公民,转发实现可以像Google Guava Forwarding***
类一样实现:
@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
}
@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
}
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
}
简单转发实现只是覆盖其父类的所有方法,并将作业委托给委托对象。扩展转发类后,可以再次覆盖某些父方法。
这只是下面使用的听众。
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;
}
下一步是实现错误报告转换器工厂,该工厂可以注入Retrofit.Builder
并监听下游转换器中发生的任何错误。请注意它是如何工作的:
GsonConverter
失败 。这应该被认为是瓶颈,因为可能有大尺寸的增长缓冲区(但是,它可能是有限的),它的内部阵列在转换器请求时被复制等等。IOException
或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
}
}
}
请注意,此实现会大量使用转发类,并且只会覆盖必要的内容。
还有一些实用程序,如连接输入流和调整迭代器到枚举。
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();
}
}
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()));
}
}
琐碎的日志记录实施。
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}
在这种情况下,错误报告转换器将生成以下转储到stdout:
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个字节。这可能需要在中间转换器中对连接的流进行校准,以便将内容“向左移动”一点。