在异常情况下记录响应主体

时间:2017-02-13 08:54:33

标签: gson retrofit2

我使用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();

当这段代码抛出异常时,我想以某种方式记录原始响应体。我怎样才能做到这一点?

1 个答案:

答案 0 :(得分:1)

我认为不能轻易完成。改造似乎没有提供一种跟踪输入流的简单方法(我认为最自然的地方是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资源...
  • 下游转换器进行转换,同时中间转换器将实际输入流内容收集到缓冲区中,以响应输入流 可能导致GsonConverter失败 。这应该被认为是瓶颈,因为可能有大尺寸的增长缓冲区(但是,它可能是有限的),它的内部阵列在转换器请求时被复制等等。
  • 如果出现IOExceptionRuntimeException,则中间转换器将缓冲的输入流内容与实际输入流连接起来,以便让消费者从一开始就接受输入流。
  • 中间转换器负责关闭资源本身。
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
        }

    }

}

请注意,此实现会大量使用转发类,并且只会覆盖必要的内容。

还有一些实用程序,如连接输入流和调整迭代器到枚举。

IteratorEnumeration.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();
    }

}

InputStreams.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}

在这种情况下,错误报告转换器将生成以下转储到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个字节。这可能需要在中间转换器中对连接的流进行校准,以便将内容“向左移动”一点。