【问题标题】:Log response body in case of exception在异常情况下记录响应正文
【发布时间】:2017-07-01 04:48:45
【问题描述】:

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

当这段代码抛出异常时,我想以某种方式记录原始响应正文。我该怎么做?

【问题讨论】:

    标签: java gson retrofit2


    【解决方案1】:

    我认为这不是一件容易的事。 Retrofit 似乎没有提供跟踪输入流的简单方法(我想到的最自然的地方是CallAdapter.Factory,但它不允许无效响应跟踪)。

    基本上,应在特定转换器中检测非法响应转换,该转换器的唯一职责是记录无效负载。听起来很像装饰器设计模式。由于 Java(不像 Kotlin?)不支持将装饰器作为一等公民,因此可以像 Google Guava Forwarding*** classes 一样实现转发实现:

    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}
    

    在这种情况下,错误报告转换器将生成以下转储到标准输出:

    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 个字节。这可能需要在中间转换器中调用连接的流,以便将内容“向左”“移动”一点。

    【讨论】:

    • 哇,感谢您的详细回复!你手头有这样的工具吗;-)?
    • @oshai 不,除了IteratorEnumerationconcatInputStreams,我什么都没有——只是一些空闲时间。 :)
    • 谢谢!看起来不错,马上去试试。
    • @oshai 不客气。但是,当我实施它时,我想到内部缓冲区可能对您来说是一个真正的瓶颈。您可能想要拒绝记录此类响应的想法,或者仅在调试模式下转储此类响应。真正和最大的问题是,即使是合法的 JSON,您也必须缓冲所有输入流,因为在解析完成之前没人知道它是合法的还是非法的。或者您可以使内部缓冲区更小以收集最后 n 个字节,以免消耗太多内存。
    • 是的,谢谢,这也是我最初想到的问题。一个这样的:在类OutputStreamConversionThrowableConsumer - out 参数未找到。应该是输出流吗?
    猜你喜欢
    • 2020-08-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-02-12
    • 2014-08-22
    • 2015-11-11
    • 2021-04-12
    • 1970-01-01
    相关资源
    最近更新 更多