一、生产环境问题:子线程中 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.xml 或 build.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 自动配置兼容性问题
问题描述:
自定义的配置类(如 WebMvcConfig、TokenService 等)在升级到 Spring Boot 3.x 后,主应用无法自动发现和加载这些组件,导致依赖注入失败。
根本原因:
Spring Boot 3.x 废弃了旧的 META-INF/spring.factories 自动配置机制,改用新的标准。
解决方案:迁移至 AutoConfiguration.imports
如果您将自定义的配置打包成独立的 JAR 包供其他项目引用(例如您截图中的 jjyang-common-security),您必须使用新的文件路径来定义自动配置。
创建文件: 在您的依赖包的
src/main/resources/META-INF/spring/目录下,创建一个名为org.springframework.boot.autoconfigure.AutoConfiguration.imports的文件。添加类路径: 将您所有需要被自动加载的配置类、服务类、切面类等组件的全限定名(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 下解决自定义依赖包中组件自动配置加载问题的标准做法。
评论区