使用feign上传下载文件扩展包填坑记

问题描述

先把有坑的代码贴上:

1
2
3
4
5
6
7
8
9
10
//...上下文我就省略了,把有问题的代码段贴出来
InputStream inputStream = null;
inputStream = fresp.body().asInputStream();
FileTypeJudger.FileInfo fileInfo = fileTypeJudger.judgeToFileInfo(inputStream);
mimeType = fileInfo.getMimeType();
// 必须将游标设置到起始位置,才能输出完整的文件流
inputStream.reset(); //这里就是有坑的地方
//...

以上代码在最后一行会抛出如下异常:

1
java.io.IOException: mark/reset not supported

但是我在开发和测试环境都验证过,fresp.body().asInputStream()返回的是ByteArrayInputStream类型,也就是说最后一行是支持reset操作的。

排查记录

  1. 查看feign源码发现asInputStream()方法有两个实现,分别是ByteArrayBodyInputStreamBody,源码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // ByteArrayBody实现
    @Override
    public InputStream asInputStream() throws IOException {
    return new ByteArrayInputStream(data);
    }
    // InputStreamBody实现
    @Override
    public InputStream asInputStream() throws IOException {
    return inputStream;
    }

    可以看出只有调用InputStreamBody实现的asInputStream()方法才会出现上面的异常。

  2. 然后在本地发起请求,开启debug模式,一步一步跟发现没有问题,每次都是走到ByteArrayBody,这让我郁闷了很长时间啊。

  3. 然后尝试跟踪ByteArrayInputStream实例中传递的data的初始化流程。发现是在ByteArrayBody类的构造函数中初始化的。

    1
    2
    3
    4
    // 构造函数
    public ByteArrayBody(byte[] data) {
    this.data = data;
    }

    再跟踪构造函数,发现在orNull()方法中被调用了

    1
    2
    3
    4
    5
    6
    7
    // orNull 方法
    private static Body orNull(byte[] data) {
    if (data == null) {
    return null;
    }
    return new ByteArrayBody(data);
    }

    再跟踪orNull()方法,发现在Response.Builder类中的被调用,调用方法如下:

    1
    2
    3
    4
    public Builder body(byte[] data) {
    this.body = ByteArrayBody.orNull(data);
    return this;
    }

    再跟踪body()方法,发现有两个地方会调用,LoggerSynchronousMethodHandler两个类中会调用。

  4. 然后先在SynchronousMethodHandler的方法中debug,代码片段如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    @Override
    public 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 10
    response.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 10
    response.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 disconnected
    byte[] 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());
    }
    }
    }
  5. 然后在Logger的方法中debug,代码片段如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    protected 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;
    }
  6. 最后发现,原来我之前在开发和测试环境一直配置的日志打印级别都是debug,feign中的日志只有配置了debug级别才会输出,上面的代码其实就是当判断需要输出日志时,会先把输入流转成byte array缓存起来,并且会封装成ByteArrayBody方便后续复用该流。生产上面配置的日志级别是info,所以会封装成InputStreamBody,也就导致后续获取的输入流是不支持mark的。

解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//...上下文我就省略了,把有问题的代码段贴出来
InputStream inputStream = null;
inputStream = fresp.body().asInputStream();
Util.checkNotNull(inputStream, "inputstream from edfs is null");
// 校验流是否支持标记
if (!inputStream.markSupported()) {
inputStream = new ByteArrayInputStream(Util.toByteArray(inputStream));
}
FileTypeJudger.FileInfo fileInfo = fileTypeJudger.judgeToFileInfo(inputStream);
mimeType = fileInfo.getMimeType();
// 必须将游标设置到起始位置,才能输出完整的文件流
inputStream.reset();
//...
小伙伴,如果您觉得文章还不错,欢迎您的支持,我会继续努力创作!