找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3482

积分

0

好友

478

主题
发表于 2026-2-14 03:07:00 | 查看: 29| 回复: 0

在实际的后端开发中,用户批量上传图片是一个常见需求。如果采用同步处理,每张图片都需要等待服务器完成水印添加、生成缩略图、存储等一系列耗时操作,用户会经历漫长的等待。本文将分享在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:整合水印、缩略图与存储

这里展示了整合了图片处理逻辑的文件上传服务。我们提供了重载方法以同时支持MultipartFileFile参数。

@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声明式异步演进的一个痕迹。

整体架构流程

整个过程可以用下面这张图来概括:
Spring异步处理文件上传的完整流程图

流程简述:

  1. 客户端上传MultipartFile
  2. Controller(标注了@EnableAsync)快速将文件保存为独立临时文件,并调用Service的异步方法后立即返回成功响应。
  3. Spring线程池自动分配线程,执行Service中标注了@Async的方法。
  4. 异步方法中,完成图片处理(水印、缩略图)、持久化存储、记录数据库等所有耗时操作。
  5. 最后,清理所有过程中产生的临时文件。

总结

通过利用Spring Boot原生的@EnableAsync@Async注解以及可定制的ThreadPoolTaskExecutor,我们可以非常优雅地实现文件上传等耗时任务的异步化。关键在于理解并处理好MultipartFile的生命周期,以及遵循“调用者@EnableAsync,执行者@Async”的规则。这种模式不仅能提升用户体验,还能更合理地利用服务器资源,应对批量文件上传、复杂计算、外部API调用等多种异步场景。

希望这篇结合具体场景的实践分享,能帮助你在自己的项目中少走弯路。如果你在实践中遇到其他问题,或者有更优的解决方案,欢迎在云栈社区与其他开发者一起交流探讨。




上一篇:Spring Batch 实战:CSV 与数据库双源批处理教程(Spring Boot 2.x)
下一篇:MySQL性能优化:8种常见低效SQL写法分析与实战改写
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-23 14:19 , Processed in 0.666358 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表