1 处理思路
1.1 引入maven
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.21</version>
</dependency>
<dependency>
<groupId>com.twelvemonkeys.imageio</groupId>
<artifactId>imageio-jpeg</artifactId>
<version>3.13.1</version>
</dependency>
<dependency>
<groupId>com.github.gotson</groupId>
<artifactId>webp-imageio</artifactId>
<version>0.2.2</version>
</dependency>
1.2 处理思路
配置压缩配置 -> 压缩接口 -> 实现压缩工具类 -> 业务中调用
2 处理代码
2.1 封装图片压缩
@Data
public class ImageCompressConfig {
private int maxWidth;
private int maxHeight;
private long targetSize;
private float quality;
private String format;
public static ImageCompressConfig of200KB() {
ImageCompressConfig config = new ImageCompressConfig();
config.maxWidth = 800;
config.maxHeight = 800;
config.targetSize = 200 * 1024;
config.quality = 0.85f;
config.format = "jpg";
return config;
}
public static ImageCompressConfig of200KBWebp() {
ImageCompressConfig config = new ImageCompressConfig();
config.setMaxWidth(800);
config.setMaxHeight(800);
config.setTargetSize(200 * 1024);
config.setQuality(0.85f);
config.setFormat("webp");
return config;
}
}
public interface ImageCompressor {
void compress(File src, File dest, ImageCompressConfig config) throws Exception;
}
------- 实现接口方法
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
@Slf4j
public class ThumbnailatorCompressor implements ImageCompressor {
@Override
public void compress(File src, File dest, ImageCompressConfig config) throws Exception {
log.info("开始压缩图片:{},目标格式:{}", src.getAbsolutePath(), config.getFormat());
String format = config.getFormat().toLowerCase();
if ("webp".equals(format)) {
compressWebP(src, dest, config);
} else {
compressWithThumbnailator(src, dest, config);
}
}
private void compressWebP(File src, File dest, ImageCompressConfig config) throws Exception {
log.info("使用 WebP 压缩: {}", src.getName());
File tempFile = File.createTempFile("temp_", ".jpg");
try {
float quality = config.getQuality();
int width = config.getMaxWidth();
int height = config.getMaxHeight();
Thumbnails.of(src)
.size(width, height)
.outputQuality(quality)
.outputFormat("jpg")
.toFile(tempFile);
while (tempFile.length() > config.getTargetSize() && quality > 0.3f) {
quality -= 0.05f;
Thumbnails.of(src)
.size(width, height)
.outputQuality(quality)
.outputFormat("jpg")
.toFile(tempFile);
}
while (tempFile.length() > config.getTargetSize() && width > 300) {
width -= 100;
height -= 100;
Thumbnails.of(src)
.useExifOrientation(true)
.size(width, height)
.outputQuality(quality)
.outputFormat("jpg")
.toFile(tempFile);
}
BufferedImage image = ImageIO.read(tempFile);
if (image == null) {
throw new IOException("无法读取临时图片");
}
boolean written = ImageIO.write(image, "webp", dest);
if (!written) {
written = ImageIO.write(image, "WEBP", dest);
if (!written) {
throw new IOException("WebP Writer 不可用,请检查 webp-imageio 依赖");
}
}
log.info("WebP 压缩完成: {} -> {} bytes", dest.getName(), dest.length());
} finally {
if (tempFile.exists()) {
tempFile.delete();
}
}
}
private void compressWithThumbnailator(File src, File dest, ImageCompressConfig config) throws Exception {
float quality = config.getQuality();
String format = config.getFormat();
Thumbnails.of(src)
.size(config.getMaxWidth(), config.getMaxHeight())
.outputQuality(quality)
.outputFormat(format)
.toFile(dest);
while (dest.length() > config.getTargetSize() && quality > 0.3f) {
quality -= 0.05f;
Thumbnails.of(src)
.size(config.getMaxWidth(), config.getMaxHeight())
.outputQuality(quality)
.outputFormat(format)
.toFile(dest);
}
int width = config.getMaxWidth();
int height = config.getMaxHeight();
while (dest.length() > config.getTargetSize() && width > 300) {
width -= 100;
height -= 100;
Thumbnails.of(src)
.useExifOrientation(true)
.size(width, height)
.outputQuality(quality)
.outputFormat(format)
.toFile(dest);
}
log.info("压缩完成: {} -> {} bytes", dest.getName(), dest.length());
}
}
@Service
@Slf4j
public class ImageCompressService {
private final ImageCompressor compressor;
public ImageCompressService() {
this.compressor = new ThumbnailatorCompressor();
}
public void compress(File src, File dest) throws Exception {
log.info("开始压缩图片:{}", src.getAbsolutePath());
compress(src, dest, ImageCompressConfig.of200KBWebp());
}
public void compress(File src, File dest, ImageCompressConfig config) throws Exception {
BufferedImage image = ImageIO.read(src);
if (image == null) {
throw new RuntimeException("非法图片文件");
}
compressor.compress(src, dest, config);
}
}
2.2 业务中使用封装图片压缩
@PreAuthorize("@ss.hasPermi('product:product:add')")
@Log(title = "商品", businessType = BusinessType.INSERT)
@PostMapping("/uploadImageZipByCode")
public AjaxResult uploadImageZipByCode(MultipartFile zipFile) throws Exception
{
Long tenantId = SecurityUtils.getTenantId();
if (zipFile == null || zipFile.isEmpty()) {
throw new ServiceException("请选择 zip 文件");
}
if (zipFile.getSize() > 100 * 1024 * 1024) {
throw new ServiceException("ZIP不能超过100MB");
}
String extension = FileUploadUtils.getExtension(zipFile);
System.out.println("文件扩展名: " + extension);
if (!FileUploadUtils.isAllowedExtension(extension, MimeTypeUtils.ZIP_EXTENSION)) {
throw new ServiceException("只能上传 zip 文件");
}
String basePath = FileUploadUtils.getDefaultBaseDir();
String tempDirPath = basePath + "/temp/" + System.currentTimeMillis();
File tempDir = new File(tempDirPath);
tempDir.mkdirs();
String filename = Optional.ofNullable(zipFile.getOriginalFilename()).orElse("upload.zip");
File tempZipFile = new File(tempDir, filename);
try {
log.info("开始上传文件: " + tempZipFile.getAbsolutePath());
zipFile.transferTo(tempZipFile);
log.info("文件保存成功: " + tempZipFile.getAbsolutePath());
unzipSafe(tempZipFile, tempDir);
log.info("文件解压成功: " + tempZipFile.getAbsolutePath());
File[] files = tempDir.listFiles(pathname ->
pathname.isFile() &&
(pathname.getName().endsWith(".jpg") ||
pathname.getName().endsWith(".png") ||
pathname.getName().endsWith(".jpeg") ||
pathname.getName().endsWith(".webp"))
);
if (files == null || files.length == 0) {
throw new ServiceException("ZIP 中未找到图片文件");
}
log.info("文件列表: " + files.length);
int total = files.length;
int success = 0;
int fail = 0;
List<ImageUploadError> failList = new ArrayList<>();
String realDirPath = basePath + "/productImage/" + DateUtils.monthPath();
File realDir = new File(realDirPath);
realDir.mkdirs();
for (File img : files) {
log.info("处理图片: " + img.getAbsolutePath());
String fullName = img.getName();
String productCode = fullName.substring(0, fullName.lastIndexOf("."));
ProductSkuVo sku = productSkuService.selectSkuBySkuCode(productCode, tenantId);
if (sku == null) {
fail++;
ImageUploadError error = new ImageUploadError();
error.setSkuCode(productCode);
error.setErrorMessage("商品条码不存在");
error.setFileName(fullName);
failList.add(error);
continue;
}
FileUploadUtils.deleteFile(sku.getSkuImage());
String newFileName = productCode + img.getName().substring(img.getName().lastIndexOf("."));
File dest = new File(realDir, newFileName);
try {
imageCompressService.compress(img, dest);
} catch (Exception e) {
log.info("图片压缩失败: " + e.getMessage());
fail++;
ImageUploadError error = new ImageUploadError();
error.setSkuCode(productCode);
error.setErrorMessage(e.getMessage());
error.setFileName(fullName);
failList.add(error);
continue;
}
String relativePath = "t_" + tenantId + "/productImage/" + DateUtils.monthPath() + "/" + newFileName;
String imageUrl = "/profile/" + relativePath;
productService.updateImage(sku, imageUrl, tenantId);
log.info("图片保存成功: " + imageUrl);
success++;
}
productServiceImpl.refreshCacheByTenant(tenantId);
AjaxResult ajax = AjaxResult.success();
ajax.put("total", total);
ajax.put("success", success);
ajax.put("fail", fail);
ajax.put("failList", failList);
return ajax;
} finally {
log.info("文件处理完成,清理临时路径: " + tempDir.getAbsolutePath());
FileUtil.del(tempDir);
}
}
private void unzipSafe(File zipFile, File targetDir) throws IOException {
String basePath = targetDir.getCanonicalPath();
int count = 0;
log.info("开始解压: " + zipFile.getAbsolutePath());
try (ZipFile zip = new ZipFile(zipFile, Charset.defaultCharset())) {
Enumeration<? extends ZipEntry> entries = zip.entries();
while (entries.hasMoreElements()) {
if (++count > 2000) {
throw new ServiceException("ZIP文件数量不允许超过2000个");
}
ZipEntry entry = entries.nextElement();
if (entry.getSize() > 5 * 1024 * 1024) {
continue;
}
File outFile = new File(targetDir, entry.getName());
String canonicalPath = outFile.getCanonicalPath();
if (!canonicalPath.startsWith(basePath + File.separator)) {
throw new ServiceException("非法ZIP文件(路径穿越攻击)");
}
if (entry.isDirectory()) {
outFile.mkdirs();
continue;
}
outFile.getParentFile().mkdirs();
try (InputStream in = zip.getInputStream(entry); OutputStream out = new FileOutputStream(outFile)) {
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
}
}
}
}