问题描述
先把有坑的代码贴上:
以上代码在最后一行会抛出如下异常:
但是我在开发和测试环境都验证过,fresp.body().asInputStream()返回的是ByteArrayInputStream类型,也就是说最后一行是支持reset操作的。
排查记录
查看feign源码发现
asInputStream()方法有两个实现,分别是ByteArrayBody和InputStreamBody,源码如下:1234567891011// ByteArrayBody实现public InputStream asInputStream() throws IOException {return new ByteArrayInputStream(data);}// InputStreamBody实现public InputStream asInputStream() throws IOException {return inputStream;}可以看出只有调用
InputStreamBody实现的asInputStream()方法才会出现上面的异常。然后在本地发起请求,开启debug模式,一步一步跟发现没有问题,每次都是走到
ByteArrayBody,这让我郁闷了很长时间啊。然后尝试跟踪
ByteArrayInputStream实例中传递的data的初始化流程。发现是在ByteArrayBody类的构造函数中初始化的。1234// 构造函数public ByteArrayBody(byte[] data) {this.data = data;}再跟踪构造函数,发现在
orNull()方法中被调用了1234567// orNull 方法private static Body orNull(byte[] data) {if (data == null) {return null;}return new ByteArrayBody(data);}再跟踪
orNull()方法,发现在Response.Builder类中的被调用,调用方法如下:1234public Builder body(byte[] data) {this.body = ByteArrayBody.orNull(data);return this;}再跟踪
body()方法,发现有两个地方会调用,Logger和SynchronousMethodHandler两个类中会调用。然后先在
SynchronousMethodHandler的方法中debug,代码片段如下:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283public Object invoke(Object[] argv) throws Throwable {RequestTemplate template = buildTemplateFromArgs.create(argv);Retryer retryer = this.retryer.clone();while (true) {try {return executeAndDecode(template);} catch (RetryableException e) {retryer.continueOrPropagate(e);if (logLevel != Logger.Level.NONE) {logger.logRetry(metadata.configKey(), logLevel);}continue;}}}Object executeAndDecode(RequestTemplate template) throws Throwable {Request request = targetRequest(template);if (logLevel != Logger.Level.NONE) {logger.logRequest(metadata.configKey(), logLevel, request);}Response response;long start = System.nanoTime();try {response = client.execute(request, options);// ensure the request is set. TODO: remove in Feign 10response.toBuilder().request(request).build();} catch (IOException e) {if (logLevel != Logger.Level.NONE) {logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));}throw errorExecuting(request, e);}long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);boolean shouldClose = true;try {if (logLevel != Logger.Level.NONE) {response =logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);// ensure the request is set. TODO: remove in Feign 10response.toBuilder().request(request).build();}if (Response.class == metadata.returnType()) {if (response.body() == null) {return response;}if (response.body().length() == null ||response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {shouldClose = false;return response;}// Ensure the response body is disconnectedbyte[] bodyData = Util.toByteArray(response.body().asInputStream());// 此处就是调用body方法的地方,断点打到这里,在实际debug过程中发现并没有走到这里return response.toBuilder().body(bodyData).build();}if (response.status() >= 200 && response.status() < 300) {if (void.class == metadata.returnType()) {return null;} else {return decode(response);}} else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {return decode(response);} else {throw errorDecoder.decode(metadata.configKey(), response);}} catch (IOException e) {if (logLevel != Logger.Level.NONE) {logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);}throw errorReading(request, response, e);} finally {if (shouldClose) {ensureClosed(response.body());}}}然后在
Logger的方法中debug,代码片段如下:123456789101112131415161718192021222324252627282930313233343536protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response,long elapsedTime) throws IOException {String reason = response.reason() != null && logLevel.compareTo(Level.NONE) > 0 ?" " + response.reason() : "";int status = response.status();log(configKey, "<--- HTTP/1.1 %s%s (%sms)", status, reason, elapsedTime);if (logLevel.ordinal() >= Level.HEADERS.ordinal()) {for (String field : response.headers().keySet()) {for (String value : valuesOrEmpty(response.headers(), field)) {log(configKey, "%s: %s", field, value);}}int bodyLength = 0;if (response.body() != null && !(status == 204 || status == 205)) {// HTTP 204 No Content "...response MUST NOT include a message-body"// HTTP 205 Reset Content "...response MUST NOT include an entity"if (logLevel.ordinal() >= Level.FULL.ordinal()) {log(configKey, ""); // CRLF}byte[] bodyData = Util.toByteArray(response.body().asInputStream());bodyLength = bodyData.length;if (logLevel.ordinal() >= Level.FULL.ordinal() && bodyLength > 0) {log(configKey, "%s", decodeOrDefault(bodyData, UTF_8, "Binary data"));}log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);// 此处会调用body方法,并且我在本地调试的时候确实会走到此处return response.toBuilder().body(bodyData).build();} else {log(configKey, "<--- END HTTP (%s-byte body)", bodyLength);}}return response;}最后发现,原来我之前在开发和测试环境一直配置的日志打印级别都是debug,feign中的日志只有配置了debug级别才会输出,上面的代码其实就是当判断需要输出日志时,会先把输入流转成byte array缓存起来,并且会封装成ByteArrayBody方便后续复用该流。生产上面配置的日志级别是info,所以会封装成InputStreamBody,也就导致后续获取的输入流是不支持mark的。
解决方案
|
|