在实际的后端开发中,用户批量上传图片是一个常见需求。如果采用同步处理,每张图片都需要等待服务器完成水印添加、生成缩略图、存储等一系列耗时操作,用户会经历漫长的等待。本文将分享在Spring Boot项目中,如何利用其原生支持的多线程能力,实现图片上传的异步处理,从而显著提升接口响应速度。
核心技术栈与环境
本项目基于以下技术构建:
- Spring Boot: 作为核心应用框架。
- thumbnailator: 一个强大的Java缩略图生成库,用于图片缩放、添加水印。
- FastDFS: 分布式文件存储系统(注:读者可替换为任何OSS或本地存储方案)。
图片处理工具类:使用Thumbnailator
首先,通过Maven引入Thumbnailator依赖,它API简洁,无需复杂的原生库配置。
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.8</version>
</dependency>
接着,我们创建一个图片处理工具类,封装加水印和生成缩略图的方法。
import net.coobird.thumbnailator.Thumbnails;
import net.coobird.thumbnailator.geometry.Positions;
import org.springframework.stereotype.Component;
import javax.imageio.ImageIO;
import java.io.File;
import java.io.IOException;
@Component
public class PictureUtil {
/**
* 水印图片
*/
private static File markIco = null;
//开机静态加载水印图片
static {
try {
markIco = new File(new File("").getCanonicalPath() + "/icon.png");
LogUtil.info(PictureUtil.class, "水印图片加载" + (markIco.exists() ? "成功" : "失败"));
} catch (Exception e) {
}
}
/**
* 加水印
*/
public void photoMark(File sourceFile, File toFile) throws IOException {
Thumbnails.of(sourceFile)
.size(600, 450)//尺寸
.watermark(Positions.BOTTOM_CENTER/*水印位置:中央靠下*/,
ImageIO.read(markIco), 0.7f/*质量,越大质量越高(1)*/)
//.outputQuality(0.8f)
.toFile(toFile);//保存为哪个文件
}
/**
* 生成图片缩略图
*/
public void photoSmaller(File sourceFile, File toFile) throws IOException {
Thumbnails.of(sourceFile)
.size(200, 150)//尺寸
//.watermark(Positions.CENTER, ImageIO.read(markIco), 0.1f)
.outputQuality(0.4f)//缩略图质量
.toFile(toFile);
}
/**
* 生成视频缩略图(这块还没用到呢)
*/
public void photoSmallerForVedio(File sourceFile, File toFile) throws IOException {
Thumbnails.of(sourceFile)
.size(440, 340)
.watermark(Positions.BOTTOM_CENTER, ImageIO.read(markIco), 0.1f)
.outputQuality(0.8f)
.toFile(toFile);
}
}
使用心得:Thumbnailator非常容易集成,但需要注意其.watermark()方法通常需要先调用.size()调整尺寸。对于简单的图片处理需求,它是一个避免引入复杂原生依赖的优秀选择。
配置Spring Boot线程池
Spring Boot使得线程池的配置和使用变得异常简单。我们只需一个配置类。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskExecutor;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
public class PoolConfig {
@Bean//return new AsyncResult<>(res);
public TaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.initialize(); // 设置核心线程数
executor.setCorePoolSize(4); // 设置最大线程数
executor.setMaxPoolSize(32); // 设置队列容量
executor.setQueueCapacity(512); // 设置线程活跃时间(秒)
executor.setKeepAliveSeconds(60); // 设置默认线程名称
executor.setThreadNamePrefix("ThreadPool-"); // 设置拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); // 等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
return executor;
}
}
关键规则:要使@Async注解生效,必须确保调用@Async方法的类本身被@EnableAsync注解标记。通常的做法是在Controller层使用@EnableAsync,在Service层的方法上使用@Async。注意,同一个类内部调用其自身的@Async方法是不会生效的。
为什么需要异步?同步 vs 异步流程对比
想象一个场景:用户一次上传10多张高清图片,每张图片处理(保存、加水印、生成缩略图、上传到文件服务器)可能需要3-15秒。
- 旧方法(同步): 客户端上传一张 -> 服务器开始处理 -> 客户端等待3-15秒 -> 收到成功响应 -> 再上传下一张。总等待时间极长,用户体验差。
- 新方法(异步): 客户端上传一张 -> 服务器立即接收并返回“上传成功” -> 耗时的图片处理任务被抛给后台线程池执行 -> 客户端无需等待即可上传下一张。
下图清晰地展示了两种方式的差异:

异步化的核心价值在于,将“网络传输成功”的即时反馈与“服务器端数据处理”的耗时任务解耦,极大提升了客户端的操作流畅度。
核心业务代码实现
1. Controller层:快速接收并返回
Controller负责接收文件,创建临时文件,并触发异步处理流程。
@ApiOperation("上传业务图片")
@PostMapping("/push/photo/{id}/{name}")
@EnableAsync // 关键:在此类上启用异步支持
public R pushHousingPhotoMethod(
@ApiParam("SourceId") @PathVariable Integer id,
@ApiParam("图片名称不约束,可不填则使用原名,可使用随机码或原名称,但必须带扩展名") @PathVariable(required = false) String name,
@RequestParam MultipartFile file) throws InterruptedException, ExecutionException, IOException {
String fileName = file.getOriginalFilename();
String ext = StringUtils.substring(fileName, fileName.lastIndexOf('.'),fileName.length());
File tempPhoto = File.createTempFile(UUIDUtil.make32BitUUID(), ext);
file.transferTo(tempPhoto);//转储临时文件
service.pushPhoto(id, name, tempPhoto); // 调用异步Service方法
return new R(); // 立即返回,无需等待处理完成
}
重要避坑点:为什么需要File.createTempFile转存?
MultipartFile本质上是一个临时文件,其生命周期与当前Controller请求绑定,在方法返回时就会被清理。如果直接将MultipartFile传递给异步方法,很可能出现异步线程还未开始处理,原文件已被删除的情况。因此,必须手动创建一个独立的临时文件供后台线程使用,并在处理完毕后手动删除。
一个简单的UUID生成工具:
/**
* 生成一个32位无横杠的UUID
*/
public synchronized static String make32BitUUID() {
return UUID.randomUUID().toString().replace("-","");
}
2. Service层:异步处理核心逻辑
在Service实现的方法上标注@Async,该方法内的所有操作将在独立的线程中执行。
@Async
public void pushHousingPhoto(Integer id,String name,File file) throws InterruptedException, ExecutionException, IOException {
//存储FDFS表id
Long startTime = System.currentTimeMillis();
Integer[] numb = fastDfsService.upLoadPhoto(StringUtils.isBlank(name) ? file.getName() : name, file).get();
SourcePhotosContext context = new SourcePhotosContext();
context.setSourceId(id);
context.setNumber(numb[0]);
context.setNumber2(numb[1]);
//保存图片关系
sourcePhotosContextService.insertNew(context);
Long endTime = System.currentTimeMillis();
LogUtil.info(this.getClass(),"source [ "+id+" ] 绑定图片 [ "+name+" ] 成功,内部处理耗时 ["+ (endTime-startTime) +"ms ]");
file.delete(); // 处理完成后,删除Controller创建的临时文件
//return new R();
}
3. 文件处理Service:整合水印、缩略图与存储
这里展示了整合了图片处理逻辑的文件上传服务。我们提供了重载方法以同时支持MultipartFile和File参数。
@Override
public Future<Integer[]> upLoadPhoto(String fileName, MultipartFile file) throws IOException {
String ext = StringUtils.substring(fileName, fileName.lastIndexOf('.'));
//创建临时文件
File sourcePhoto = File.createTempFile(UUIDUtil.make32BitUUID(), ext);
file.transferTo(sourcePhoto);
return upLoadPhoto(fileName, sourcePhoto); // 转为对File参数方法的重用调用
}
@Override
public Future<Integer[]> upLoadPhoto(String fileName, File sourcePhoto) throws IOException {
String ext = StringUtils.substring(fileName, fileName.lastIndexOf('.'));
//创建临时文件
File markedPhoto = File.createTempFile(UUIDUtil.make32BitUUID(), ext);
File smallerPhoto = File.createTempFile(UUIDUtil.make32BitUUID(), ext);
//加水印 缩图
pictureUtil.photoMark(sourcePhoto, markedPhoto);
pictureUtil.photoSmaller(markedPhoto, smallerPhoto);
//上传至文件存储系统(如FastDFS)
Integer markedPhotoNumber = upLoadPhotoCtrl(fileName, markedPhoto);
Integer smallerPhotoNumber = upLoadPhotoCtrl("mini_" + fileName, smallerPhoto);
//删除所有临时文件
sourcePhoto.delete();
markedPhoto.delete();
smallerPhoto.delete();
Integer[] res = new Integer[]{markedPhotoNumber, smallerPhotoNumber};
return new AsyncResult(res);
}
代码演进说明:上述代码中保留了Future<Integer[]>的返回类型,这是早期探索异步调用时使用的模式,通过.get()方法等待结果。在使用Spring的@Async后,如果不需要等待处理结果,完全可以将其改为void类型,使调用更加纯粹。这里保留原貌,也展示了从传统多线程向Spring声明式异步演进的一个痕迹。
整体架构流程
整个过程可以用下面这张图来概括:

流程简述:
- 客户端上传
MultipartFile。
- Controller(标注了
@EnableAsync)快速将文件保存为独立临时文件,并调用Service的异步方法后立即返回成功响应。
- Spring线程池自动分配线程,执行Service中标注了
@Async的方法。
- 异步方法中,完成图片处理(水印、缩略图)、持久化存储、记录数据库等所有耗时操作。
- 最后,清理所有过程中产生的临时文件。
总结
通过利用Spring Boot原生的@EnableAsync、@Async注解以及可定制的ThreadPoolTaskExecutor,我们可以非常优雅地实现文件上传等耗时任务的异步化。关键在于理解并处理好MultipartFile的生命周期,以及遵循“调用者@EnableAsync,执行者@Async”的规则。这种模式不仅能提升用户体验,还能更合理地利用服务器资源,应对批量文件上传、复杂计算、外部API调用等多种异步场景。
希望这篇结合具体场景的实践分享,能帮助你在自己的项目中少走弯路。如果你在实践中遇到其他问题,或者有更优的解决方案,欢迎在云栈社区与其他开发者一起交流探讨。