事件过程介绍
- 首先我们此次遇到的问题瓶颈;
- PDF页数较多,转换事件太久,内存溢出
- 文件较多,不适用于单一线程,过程太耗时
- 转换的图片较大,再次影响性能
解决方法
异步处理文件数据,等待处理结果,数据存储提升性能
话不多说直接上代码
srevice层 此时的sendMessage 是个异步类
public Boolean addJyKsTrainInformation(JyKsTrainInformation train, ServletWebRequest request) {
try {
train.setCreateName(LoginHelper.getNickName());
save(train);
if (CollectionUtils.isNotEmpty(train.getList())) {
List<JyKsFile> list = train.getList();
list.forEach(v -> {
v.setDataId(train.getId());
});
jyKsFileMapper.insertBatch(list);
//文件新增时需要异步处理pdf文件转成图片
sendMessage.createPdfToImageAsync(list, LoginHelper.getLoginUser(), request);
}
//判断资料分类是否不是管理员资料,如果是管理员资料这里暂时不进行通知,否则进行消息通知
JyKsGroupTitle groupTitle = jyKsGroupTitleMapper.selectById(train.getGroupTitleId());
if (groupTitle.getIsAdmin().equals(JySystemConstant.DELETE_FLG_FALSE)) {
List<SysUser> users = sysUserMapper.getSysUserListByRoleKey("XMJL");
String msg = train.getTitle() + "已发布,请尽快完成培训!";
for (SysUser user : users) {
sendMessage.sendMessage(msg, user.getUserId(), user.getUserName(), train.getGroupTitleName(), train.getGroupTitleId().toString(), msg);
}
}
return true;
} catch (Exception e) {
throw new GlobalException("数据新增失败,请联系管理员!");
}
}
sendMessage 此时我们在异步中进行了再次异步处理,将每个文件进行单一处理,当然pdfToImageAsync也是异步类
/**
* 异步将pdf转为图片,便于前端获取图片,缩短时长
* @param list
* @param loginUser
*/
@Async
public void createPdfToImageAsync(List<JyKsFile> list, LoginUser loginUser, ServletWebRequest request) {
RequestContextHolder.setRequestAttributes(request);
List<Future<JyKsFileToImage>> futures = new ArrayList<>();
List<Future<JyKsFile>> videos = new ArrayList<>();
boolean flg = false;
for (JyKsFile ksFile : list) {
if (ksFile.getFileSuffix().equals(".pdf")) {
Future<JyKsFileToImage> future = pdfToImageAsync.pdfToImagesAsync(ksFile, loginUser,request);
futures.add(future);
} else if (ksFile.getFileSuffix().equals(".mp4")){
Future<JyKsFile> future = pdfToImageAsync.videoToImage(ksFile, request);
videos.add(future);
}
}
if (CollectionUtils.isNotEmpty(videos)) {
List<JyKsFile> files = new ArrayList<>();
for (Future<JyKsFile> video : videos) {
try {
JyKsFile file = video.get();
if ("1".equals(file.getCode())) {
files.add(file);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
if (CollectionUtils.isNotEmpty(files)) {
jyKsFileMapper.updateBatchById(files);
}
}
if (CollectionUtils.isNotEmpty(futures)) {
List<JyKsFileToImage> imageList = new ArrayList<>();
for (Future<JyKsFileToImage> future : futures) {
try {
JyKsFileToImage image = future.get();
if ("1".equals(image.getCode())) {
imageList.add(image);
}
} catch (Exception e) {
log.error(e.getMessage(), e);
}
}
if (CollectionUtils.isNotEmpty(imageList)) {
jyKsFileToImageMapper.insertBatch(imageList);
}
}
}
PdfToImageAsync
import cn.hutool.core.lang.Snowflake;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson.JSONArray;
import com.ruoyi.common.cache.NotResourceCache;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.VideoUtils;
import com.ruoyi.jinyu.entity.JyKsFile;
import com.ruoyi.jinyu.entity.JyKsFileToImage;
import com.ruoyi.oss.entity.SysOss;
import com.ruoyi.system.service.ISysOssService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.tools.imageio.ImageIOUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockMultipartFile;
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.AsyncResult;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.multipart.MultipartFile;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
/**
* @Description
* @Author GGBond
* @Date 2023-09-14 16:48
*/
@Slf4j
@Component
public class PdfToImageAsync {
@Autowired
private ISysOssService ossService;
@Async
public Future<JyKsFileToImage> pdfToImagesAsync(JyKsFile ksFile, LoginUser loginUser, ServletWebRequest request) {
RequestContextHolder.setRequestAttributes(request);
JyKsFileToImage toImage = new JyKsFileToImage();
toImage.setFileId(ksFile.getId());
toImage.setCreateBy(loginUser.getNickname());
toImage.setUpdateBy(loginUser.getNickname());
InputStream inputStream = null;
try {
// 当作一个URL来装载文件
URL url = new URL(ksFile.getPath());
URLConnection con = url.openConnection();
con.setConnectTimeout(3 * 1000);
inputStream = con.getInputStream();
PDDocument doc = PDDocument.load(inputStream);
doc.setResourceCache(new NotResourceCache());
int pageCount = doc.getNumberOfPages();
PDFRenderer pdfRenderer = new PDFRenderer(doc);
String imageFilePath;
List<String> lists = new ArrayList<>();
Snowflake snowflake = IdUtil.createSnowflake(1,1);
long id = snowflake.nextId();
for (int pageIndex = 0; pageIndex < pageCount; pageIndex++) {
imageFilePath = id + pageIndex + ".png";
File file = new File(imageFilePath);
//需要注意的是dpi是像素,参数越大生成图片分辨率越高,转换时间也就越长
BufferedImage image = pdfRenderer.renderImageWithDPI(pageIndex, 105, ImageType.RGB);
ImageIOUtil.writeImage(image, imageFilePath,105);
MultipartFile multipartFile = new MockMultipartFile(String.valueOf(pageIndex), imageFilePath, null, Files.newInputStream(file.toPath()));
SysOss sysOss = ossService.upload(multipartFile);
file.delete();
lists.add(sysOss.getUrl());
}
// 关闭文档
doc.close();
toImage.setImages(JSONArray.toJSONString(lists));
toImage.setCode("1");
return new AsyncResult<>(toImage);
} catch (IOException e) {
log.error("pdf转换jpg图片异常!", e);
} finally {
try {
inputStream.close();
} catch (IOException e) {
//do nothing
}
}
toImage.setCode("2");
return new AsyncResult<>(toImage);
}
@Async
public Future<JyKsFile> videoToImage(JyKsFile ksFile,ServletWebRequest request) {
RequestContextHolder.setRequestAttributes(request);
File file = null;
Snowflake snowflake = IdUtil.createSnowflake(1,1);
long id = snowflake.nextId();
try {
//如果是视频时,需要截图,用来做封面图片
file = new File(id + "temp.mp4");
if (!file.exists()) {
file.createNewFile();
}
FileUtils.copyURLToFile(new URL(ksFile.getPath()), file);
MultipartFile multipartFile = VideoUtils.generateCover(file, 1);
SysOss sysOss = ossService.upload(multipartFile);
ksFile.setVideoPath(sysOss.getUrl());
ksFile.setCode("1");
return new AsyncResult<>(ksFile);
} catch (Exception e) {
log.error(e.getMessage(), e);
} finally {
if (null != file) {
file.delete();
}
}
ksFile.setCode("2");
return new AsyncResult<>(ksFile);
}
}
事件注意点
- 1、ServletWebRequest 是什么,我们这里采用了satoken + jwt 方式进行用户登录,当我们使用@Async时,后续的方法是无法在异步时获取我们的用户信息的,RequestContextHolder.setRequestAttributes(request);这里我们将每个异步线程进行请求头绑定,后续就可以拿到用户信息了
- 2、我们这里采用异步线程池,控制线程数量
- 3、Future 我们这里为什么采用Future,首先根据自己的业务,我们这里将图片存入到oss系统进行保存,所以我们需要等待异步结束后get()获取执行结果的url在更新到我们的业务表中;
- 4、我们读取PDF时需要注意的是 renderImageWithDPI()方法,第二个参数越大生成图片分辨率越高,转换时间也就越长,如果时间还是很久建议再调整下这里的参数,满足正常看的功能就行
异步线程池配置
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
/**
* 原生(Spring)异步任务线程池装配类,实现AsyncConfigurer重写他的两个方法,这样在使用默认的
* 线程池的时候就会使用自己重写的
* @Description
* @Author GGBond
* @Date 2023-09-14 16:48
*/
@Slf4j
@Configuration
@ComponentScan(basePackages = {"当前文件的包路径"})
@EnableAsync
public class ThreadConfig implements AsyncConfigurer {
private int core = 20;
private int max = 200;
private int capacity = 500;
private int alive = 60;
@Override
@Bean
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
// 核心线程数
taskExecutor.setCorePoolSize(core);
// 最大线程数
taskExecutor.setMaxPoolSize(max);
// 队列最大长度
taskExecutor.setQueueCapacity(capacity);
// 线程池维护线程所允许的空闲时间(单位秒)
taskExecutor.setKeepAliveSeconds(alive);
// 线程池对拒绝任务(无线程可用)的处理策略 ThreadPoolExecutor.CallerRunsPolicy策略 ,调用者的线程会执行该任务,如果执行器已关闭,则丢弃.
taskExecutor.setRejectedExecutionHandler((r, executor) -> {
// 如果出现了任务数量过多,后续考虑增加队列容量,或者增加线程数量等
log.error("线程池等待任务数量过多,该任务已被丢弃!");
});
taskExecutor.initialize();
return taskExecutor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
log.warn("Get in AsyncUncaughtExceptionHandler !");
return null;
}
}
结语
编写不易,大家多点点赞,支持下,感谢!!!