使用环境
- SpringBoot+FastDfs+thumbnailator
- fdfs环境自己搞吧
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
- 项目地址:https://github.com/YunaiV/ruoyi-vue-pro
- 视频教程:https://doc.iocoder.cn/video/
thumbnailator
maven依赖:
<dependency>
<groupId>net.coobirdgroupId>
<artifactId>thumbnailatorartifactId>
<version>0.4.8version>
dependency>
工具类:
importnet.coobird.thumbnailator.Thumbnails;
importnet.coobird.thumbnailator.geometry.Positions;
importorg.springframework.stereotype.Component;
importjavax.imageio.ImageIO;
importjava.io.File;
importjava.io.IOException;
@Component
publicclassPictureUtil{
/**
*水印图片
*/
privatestaticFilemarkIco=null;
//开机静态加载水印图片
static{
try{
markIco=newFile(newFile("").getCanonicalPath()+"/icon.png");
LogUtil.info(PictureUtil.class,"水印图片加载"+(markIco.exists()?"成功":"失败"));
}catch(Exceptione){
}
}
/**
*加水印
*/
publicvoidphotoMark(FilesourceFile,FiletoFile)throwsIOException{
Thumbnails.of(sourceFile)
.size(600,450)//尺寸
.watermark(Positions.BOTTOM_CENTER/*水印位置:中央靠下*/,
ImageIO.read(markIco),0.7f/*质量,越大质量越高(1)*/)
//.outputQuality(0.8f)
.toFile(toFile);//保存为哪个文件
}
/**
*生成图片缩略图
*/
publicvoidphotoSmaller(FilesourceFile,FiletoFile)throwsIOException{
Thumbnails.of(sourceFile)
.size(200,150)//尺寸
//.watermark(Positions.CENTER,ImageIO.read(markIco),0.1f)
.outputQuality(0.4f)//缩略图质量
.toFile(toFile);
}
/**
*生成视频缩略图(这块还没用到呢)
*/
publicvoidphotoSmallerForVedio(FilesourceFile,FiletoFile)throwsIOException{
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 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
- 项目地址:https://github.com/YunaiV/yudao-cloud
- 视频教程:https://doc.iocoder.cn/video/
线程池
使用springboot线程池,方便易用,只需配置和加注解即可。
importorg.springframework.context.annotation.Bean;
importorg.springframework.context.annotation.Configuration;
importorg.springframework.core.task.TaskExecutor;
importorg.springframework.scheduling.annotation.EnableAsync;
importorg.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
importjava.util.concurrent.ThreadPoolExecutor;
@Configuration
@EnableAsync
publicclassPoolConfig{
@Bean//returnnewAsyncResult<>(res);
publicTaskExecutortaskExecutor(){
ThreadPoolTaskExecutorexecutor=newThreadPoolTaskExecutor();
executor.initialize();//设置核心线程数
executor.setCorePoolSize(4);//设置最大线程数
executor.setMaxPoolSize(32);//设置队列容量
executor.setQueueCapacity(512);//设置线程活跃时间(秒)
executor.setKeepAliveSeconds(60);//设置默认线程名称
executor.setThreadNamePrefix("ThreadPool-");//设置拒绝策略
executor.setRejectedExecutionHandler(newThreadPoolExecutor.CallerRunsPolicy());//等待所有任务结束后再关闭线程池
executor.setWaitForTasksToCompleteOnShutdown(true);
returnexecutor;
}
}
避坑知识点:配置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}")
publicRpushHousingPhotoMethod(
@ApiParam("SourceId")@PathVariableIntegerid,
@ApiParam("图片名称不约束,可不填则使用原名,可使用随机码或原名称,但必须带扩展名")@PathVariable(required=false)Stringname,
@RequestParamMultipartFilefile)throwsInterruptedException,ExecutionException,IOException{
StringfileName=file.getOriginalFilename();
Stringext=StringUtils.substring(fileName,fileName.lastIndexOf('.'),fileName.length());
FiletempPhoto=File.createTempFile(UUIDUtil.make32BitUUID(),ext);
file.transferTo(tempPhoto);//转储临时文件
service.pushPhoto(id,name,tempPhoto);
returnnewR();
}
业务代码里隐藏了一些项目相关的信息,就是某些名改了,嗯。
可以看到,使用StringUtils.substring(fileName, fileName.lastIndexOf(’.’),fileName.length());
这句代码,调用apache.common.lang3
工具类获取出了扩展名,因为扩展名对图片处理工具类有用,他通过扩展名识别图片格式,所以这个必须有,如代码,生成了一个使用随机码命名,但带有.png扩展名的临时文件,保存在默认临时路径以供处理。File.createTempFile(UUIDUtil.make32BitUUID(), ext);
是生成临时文件的方法,UUIDUtil也很简单,我贴出来吧,省着还要找
注意:controller类上需要标注注解@EnableAsync
/**
*生成一个32位无横杠的UUID
*/
publicsynchronizedstaticStringmake32BitUUID(){
returnUUID.randomUUID().toString().replace("-","");
}
避坑知识点:Spring使用MultipartFile
接收文件,但不能直接把MultipartFile
传下去处理,而是保存为临时文件,并不是多此一举。因为MultipartFile
也是临时文件,他的销毁时间是你这个Controller层方法return的时候。
如果不使用异步,是可以在调用的方法里去处理MultipartFile
文件的,但如果使用异步处理,肯定是这边线程还没处理完,那边Controller层已经return了,这个MultipartFile
就被删除了,于是你的异步线程就找不到这张图了。那还处理个啥,对吧。所以需要手动保存为自己创建的临时文件,再在线程中处理完把他删掉。
贴Service层Impl实现类代码
@Async
publicvoidpushHousingPhoto(Integerid,Stringname,Filefile)throwsInterruptedException,ExecutionException,IOException{
//存储FDFS表id
LongstartTime=System.currentTimeMillis();
Integer[]numb=fastDfsService.upLoadPhoto(StringUtils.isBlank(name)?file.getName():name,file).get();
SourcePhotosContextcontext=newSourcePhotosContext();
context.setSourceId(id);
context.setNumber(numb[0]);
context.setNumber2(numb[1]);
//保存图片关系
sourcePhotosContextService.insertNew(context);
LongendTime=System.currentTimeMillis();
LogUtil.info(this.getClass(),"source["+id+"]绑定图片["+name+"]成功,内部处理耗时["+(endTime-startTime)+"ms]");
//returnnewR();
}
这里的number和number2分别是带水印的原图和缩略图,context是个表,用来存图片和缩略图对应fdfs路径的,就不贴了。可见这个方法上带有注解@Async
所以整个方法会异步执行。
加水印处理写到fdfs的service里了,这样不算规范,可以不要学我:
@Override
publicFutureupLoadPhoto(StringfileName,MultipartFilefile)throwsIOException{
Stringext=StringUtils.substring(fileName,fileName.lastIndexOf('.'));
//创建临时文件
FilesourcePhoto=File.createTempFile(UUIDUtil.make32BitUUID(),ext);
file.transferTo(sourcePhoto);
returnupLoadPhoto(fileName,sourcePhoto);
}
@Override
publicFutureupLoadPhoto(StringfileName,FilesourcePhoto)throwsIOException{
Stringext=StringUtils.substring(fileName,fileName.lastIndexOf('.'));
//创建临时文件
FilemarkedPhoto=File.createTempFile(UUIDUtil.make32BitUUID(),ext);
FilesmallerPhoto=File.createTempFile(UUIDUtil.make32BitUUID(),ext);
//加水印缩图
pictureUtil.photoMark(sourcePhoto,markedPhoto);
pictureUtil.photoSmaller(markedPhoto,smallerPhoto);
//上传
IntegermarkedPhotoNumber=upLoadPhotoCtrl(fileName,markedPhoto);
IntegersmallerPhotoNumber=upLoadPhotoCtrl("mini_"+fileName,smallerPhoto);
//删除临时文件
sourcePhoto.delete();
markedPhoto.delete();
smallerPhoto.delete();
Integer[]res=newInteger[]{markedPhotoNumber,smallerPhotoNumber};
returnnewAsyncResult(res);
}
使用了方法重载,一个调用了另一个,方便以后处理MultipartFile
和File格式的图片都能使用,可以见到使用了Future
这个东西作为返回值,完全可以不这么做,正常返回就行。我懒得改了,这也是不断探索多线程处理图片的过程中,遗留下来的东西。
在service中fastDfsService.upLoadPhoto(StringUtils.isBlank(name) ? file.getName() : name, file).get()
这句就是得到了这个future的内容,可以去掉.get()
和Future<>
。可见这一个小小的异步功能,其实走过了很多弯路。future其实是异步调用方法时,从.get()
等待异步处理的结果,等待得到结果后获取内容并执行。现在使用spring线程池处理,已经不需要这样做了。
以上,希望你在实现这个功能时可以少走弯路。
附总体示意图:
审核编辑 :李倩
-
多线程
+关注
关注
0文章
278浏览量
19956 -
spring
+关注
关注
0文章
340浏览量
14343
原文标题:Spring 多线程异步上传图片、处理水印、缩略图
文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论