文章目录
- 0.前言
- 1.代码实现
- 2.压缩工具包的配置
0.前言
首先说明一下这个图片压缩为什么那么艰难,主要原因还是在于需求过于奇葩。比较奇葩的原因有如下几点:
1.图片是一个很大的文件,我长这么大还没见过这个大的文件。图下可以图片文件可以直奔100Mb了。其实还真有超过100Mb的图片,就不一一列举了。
2.第二个原因,图片大小不做限制。
3.由于压缩图片执行比较慢,在还没执行完的时候就已经有别的线程进行压缩了,特别是压缩的文件时可能消耗30s左右,此时其他用户线程也进入到压缩程序中执行,严重是还导致了内存溢出.。
解决问题的前置知识,可查看往期文章:
手写redis实现分布式锁详细教程,满足可续锁、可重入等分布式锁条件
Redisson分布式锁分析,可重入、可续锁(看门狗)
1.代码实现
首先为了避免同一个文件多次压缩,导致内存溢出问题,我们要确保一个大文件一天内只能压缩一次,而且压缩后的结果会放到本地磁盘暂存,避免反复压缩。
大文件一天内只能压缩一次如何实现呢?这将是这次问题解决方案的重中之重。加锁分布式锁,使用双检加锁方法,就在获取锁以后还要在判断本地磁盘上是否已经存有已经压缩的图片。
由此避免大图片文件多次重复压缩造成了内存溢出。但是文中的分布式锁并不是完善的,因为系统并发量并不是很高,所以做了一个简易的分布式锁。
public void getCompressImage(HttpServletResponse response, String attachId) throws IOException {if (StringUtils.isBlank(attachId)){log.info("附件id不能为空" );return;}String compressKey = RedisKeyConsts.CACHE_KEY_COMPRESS_BYTE + attachId;byte[] compressbyte = null;String filePath = "";filePath = (String) redisTemplate.opsForValue().get(compressKey);if (StringUtils.isNotBlank(filePath)){File fileCache = new File(filePath);if (fileCache.exists()){compressbyte = attachInfoService.convertFileToByteArray(fileCache);}}Attachment attach = this.attachmentService.getById(attachId);String suffix = FileUtil.getSuffix(attach.getFileName());if (compressbyte == null || compressbyte.length == 0) {AttachmentVO attachmentVO = new AttachmentVO();BeanUtil.copyProperties(attach, attachmentVO);byte[] fileBytes = null ;try {fileBytes = this.attachmentService.download(attachmentVO);}catch (Exception e){e.printStackTrace();}if (fileBytes == null){return;}String lockKey = "LOCK::"+compressKey;//自旋加锁while (!redisTemplate.opsForValue().setIfAbsent(lockKey , "1" , 70 , TimeUnit.SECONDS)){try {Thread.sleep(30);} catch (InterruptedException e) {e.printStackTrace();}}//如果已经压缩过了,就不用再压缩了,直接返回压缩好的文件直接取数filePath = (String) redisTemplate.opsForValue().get(compressKey);if (StringUtils.isNotBlank(filePath)){File fileCache = new File(filePath);if (fileCache.exists()){compressbyte = attachInfoService.convertFileToByteArray(fileCache);}}if (compressbyte == null || compressbyte.length == 0){long time1 = System.currentTimeMillis();//压缩文件String tempPath = com.haday.tp.attachment.core.util.FileUtil.concatPath(System.getProperty("java.io.tmpdir"), AttachConstants.TEMP_PATH, IdUtil.fastSimpleUUID(), attach.getFileName());File file = FileUtil.writeBytes(fileBytes, tempPath);CompressUtils.doWithPhoto(tempPath , fileBytes , 10 );long time2 = System.currentTimeMillis();log.info("文件压缩时间" + (time2 - time1) + "ms");redisTemplate.opsForValue().set(compressKey,tempPath );//转成字节型compressbyte = attachInfoService.convertFileToByteArray(file);//压缩时间没有确定时间,需要手动删除redisTemplate.delete(lockKey);}}response.setContentType("image/" + suffix);response.getOutputStream().write(compressbyte);response.getOutputStream().close();}
执行图片的压缩,压缩图片使用到com.sun.image.codec.jpeg包,java8以后变成了私有包了,所以要使用这个必须做相应的配置,下面会给出详细的配置。
package com.haday.media.utils;import com.sun.image.codec.jpeg.JPEGCodec;
import com.sun.image.codec.jpeg.JPEGImageEncoder;
import org.jcp.xml.dsig.internal.dom.Utils;import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.MemoryCacheImageInputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.Iterator;
public class CompressUtils {/*** 对图片进行原比例无损压缩,压缩后覆盖原图片*** @param path* @param rate 压缩比例*/public static void doWithPhoto(String path ,byte [] filebyte , int rate) {BufferedImage image = null;OutputStream os = null;try {image = readMemoryImage(filebyte);int width = image.getWidth() / rate ;int height = image.getHeight() / rate ;BufferedImage bfImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);bfImage.getGraphics().drawImage(image.getScaledInstance(width, height, Image.SCALE_SMOOTH), 0, 0, null);os = new FileOutputStream(path);JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(os);encoder.encode(bfImage);} catch (IOException e) {e.printStackTrace();} finally {if (os != null) {try {os.close();} catch (IOException e) {e.printStackTrace();}}}/*** 获取文件流* @param path* @return*/public static InputStream getUrlFile(String path){try {File file = new File(path);InputStream inputStream = new FileInputStream(file);return inputStream;} catch (Exception e) {e.printStackTrace();}return null;}public static final byte[] readBytes(InputStream in) throws IOException {if (null == in){throw new NullPointerException("the argument 'in' must not be null");}try {int buffSize = Math.max(in.available(), 1024 * 8);byte[] temp = new byte[buffSize];ByteArrayOutputStream out = new ByteArrayOutputStream(buffSize);int size = 0;while ((size = in.read(temp)) != -1) {out.write(temp, 0, size);}return out.toByteArray();} finally {in.close();}}/*** 从内存字节数组中读取图像** @param imgBytes* 未解码的图像数据* @return 返回 {@link BufferedImage}* @throws IOException* 当读写错误或不识别的格式时抛出*/public static final BufferedImage readMemoryImage(byte[] imgBytes) throws IOException {if (null == imgBytes || 0 == imgBytes.length){throw new NullPointerException("the argument 'imgBytes' must not be null or empty");}// 将字节数组转为InputStream,再转为MemoryCacheImageInputStreamImageInputStream imageInputstream = new MemoryCacheImageInputStream(new ByteArrayInputStream(imgBytes));try {// 获取所有能识别数据流格式的ImageReader对象Iterator<ImageReader> it = ImageIO.getImageReaders(imageInputstream);// 迭代器遍历尝试用ImageReader对象进行解码while (it.hasNext()) {ImageReader imageReader = it.next();// 设置解码器的输入流imageReader.setInput(imageInputstream, true, true);// 图像文件格式后缀String suffix = imageReader.getFormatName().trim().toLowerCase();// 图像宽度int width = imageReader.getWidth(0);// 图像高度int height = imageReader.getHeight(0);System.out.printf("format %s,%dx%d\n", suffix, width, height);try {// 解码成功返回BufferedImage对象// 0即为对第0张图像解码(gif格式会有多张图像),前面获取宽度高度的方法中的参数0也是同样的意思return imageReader.read(0, imageReader.getDefaultReadParam());} catch (Exception e) {imageReader.dispose();// 如果解码失败尝试用下一个ImageReader解码}}}catch (Exception e){e.printStackTrace();}finally {imageInputstream.close();}// 没有能识别此数据的图像ImageReader对象,抛出异常throw new IOException("unsupported image format");}
}
2.压缩工具包的配置
如果不进行下面配置,项目打包将会报错:程序包com.sun.image.codec.jpeg不存在。下面的的配置正是解决改问题的方法。
<build><plugins><!-- 指定JDK编译版本 --><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><source>1.8</source><target>1.8</target><encoding>UTF-8</encoding><compilerArguments><verbose /><bootclasspath>${JAVA_HOME}/jre/lib/rt.jar${path.separator}${JAVA_HOME}/jre/lib/jce.jar</bootclasspath></compilerArguments></configuration></plugin></plugins></build><!-- 环境 --><profiles><!-- 开发 --><profile><id>dev</id><activation><!--默认激活配置--><activeByDefault>true</activeByDefault></activation><properties><!--当前环境--><profile.name>dev</profile.name></properties></profile><!-- 测试 --><profile><id>test</id><activation><!--默认激活配置--><activeByDefault>false</activeByDefault></activation><properties><!--当前环境--><profile.name>test</profile.name><JAVA_HOME>/data/usr/repo/jdk1.8.0_202</JAVA_HOME></properties></profile><!-- 生产 --><profile><id>prod</id><properties><!--当前环境,生产环境为空--><profile.name>prod</profile.name><JAVA_HOME>/usr/nhip/jdk1.8.0_202</JAVA_HOME></properties></profile></profiles>