Spring Boot异步图片生成问题解决方案
这个错误表明在处理异步图片生成时,上传的临时文件已经被系统清理了。这是Spring Boot中常见的异步文件处理问题。让我提供解决方案:
解决方案
1. 修改 generateBeautyImageAdvancedAsync 方法
在方法开头立即读取文件内容,避免后续读取:
java
@Async
@Retryable(
value = {Exception.class},
maxAttempts = 3,
backoff = @Backoff(delay = 10000, multiplier = 2)
)
public void generateBeautyImageAdvancedAsync(String taskId, String productName, String description,
MultipartFile referenceImage, String targetStyle,
String imageSize, Map<String, Object> taskInfo) {
log.info("开始高级美妆产品图片生成,任务ID: {}, 产品: {}", taskId, productName);
updateTask(taskInfo, "初始化任务", 5);
// 重要:立即读取并存储文件内容,避免后续文件被清理
byte[] referenceImageBytes = null;
String imageBase64 = null;
MultipartFile memoryImage = null;
boolean hasReferenceImage = referenceImage != null && !referenceImage.isEmpty();
if (hasReferenceImage) {
try {
// 1. 立即读取文件内容到内存
referenceImageBytes = referenceImage.getBytes();
imageBase64 = Base64.getEncoder().encodeToString(referenceImageBytes);
// 2. 创建内存中的MultipartFile副本
memoryImage = new InMemoryMultipartFile(
referenceImage.getName(),
referenceImage.getOriginalFilename(),
referenceImage.getContentType(),
referenceImageBytes
);
log.info("任务 {}: 已将参考图片读取到内存,大小: {} bytes", taskId, referenceImageBytes.length);
} catch (IOException e) {
log.error("任务 {}: 读取参考图片失败", taskId, e);
updateTask(taskInfo, "失败", 0);
taskInfo.put("success", false);
taskInfo.put("error", "读取参考图片失败: " + e.getMessage());
throw new RuntimeException("读取参考图片失败: " + taskId, e);
}
}
try {
String finalPrompt = null;
Map<String, Object> promptAnalysis = null;
// 1. 获取提示词
if (hasReferenceImage) {
log.info("任务 {}: 开始多模态分析", taskId);
updateTask(taskInfo, "多模态分析", 25);
// 使用内存中的图片副本
promptAnalysis = multimodalPromptService.generateUltraDetailedPromptAnalysis(
productName, description, memoryImage, targetStyle);
if (promptAnalysis != null && promptAnalysis.containsKey("mainPrompt")) {
finalPrompt = (String) promptAnalysis.get("mainPrompt");
log.info("任务 {}: 多模态分析成功!提示词长度: {}", taskId, finalPrompt.length());
taskInfo.put("promptAnalysis", promptAnalysis);
taskInfo.put("promptSource", "multimodal");
} else {
log.warn("任务 {}: 多模态分析未返回有效提示词,将使用文本生成", taskId);
// 如果多模态分析失败,回退到文本生成
updateTask(taskInfo, "多模态分析失败,使用文本生成", 30);
finalPrompt = generateUltraHighQualityPrompt(productName, description, targetStyle);
taskInfo.put("promptSource", "fallback_text");
}
} else {
// 无参考图:生成高质量提示词
log.info("任务 {}: 无参考图,生成高质量文本提示词", taskId);
updateTask(taskInfo, "生成高质量提示词", 30);
finalPrompt = generateUltraHighQualityPrompt(productName, description, targetStyle);
taskInfo.put("promptSource", "ultra_quality_text");
}
// 验证提示词
if (finalPrompt == null || finalPrompt.trim().isEmpty()) {
throw new RuntimeException("提示词生成失败");
}
log.info("任务 {}: 最终提示词预览: {}", taskId, finalPrompt.substring(0, Math.min(100, finalPrompt.length())));
updateTask(taskInfo, "准备生成参数", 45);
// 2. 构建请求参数
Map<String, Object> parameters = new HashMap<>();
parameters.put("negative_prompt", buildDefaultNegativePrompt());
parameters.put("guidance_scale", 7.5);
parameters.put("num_inference_steps", 30);
// 3. 构建请求体
JSONObject requestBody;
if (hasReferenceImage) {
// 使用之前保存的base64字符串
requestBody = buildImageToImageRequest(finalPrompt, imageSize, imageBase64, parameters);
log.info("任务 {}: 构建图生图请求", taskId);
} else {
requestBody = buildTextToImageRequest(finalPrompt, imageSize, parameters);
log.info("任务 {}: 构建文生图请求", taskId);
}
updateTask(taskInfo, "调用图片生成API", 65);
// 4. 调用图片生成API
List<String> imageUrls = submitImageGeneration(requestBody, taskId, 3);
// 5. 处理结果
if (imageUrls.isEmpty() || imageUrls.get(0).startsWith("async://")) {
// 异步任务,需要进一步处理
updateTask(taskInfo, "等待异步处理", 85);
// 简化处理:标记为异步
taskInfo.put("asyncTask", true);
taskInfo.put("requestId", imageUrls.get(0).replace("async://", ""));
taskInfo.put("status", "异步处理中");
taskInfo.put("progress", 90);
} else {
// 同步任务成功
updateTask(taskInfo, "图片生成完成", 95);
taskInfo.put("imageUrls", imageUrls);
taskInfo.put("prompt", finalPrompt);
taskInfo.put("success", true);
taskInfo.put("generatedAt", System.currentTimeMillis());
taskInfo.put("qualityLevel", "ultra");
// 记录生成的图片数量
taskInfo.put("imageCount", imageUrls.size());
log.info("任务 {}: 成功生成 {} 张图片", taskId, imageUrls.size());
}
// 保存元数据
if (promptAnalysis != null) {
taskInfo.putAll(promptAnalysis);
}
updateTask(taskInfo, "任务完成", 100);
log.info("任务 {}: 图片生成任务完成", taskId);
} catch (Exception e) {
log.error("任务 {}: 图片生成任务失败", taskId, e);
updateTask(taskInfo, "失败", 0);
taskInfo.put("success", false);
taskInfo.put("error", "图片生成失败: " + e.getMessage());
taskInfo.put("errorDetails", e.getClass().getName() + ": " + e.getMessage());
taskInfo.put("qualityLevel", "failed");
throw new RuntimeException("图片生成任务失败: " + taskId, e);
}
}
2. 添加异步任务配置(可选但建议)
创建配置文件:
java
package com.alatus.salesSystem.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10);
executor.setMaxPoolSize(50);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("AsyncImageGen-");
executor.initialize();
return executor;
}
@Bean(name = "imageGenerationExecutor")
public Executor imageGenerationExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(20);
executor.setQueueCapacity(50);
executor.setThreadNamePrefix("ImageGen-");
executor.setKeepAliveSeconds(60);
executor.initialize();
return executor;
}
}
3. 修改Controller层,添加文件立即读取逻辑
java
@RestController
@RequestMapping("/api/image")
public class ImageGenerationController {
@PostMapping("/generate/advanced")
public ResponseEntity<Map<String, Object>> generateBeautyImageAdvanced(
@RequestParam("productName") String productName,
@RequestParam("description") String description,
@RequestParam(value = "referenceImage", required = false) MultipartFile referenceImage,
@RequestParam(value = "targetStyle", defaultValue = "luxury") String targetStyle,
@RequestParam(value = "imageSize", defaultValue = "1024x1024") String imageSize) {
String taskId = "img_" + System.currentTimeMillis();
Map<String, Object> taskInfo = new ConcurrentHashMap<>();
taskInfo.put("taskId", taskId);
taskInfo.put("productName", productName);
taskInfo.put("startTime", System.currentTimeMillis());
taskInfo.put("status", "init");
// 启动异步任务
try {
imageGenerationService.generateBeautyImageAdvancedAsync(
taskId, productName, description, referenceImage,
targetStyle, imageSize, taskInfo
);
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("taskId", taskId);
response.put("message", "图片生成任务已启动");
response.put("startTime", System.currentTimeMillis());
return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("启动图片生成任务失败", e);
Map<String, Object> response = new HashMap<>();
response.put("success", false);
response.put("error", "启动任务失败: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}
@GetMapping("/task/{taskId}/status")
public ResponseEntity<Map<String, Object>> getTaskStatus(@PathVariable String taskId) {
// 从内存、数据库或缓存中获取任务状态
Map<String, Object> taskInfo = taskStatusService.getTaskInfo(taskId);
if (taskInfo == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("error", "任务不存在"));
}
return ResponseEntity.ok(taskInfo);
}
}
4. 添加任务状态管理服务
java
@Service
public class TaskStatusService {
private final Map<String, Map<String, Object>> taskStore = new ConcurrentHashMap<>();
public void saveTaskInfo(String taskId, Map<String, Object> taskInfo) {
taskStore.put(taskId, taskInfo);
}
public Map<String, Object> getTaskInfo(String taskId) {
return taskStore.get(taskId);
}
public void removeTask(String taskId) {
taskStore.remove(taskId);
}
}
5. 在ImageGenerationService中添加状态更新
java
@Autowired
private TaskStatusService taskStatusService;
private void updateTask(Map<String, Object> taskInfo, String status, int progress) {
taskInfo.put("status", status);
taskInfo.put("progress", progress);
taskInfo.put("lastUpdated", System.currentTimeMillis());
// 保存到任务存储
String taskId = (String) taskInfo.get("taskId");
if (taskId != null) {
taskStatusService.saveTaskInfo(taskId, taskInfo);
}
log.info("任务状态更新 - ID: {}, 状态: {}, 进度: {}%",
taskId, status, progress);
}
关键改进点:
-
立即读取文件内容:在异步方法开始时立即读取MultipartFile内容到内存
-
使用内存副本:创建
InMemoryMultipartFile副本,避免后续读取临时文件 -
错误处理:在文件读取失败时立即返回错误,避免后续重试
-
状态管理:使用
ConcurrentHashMap存储任务状态 -
日志完善:添加详细的日志记录
使用注意事项:
-
大文件处理:如果参考图片很大,需要考虑内存限制
-
任务清理:建议添加定时任务清理过期的任务信息
-
持久化存储:如果需要持久化任务状态,可以考虑使用数据库或Redis
-
超时处理:添加任务超时监控,避免任务无限期运行
这样修改后,应该可以解决临时文件被清理导致NoSuchFileException的问题。
我们注意到错误日志中显示了一个文件找不到的异常。具体是:
java.nio.file.NoSuchFileException: C:\Users\15713\AppData\Local\Temp\tomcat.8080.8081934852499604051\work\Tomcat\localhost\ROOT\upload_7a4d2a3e_959c_4783_bca9_4f205caaed53_00000004.tmp
这个异常发生在第1251行,即调用referenceImage.getBytes()时。这是因为在异步处理中,上传的文件是临时存储的,可能在异步方法执行时已经被清理。
我们已经有一个InMemoryMultipartFile类来避免文件重试问题,但是在有参考图片的情况下,我们在两个地方读取了文件:
-
在生成提示词时,我们调用
multimodalPromptService.generateUltraDetailedPromptAnalysis,这里我们传递了一个内存中的MultipartFile。 -
在构建图生图请求时,我们再次读取了
referenceImage.getBytes()。
但是,注意在异步方法中,我们首先在开头读取了文件内容并创建了内存中的MultipartFile,然后在构建请求时又读取了一次。然而,在构建请求时,我们使用的是原始的referenceImage,而不是我们创建的内存副本。
因此,我们需要确保在整个异步方法中,我们都使用内存中的文件副本,而不是原始的MultipartFile,因为原始文件可能已经被清理。
让我们检查generateBeautyImageAdvancedAsync方法:
在方法开头,我们确实读取了参考图片并创建了内存副本,但是在构建请求时,我们使用了原始的referenceImage来获取字节数组。这可能导致在重试时文件不存在。
修正步骤:
-
在方法开头,如果有参考图片,立即读取字节数组并创建内存副本,然后后续所有操作都使用这个内存副本。
-
确保在构建请求时,使用内存副本的字节数组,而不是重新读取原始文件。
但是,我们注意到在构建请求时,我们又一次调用了referenceImage.getBytes(),这可能会再次读取临时文件,而临时文件可能已经被删除。
所以,我们需要修改代码,在方法开头就将参考图片的字节数组保存起来,并且后续只使用这个字节数组。
具体修改如下:
在方法开头,我们读取参考图片的字节数组,并创建一个内存中的MultipartFile副本(我们已经这样做了)。然后,在构建请求时,我们使用这个字节数组,而不是重新读取原始文件。
但是,我们注意到在构建请求时,我们调用了buildImageToImageRequest,它需要的是base64字符串,所以我们可以直接使用之前已经读取的字节数组。
所以,我们可以这样做:
-
声明一个字节数组变量
referenceImageBytes,在开头读取并保存。 -
在需要参考图片的地方,都使用这个字节数组。
但是,我们注意到在构建请求时,我们又一次调用了referenceImage.getBytes(),这应该是导致错误的原因。
让我们修改代码,在开头读取参考图片的字节数组,然后在后续使用这个字节数组。
修改后的代码结构:
在方法开头:
boolean hasReferenceImage = referenceImage != null && !referenceImage.isEmpty();
byte[] referenceImageBytes = null;
if (hasReferenceImage) {
referenceImageBytes = referenceImage.getBytes(); // 这里读取原始文件
// 创建内存中的MultipartFile副本,用于多模态分析
MultipartFile copiedImage = new InMemoryMultipartFile(...);
// 然后使用copiedImage进行多模态分析
}
然后,在构建请求时,我们使用referenceImageBytes,而不是再次调用referenceImage.getBytes()。
但是,我们注意到在构建请求时,我们调用了referenceImage.getBytes(),而referenceImage是原始参数,可能已经不可用。所以,我们应该使用之前保存的referenceImageBytes。
所以,将构建请求的部分改为使用referenceImageBytes。
另外,我们注意到在构建请求时,我们又一次读取了原始文件,这应该被替换。
修改后的代码:
在方法开头读取参考图片的字节数组,并创建内存副本。然后,在构建图生图请求时,使用这个字节数组。
但是,我们注意到在构建请求时,我们调用了buildImageToImageRequest,它需要base64字符串,所以我们可以在那里使用之前保存的字节数组。
所以,修改后的代码:
-
在开头读取参考图片字节数组。
-
在构建请求时,使用这个字节数组。
另外,我们注意到在构建请求时,我们又一次调用了referenceImage.getBytes(),这行代码应该被替换。
让我们修改generateBeautyImageAdvancedAsync方法:
将原来的:
if (hasReferenceImage) {
byte[] imageBytes = referenceImage.getBytes();
String imageBase64 = Base64.getEncoder().encodeToString(imageBytes);
requestBody = buildImageToImageRequest(finalPrompt, imageSize, imageBase64, parameters);
}
改为使用之前保存的referenceImageBytes。
但是,我们之前已经将参考图片的字节数组保存到了referenceImageBytes变量中,所以这里可以直接使用。
但是,注意我们之前已经创建了一个内存副本,并且这个内存副本的字节数组就是referenceImageBytes。所以,我们可以直接使用referenceImageBytes。
所以,修改后的代码:
在开头:
byte[] referenceImageBytes = null;
if (hasReferenceImage) {
referenceImageBytes = referenceImage.getBytes();
// 创建内存副本,用于多模态分析
MultipartFile copiedImage = new InMemoryMultipartFile(...);
// 使用copiedImage进行多模态分析
}
然后,在构建请求时:
if (hasReferenceImage) {
String imageBase64 = Base64.getEncoder().encodeToString(referenceImageBytes);
requestBody = buildImageToImageRequest(finalPrompt, imageSize, imageBase64, parameters);
}
这样,我们就避免了多次读取原始文件。
但是,我们注意到在构建请求之前,我们可能再次调用了referenceImage.getBytes(),这是在第1251行,也就是在构建请求之前。实际上,我们在构建请求之前,还有一次读取:
在生成提示词时,我们调用了multimodalPromptService.generateUltraDetailedPromptAnalysis,并传递了内存副本,所以这里没有问题。
所以,主要问题是在构建请求时,我们再次读取了原始文件。
让我们修改代码,确保在构建请求时使用之前保存的字节数组。
另外,我们注意到在构建请求时,我们使用了referenceImage.getBytes(),而referenceImage是原始参数,可能已经被清理。所以,我们不能再使用它。
修改后的完整代码段如下:
我将重写generateBeautyImageAdvancedAsync方法中关于参考图片的部分,以确保只读取一次文件,并使用内存中的字节数组。
但是,由于代码较长,我在这里只给出修改后的关键部分:
-
在方法开头,读取参考图片的字节数组,并创建内存副本。
-
在生成提示词时,使用内存副本。
-
在构建请求时,使用之前保存的字节数组。
修改后的代码:
注意:由于我们已经在开头读取了参考图片的字节数组,并且创建了内存副本,那么在构建请求时,我们就不应该再使用原始的referenceImage,而是使用保存的字节数组。
所以,修改后的generateBeautyImageAdvancedAsync方法中关于参考图片的部分:
原始代码:
boolean hasReferenceImage = referenceImage != null && !referenceImage.isEmpty();
if (hasReferenceImage) {
// 读取图片内容(避免重试时文件不可用)
byte[] imageBytes = referenceImage.getBytes();
String imageBase64 = Base64.getEncoder().encodeToString(imageBytes);
// 创建内存中的MultipartFile
MultipartFile copiedImage = new InMemoryMultipartFile(...);
promptAnalysis = multimodalPromptService.generateUltraDetailedPromptAnalysis(...);
...
}
然后,在构建请求时,我们再次读取了原始文件:
if (hasReferenceImage) {
byte[] imageBytes = referenceImage.getBytes(); // 这里再次读取,可能导致文件不存在
String imageBase64 = Base64.getEncoder().encodeToString(imageBytes);
requestBody = buildImageToImageRequest(finalPrompt, imageSize, imageBase64, parameters);
}
修改后:
boolean hasReferenceImage = referenceImage != null && !referenceImage.isEmpty();
byte[] referenceImageBytes = null;
if (hasReferenceImage) {
// 只读取一次
referenceImageBytes = referenceImage.getBytes();
// 创建内存中的MultipartFile
MultipartFile copiedImage = new InMemoryMultipartFile(...);
promptAnalysis = multimodalPromptService.generateUltraDetailedPromptAnalysis(...);
...
}
... // 其他代码
// 构建请求
if (hasReferenceImage) {
// 使用之前保存的字节数组
String imageBase64 = Base64.getEncoder().encodeToString(referenceImageBytes);
requestBody = buildImageToImageRequest(finalPrompt, imageSize, imageBase64, parameters);
} else {
requestBody = buildTextToImageRequest(finalPrompt, imageSize, parameters);
}
这样,我们就避免了多次读取原始文件,从而避免了临时文件被删除后读取失败的问题。
但是,我们还需要注意,在构建请求时,我们使用的是referenceImageBytes,这个变量在if块中赋值,所以如果hasReferenceImage为true,它一定不为null。
修改后的代码应该可以解决临时文件找不到的问题。
另外,我们注意到在构建请求时,我们使用了referenceImageBytes,而在生成提示词时,我们使用了内存副本。这样,整个异步方法中,我们都没有再次使用原始的referenceImage,从而避免了临时文件的问题。
让我们重新检查整个方法,确保没有其他地方再次使用了原始的referenceImage。
在方法中,我们只在开头使用了referenceImage来读取字节数组和创建内存副本,之后就没有再使用它。所以,修改后的代码应该是安全的。
现在,我们将修改后的generateBeautyImageAdvancedAsync方法的关键部分写出:
注意:由于代码较长,我只修改关键部分,其余部分保持不变。
修改后的generateBeautyImageAdvancedAsync方法:
我将重写整个方法,但为了简洁,只修改与参考图片相关的部分。
更多推荐



所有评论(0)