这个错误表明在处理异步图片生成时,上传的临时文件已经被系统清理了。这是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);
}

关键改进点:

  1. 立即读取文件内容:在异步方法开始时立即读取MultipartFile内容到内存

  2. 使用内存副本:创建InMemoryMultipartFile副本,避免后续读取临时文件

  3. 错误处理:在文件读取失败时立即返回错误,避免后续重试

  4. 状态管理:使用ConcurrentHashMap存储任务状态

  5. 日志完善:添加详细的日志记录

使用注意事项:

  1. 大文件处理:如果参考图片很大,需要考虑内存限制

  2. 任务清理:建议添加定时任务清理过期的任务信息

  3. 持久化存储:如果需要持久化任务状态,可以考虑使用数据库或Redis

  4. 超时处理:添加任务超时监控,避免任务无限期运行

这样修改后,应该可以解决临时文件被清理导致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类来避免文件重试问题,但是在有参考图片的情况下,我们在两个地方读取了文件:

  1. 在生成提示词时,我们调用multimodalPromptService.generateUltraDetailedPromptAnalysis,这里我们传递了一个内存中的MultipartFile。

  2. 在构建图生图请求时,我们再次读取了referenceImage.getBytes()

但是,注意在异步方法中,我们首先在开头读取了文件内容并创建了内存中的MultipartFile,然后在构建请求时又读取了一次。然而,在构建请求时,我们使用的是原始的referenceImage,而不是我们创建的内存副本。

因此,我们需要确保在整个异步方法中,我们都使用内存中的文件副本,而不是原始的MultipartFile,因为原始文件可能已经被清理。

让我们检查generateBeautyImageAdvancedAsync方法:

在方法开头,我们确实读取了参考图片并创建了内存副本,但是在构建请求时,我们使用了原始的referenceImage来获取字节数组。这可能导致在重试时文件不存在。

修正步骤:

  1. 在方法开头,如果有参考图片,立即读取字节数组并创建内存副本,然后后续所有操作都使用这个内存副本。

  2. 确保在构建请求时,使用内存副本的字节数组,而不是重新读取原始文件。

但是,我们注意到在构建请求时,我们又一次调用了referenceImage.getBytes(),这可能会再次读取临时文件,而临时文件可能已经被删除。

所以,我们需要修改代码,在方法开头就将参考图片的字节数组保存起来,并且后续只使用这个字节数组。

具体修改如下:

在方法开头,我们读取参考图片的字节数组,并创建一个内存中的MultipartFile副本(我们已经这样做了)。然后,在构建请求时,我们使用这个字节数组,而不是重新读取原始文件。

但是,我们注意到在构建请求时,我们调用了buildImageToImageRequest,它需要的是base64字符串,所以我们可以直接使用之前已经读取的字节数组。

所以,我们可以这样做:

  1. 声明一个字节数组变量referenceImageBytes,在开头读取并保存。

  2. 在需要参考图片的地方,都使用这个字节数组。

但是,我们注意到在构建请求时,我们又一次调用了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字符串,所以我们可以在那里使用之前保存的字节数组。

所以,修改后的代码:

  1. 在开头读取参考图片字节数组。

  2. 在构建请求时,使用这个字节数组。

另外,我们注意到在构建请求时,我们又一次调用了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方法中关于参考图片的部分,以确保只读取一次文件,并使用内存中的字节数组。

但是,由于代码较长,我在这里只给出修改后的关键部分:

  1. 在方法开头,读取参考图片的字节数组,并创建内存副本。

  2. 在生成提示词时,使用内存副本。

  3. 在构建请求时,使用之前保存的字节数组。

修改后的代码:

注意:由于我们已经在开头读取了参考图片的字节数组,并且创建了内存副本,那么在构建请求时,我们就不应该再使用原始的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方法:

我将重写整个方法,但为了简洁,只修改与参考图片相关的部分。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐