Spring 多线程异步上传图片、处理水印、缩略图

简介: Spring 多线程异步上传图片、处理水印、缩略图


使用环境

  • SpringBoot+FastDfs+thumbnailator
  • fdfs环境自己搞吧

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

thumbnailator

maven依赖:

<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);
    }
}

这个插件很好用,只需集成调用即可,我记得我还试过另外几个,需要另外在linux下配置.so文件的依赖等等,查了半天也没弄明白,很麻烦,这个方便。

这个插件又很不好用,必须要先调整尺寸,才能加水印,而且调整尺寸简直是负压缩。压了分辨率图片还能变大那种。但是简单嘛,这块不是重点。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

线程池

使用springboot线程池,方便易用,只需配置和加注解即可。

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;
    }
}

避坑知识点:配置springboot线程池,类上需要@Configuration@EnableAsync这两个注解,实际调用时,需要遵守一个规则,即在调用的方法的类上必须使用注解@EnableAsync,调用一个带有@Async的方法。

比如A类使用了注解@EnableAsync 在A类中调用B类的有@Async的方法,只有这样多线程才生效,A类内调用A类的@Async方法不生效。可以理解为Controller层使用@EnableAsync注解,Service层方法上标注@Async。这样在Controller层调用的Service方法会从线程池调用线程来执行。

异步逻辑:为什么要用多线程?

我画了一张简单的示意图,在这个项目中,客户端一次上传10多张图片,每个图片单独上传,等待所有图片上传返回200后,继续执行操作,如果一步一步处理,客户端需等待服务器处理完所有逻辑,这样浪费没必要的时间。顾使用异步操作,客户端只需上传图片,无需等待服务器处理(我们服务器很辣鸡,一个10M的图可能要搞10多秒,见笑)

业务代码

@ApiOperation("上传业务图片")
@PostMapping("/push/photo/{id}/{name}")
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);
    return new R();
}

业务代码里隐藏了一些项目相关的信息,就是某些名改了,嗯。

可以看到,使用StringUtils.substring(fileName, fileName.lastIndexOf(’.’),fileName.length());这句代码,调用apache.common.lang3工具类获取出了扩展名,因为扩展名对图片处理工具类有用,他通过扩展名识别图片格式,所以这个必须有,如代码,生成了一个使用随机码命名,但带有.png扩展名的临时文件,保存在默认临时路径以供处理。File.createTempFile(UUIDUtil.make32BitUUID(), ext);是生成临时文件的方法,UUIDUtil也很简单,我贴出来吧,省着还要找

注意:controller类上需要标注注解@EnableAsync

/**
 * 生成一个32位无横杠的UUID
 */
public synchronized static String make32BitUUID(){
    return UUID.randomUUID().toString().replace("-","");
}

避坑知识点:Spring使用MultipartFile接收文件,但不能直接把MultipartFile传下去处理,而是保存为临时文件,并不是多此一举。因为MultipartFile也是临时文件,他的销毁时间是你这个Controller层方法return的时候。

如果不使用异步,是可以在调用的方法里去处理MultipartFile文件的,但如果使用异步处理,肯定是这边线程还没处理完,那边Controller层已经return了,这个MultipartFile就被删除了,于是你的异步线程就找不到这张图了。那还处理个啥,对吧。所以需要手动保存为自己创建的临时文件,再在线程中处理完把他删掉。

贴Service层Impl实现类代码

@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 ]");
    //return new R();
}

这里的number和number2分别是带水印的原图和缩略图,context是个表,用来存图片和缩略图对应fdfs路径的,就不贴了。可见这个方法上带有注解@Async 所以整个方法会异步执行。

加水印处理写到fdfs的service里了,这样不算规范,可以不要学我:

@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);
}
@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);
    //上传
    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);
}

使用了方法重载,一个调用了另一个,方便以后处理MultipartFile和File格式的图片都能使用,可以见到使用了Future<Integer[]>这个东西作为返回值,完全可以不这么做,正常返回就行。我懒得改了,这也是不断探索多线程处理图片的过程中,遗留下来的东西。

在service中fastDfsService.upLoadPhoto(StringUtils.isBlank(name) ? file.getName() : name, file).get()这句就是得到了这个future的内容,可以去掉.get()Future<>。可见这一个小小的异步功能,其实走过了很多弯路。future其实是异步调用方法时,从.get()等待异步处理的结果,等待得到结果后获取内容并执行。现在使用spring线程池处理,已经不需要这样做了。

以上,希望你在实现这个功能时可以少走弯路。

附总体示意图:



相关文章
|
2月前
|
编解码 数据安全/隐私保护 计算机视觉
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
如何使用OpenCV进行同步和异步操作来打开海康摄像头,并提供了相关的代码示例。
127 1
Opencv学习笔记(十):同步和异步(多线程)操作打开海康摄像头
|
26天前
|
安全 Java 开发者
Spring容器中的bean是线程安全的吗?
Spring容器中的bean默认为单例模式,多线程环境下若操作共享成员变量,易引发线程安全问题。Spring未对单例bean做线程安全处理,需开发者自行解决。通常,Spring bean(如Controller、Service、Dao)无状态变化,故多为线程安全。若涉及线程安全问题,可通过编码或设置bean作用域为prototype解决。
32 1
|
3月前
|
Java Spring
spring多线程实现+合理设置最大线程数和核心线程数
本文介绍了手动设置线程池时的最大线程数和核心线程数配置方法,建议根据CPU核数及程序类型(CPU密集型或IO密集型)来合理设定。对于IO密集型,核心线程数设为CPU核数的两倍;CPU密集型则设为CPU核数加一。此外,还讨论了`maxPoolSize`、`keepAliveTime`、`allowCoreThreadTimeout`和`queueCapacity`等参数的设置策略,以确保线程池高效稳定运行。
391 10
spring多线程实现+合理设置最大线程数和核心线程数
|
3月前
|
Java Spring 容器
Spring使用异步注解@Async正确姿势
Spring使用异步注解@Async正确姿势,异步任务,spring boot
|
2月前
|
安全 调度 C#
STA模型、同步上下文和多线程、异步调度
【10月更文挑战第19天】本文介绍了 STA 模型、同步上下文和多线程、异步调度的概念及其优缺点。STA 模型适用于单线程环境,确保资源访问的顺序性;同步上下文和多线程提高了程序的并发性和响应性,但增加了复杂性;异步调度提升了程序的响应性和资源利用率,但也带来了编程复杂性和错误处理的挑战。选择合适的模型需根据具体应用场景和需求进行权衡。
|
2月前
|
网络协议 安全 Java
难懂,误点!将多线程技术应用于Python的异步事件循环
难懂,误点!将多线程技术应用于Python的异步事件循环
97 0
|
2月前
|
自然语言处理 JavaScript Java
Spring 实现 3 种异步流式接口,干掉接口超时烦恼
本文介绍了处理耗时接口的几种异步流式技术,包括 `ResponseBodyEmitter`、`SseEmitter` 和 `StreamingResponseBody`。这些工具可在执行耗时操作时不断向客户端响应处理结果,提升用户体验和系统性能。`ResponseBodyEmitter` 适用于动态生成内容场景,如文件上传进度;`SseEmitter` 用于实时消息推送,如状态更新;`StreamingResponseBody` 则适合大数据量传输,避免内存溢出。文中提供了具体示例和 GitHub 地址,帮助读者更好地理解和应用这些技术。
446 0
|
4月前
|
Java 数据库
异步&线程池 CompletableFuture 异步编排 实战应用 【终结篇】
这篇文章通过一个电商商品详情页的实战案例,展示了如何使用`CompletableFuture`进行异步编排,以解决在不同数据库表中查询商品信息的问题,并提供了详细的代码实现和遇到问题(如图片未显示)的解决方案。
异步&线程池 CompletableFuture 异步编排 实战应用 【终结篇】
|
3月前
|
设计模式 缓存 Java
谷粒商城笔记+踩坑(14)——异步和线程池
初始化线程的4种方式、线程池详解、异步编排 CompletableFuture
|
4月前
|
机器学习/深度学习 文字识别 前端开发
基于 Spring Boot 3.3 + OCR 实现图片转文字功能
【8月更文挑战第30天】在当今数字化信息时代,图像中的文字信息越来越重要。无论是文档扫描、名片识别,还是车辆牌照识别,OCR(Optical Character Recognition,光学字符识别)技术都发挥着关键作用。本文将围绕如何使用Spring Boot 3.3结合OCR技术,实现图片转文字的功能,分享工作学习中的技术干货。
252 2