侧边栏壁纸
  • 累计撰写 68 篇文章
  • 累计创建 22 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Spring Boot 3.x之MultipartFile 兼容性及解决方案文档

七月流火
2025-12-29 / 0 评论 / 0 点赞 / 25 阅读 / 0 字

一、生产环境问题:子线程中 MultipartFile 文件内容丢失

问题描述:

在 Spring Cloud 项目中,使用 EasyExcel 等工具在子线程中生成文件字节数组(ByteArrayOutputStream),然后尝试将其包装成 MultipartFile 并通过 Feign 客户端上传到文件服务器(如 MinIO)。升级到 Spring Boot 3.x 后,上传成功但文件内容丢失(大小为 0)。

根本原因:

在生产代码中,我们不能使用测试专用的 MockMultipartFile。自定义实现的 MultipartFile(如 ResourceMultipartFile)是正确的方向,但需要确保实现健壮性,同时确保 EasyExcel 内容写入的完整性

解决方案:使用健壮的 ResourceMultipartFile

1. 定义自定义 ResourceMultipartFile

src/main/java 目录下创建一个健壮的 MultipartFile 实现,确保它正确实现了读取字节数组的方法。

Java

package com.yourcompany.util.file; // 请替换为您的实际包名

import org.springframework.web.multipart.MultipartFile;
import org.springframework.util.FileCopyUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.File;

/**
 * 生产环境用于包装字节数组的 MultipartFile 实现,替代 MockMultipartFile
 */
public class ResourceMultipartFile implements MultipartFile {

    private final String name;
    private final String originalFilename;
    private final String contentType;
    private final byte[] content;

    public ResourceMultipartFile(String name, String originalFilename, String contentType, byte[] content) {
        this.name = name;
        this.originalFilename = originalFilename;
        this.contentType = contentType;
        // 确保 content 不为 null
        this.content = (content == null ? new byte[0] : content);
    }

    @Override
    public String getName() { 
        return this.name; 
    }

    @Override
    public String getOriginalFilename() { 
        return this.originalFilename; 
    }

    @Override
    public String getContentType() { 
        return this.contentType; 
    }

    @Override
    public boolean isEmpty() { 
        return this.content.length == 0; 
    }

    @Override
    public long getSize() { 
        return this.content.length; 
    }

    @Override
    public byte[] getBytes() throws IOException { 
        return this.content; 
    }

    @Override
    public InputStream getInputStream() throws IOException {
        // 关键:将字节数组转换为输入流
        return new ByteArrayInputStream(this.content); 
    }

    @Override
    public void transferTo(File dest) throws IOException, IllegalStateException {
        // 使用 Spring 工具类安全地将内容写入目标文件
        FileCopyUtils.copy(this.content, dest);
    }
}

2. 在子线程中执行并验证

在子线程的 finally 块中,务必在获取字节数组之前调用 excelWriter.finish(),并进行日志检查。

Java

// 确保导入您自定义的 ResourceMultipartFile
import com.yourcompany.util.file.ResourceMultipartFile;
// 确保导入其他 Spring/EasyExcel 相关的类
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.ExcelWriter;
import com.alibaba.excel.write.metadata.WriteSheet;
import java.io.ByteArrayOutputStream;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
// ... (其他您需要的导入)

// 假设这段代码位于某个 Service 或 Controller 中
public void exportOrderDetails(QueryParametersVo queryParametersVo) {
    
    final Logger log = LoggerFactory.getLogger(getClass());
    
    log.info("-----导出助餐订单详细信息列表主线程开始调用----");
    
    new Thread(() -> {
        log.info("-----导出助餐订单详细信息列表线程异步调用----");
        
        // 1. 获取数据
        List<MealOrderDetailsVo> list = jshMealOrderHeadService.exportMealOrderDetailsList(queryParametersVo);
        
        // 2. 初始化 IO 和 EasyExcel
        String fileName = queryParametersVo.getStartDate() + "至" + queryParametersVo.getEndDate() + "订单详情.xlsx";
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ExcelWriter excelWriter = null;

        try {
            excelWriter = EasyExcel.write(byteArrayOutputStream).build();
            // 写入 Sheet
            WriteSheet writeSheet = EasyExcel.writerSheet(0, "sheet").head(MealOrderDetailsVo.class).build();
            excelWriter.write(list, writeSheet);
            
        } catch (Exception e) {
            log.error("EasyExcel 写入文件异常失败: {}", e.getMessage(), e);
        } finally {
            if (excelWriter != null) {
                excelWriter.finish(); // 关键:确保内容已写入流
            }

            // 3. 获取字节内容并检查
            byte[] fileContent = byteArrayOutputStream.toByteArray();
            log.info("【上传检查】Excel文件字节数组大小: {} bytes", fileContent.length); 

            if (fileContent.length > 0) {
                log.info("-----开始上传文件---");
                
                // 4. 使用 ResourceMultipartFile 替换 MockMultipartFile
                org.springframework.web.multipart.MultipartFile file = new ResourceMultipartFile(
                    "file", // 推荐使用 "file"
                    fileName,
                    "application/vnd.openxmlformats-officedocument.spreadsheetml.xlsx", 
                    fileContent
                );
                
                // 5. 调用远程文件上传服务
                R<SysFile> sysFileR = remoteFileService.erpStatisticUpload(file, SecurityConstants.INNER);
                SysFile sysFile = sysFileR.getData();
                
                if (ObjectUtil.isEmpty(sysFile)) {
                    // 使用 RuntimeException 可能会中断线程,但可能无法通知主流程
                    log.error("------文件服务器出现异常,请联系管理员");
                    return; // 结束子线程
                }
                
                // 6. 保存文件地址信息
                JshMealOrderHeadStatisticsFile jshMealOrderHeadStatisticsFile = new JshMealOrderHeadStatisticsFile();
                // ... (设置其他文件信息)
                jshMealOrderHeadStatisticsFile.setFileUrl(sysFile.getUrl());
                jshMealOrderHeadStatisticsFileService.insertJshMealOrderHeadStatisticsFile(jshMealOrderHeadStatisticsFile);
                
                log.info("-----导出订单详细信息上传文件成功---");
            } else {
                log.warn("【上传中止】Excel文件内容为空,不执行上传。");
            }
        }
    }).start();
    
    log.info("-----导出订单详细信息主线程调用结束----");
}

二、测试环境问题:MockMultipartFile 找不到

问题描述:

在单元测试中,import org.springframework.mock.web.MockMultipartFile; 报错,编译器找不到该类。

根本原因:

MockMultipartFile 属于 Spring Test 框架,而 Spring Boot 3.x 的默认依赖管理可能未将该依赖正确引入到您的编译路径中。

解决方案:添加或检查 spring-boot-starter-test 依赖

确保您的构建文件(pom.xmlbuild.gradle)中明确包含了正确的测试依赖。

1. Maven (pom.xml)

XML

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope> </dependency>

2. Gradle (build.gradle)

Groovy

dependencies {
    // ... 其他依赖
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

注意: spring-boot-starter-test 自动包含了 spring-test,其中就包含了 MockMultipartFile。如果您的测试类仍然报错,请清理项目缓存并重新编译。


三、Spring Boot 3.x 自动配置兼容性问题

问题描述:

自定义的配置类(如 WebMvcConfigTokenService 等)在升级到 Spring Boot 3.x 后,主应用无法自动发现和加载这些组件,导致依赖注入失败。

根本原因:

Spring Boot 3.x 废弃了旧的 META-INF/spring.factories 自动配置机制,改用新的标准。

解决方案:迁移至 AutoConfiguration.imports

如果您将自定义的配置打包成独立的 JAR 包供其他项目引用(例如您截图中的 jjyang-common-security),您必须使用新的文件路径来定义自动配置。

  1. 创建文件: 在您的依赖包的 src/main/resources/META-INF/spring/ 目录下,创建一个名为 org.springframework.boot.autoconfigure.AutoConfiguration.imports 的文件。

  2. 添加类路径: 将您所有需要被自动加载的配置类、服务类、切面类等组件的全限定名(Fully Qualified Name)逐行写入此文件。

    示例(您的截图内容):

    com.jjyang.common.security.config.WebMvcConfig
    com.jjyang.common.security.service.TokenService
    com.jjyang.common.security.aspect.PreAuthorizeAspect
    com.jjyang.common.security.aspect.InnerAuthAspect
    com.jjyang.common.security.handler.GlobalExceptionHandler
    

结论: 采用 AutoConfiguration.imports 是 Spring Boot 3.x 下解决自定义依赖包中组件自动配置加载问题的标准做法

0

评论区