You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Quarkus使用StreamingOutput返回文件附件报错,求正确实现方式

问题:Quarkus中返回S3文件下载时出现Jackson序列化错误

尝试将S3文件作为响应附件返回供用户下载时,遇到Jackson序列化错误:

com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class com.fanthreesixty.audience.resource.S3ClientResource$$Lambda$2984/0x0000000801f44000 and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)

当前实现代码:

public Response downloadFile(final String bucketName, final String objectKey, final String fileExtension) {
        try {

            CompletableFuture<ResponseInputStream<GetObjectResponse>> response = s3.getObject(GetObjectRequest.builder()
                    .bucket(bucketName)
                    .key(objectKey)
                    .build(), AsyncResponseTransformer.toBlockingInputStream());

            final String fileName = objectKey.contains("/") ? objectKey.split("/")[1] : objectKey;

//            StreamingOutput stream = outputStream -> {
//                try (InputStream is = response.join()) {
//                    byte[] buffer = new byte[4096];
//                    int bytesRead;
//                    while ((bytesRead = is.read(buffer)) != -1) {
//                        outputStream.write(buffer, 0, bytesRead);
//                    }
//                }
//            };

            StreamingOutput stream = output -> output.write(response.join().readAllBytes());

            return Response.ok(stream)
                    .header("Content-Disposition", "attachment; filename=\"" + fileName + "\"")
                    .build();

        } catch (Exception e) {
            LOGGER.error("Could not create FileOutputStream due to error", e);
        }
        return null;
    }

问题原因

在Quarkus的Resteasy Reactive模式下,直接返回StreamingOutput会被Jackson当作普通Java Bean尝试序列化,而StreamingOutput的Lambda实现没有可序列化的属性,因此抛出该错误。

可行解决方案

方案一:直接返回S3输入流(同步方式)

替换StreamingOutput,直接将S3获取到的ResponseInputStream作为响应体,并指定正确的媒体类型:

public Response downloadFile(final String bucketName, final String objectKey, final String fileExtension) {
    try {
        CompletableFuture<ResponseInputStream<GetObjectResponse>> responseFuture = s3.getObject(
                GetObjectRequest.builder()
                        .bucket(bucketName)
                        .key(objectKey)
                        .build(),
                AsyncResponseTransformer.toBlockingInputStream()
        );
        ResponseInputStream<GetObjectResponse> inputStream = responseFuture.join();
        
        final String fileName = objectKey.contains("/") ? objectKey.split("/")[1] : objectKey;
        
        // 根据实际文件类型调整Content-Type,比如application/pdf、image/png等
        String contentType = "application/octet-stream";
        if (fileExtension != null) {
            contentType = switch(fileExtension.toLowerCase()) {
                case "pdf" -> "application/pdf";
                case "jpg", "jpeg" -> "image/jpeg";
                case "png" -> "image/png";
                default -> "application/octet-stream";
            };
        }
        
        return Response.ok(inputStream)
                .header("Content-Disposition", "attachment; filename=\"" + fileName + "\"")
                .header("Content-Type", contentType)
                .build();
    } catch (Exception e) {
        LOGGER.error("下载文件失败", e);
        return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("文件下载失败").build();
    }
}

方案二:使用Mutiny Uni实现异步响应(推荐,符合Reactive风格)

避免阻塞join(),利用Quarkus原生的Mutiny异步API处理S3请求:

import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.core.Response;

public Uni<Response> downloadFile(final String bucketName, final String objectKey, final String fileExtension) {
    return Uni.createFrom().completionStage(() -> 
            s3.getObject(
                    GetObjectRequest.builder()
                            .bucket(bucketName)
                            .key(objectKey)
                            .build(),
                    AsyncResponseTransformer.toBlockingInputStream()
            )
    )
    .map(inputStream -> {
        final String fileName = objectKey.contains("/") ? objectKey.split("/")[1] : objectKey;
        String contentType = "application/octet-stream";
        if (fileExtension != null) {
            contentType = switch(fileExtension.toLowerCase()) {
                case "pdf" -> "application/pdf";
                case "jpg", "jpeg" -> "image/jpeg";
                case "png" -> "image/png";
                default -> "application/octet-stream";
            };
        }
        return Response.ok(inputStream)
                .header("Content-Disposition", "attachment; filename=\"" + fileName + "\"")
                .header("Content-Type", contentType)
                .build();
    })
    .onFailure().invoke(e -> LOGGER.error("下载文件失败", e))
    .onFailure().recoverWithItem(Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity("文件下载失败").build());
}

注意事项

  • 不要在Reactive端点中使用CompletableFuture.join(),这会阻塞线程,破坏Reactive模式的非阻塞特性,方案二更适合Quarkus的Reactive架构。
  • 务必根据实际文件类型设置正确的Content-Type,帮助浏览器正确识别文件格式。
  • 原代码中返回null会导致服务器返回500错误,建议在异常分支返回明确的错误响应。

内容的提问来源于stack exchange,提问作者ShowAndTell

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 05:27:04