一、為什麼選擇 Thumbnailator?
|
對比項
|
自研 AWT 方案
|
Thumbnailator
|
|
代碼量
|
80+ 行
|
3 行核心邏輯 |
|
抗鋸齒
|
需手動設置 RenderingHint
|
✅ 默認高質量
|
|
裁剪邏輯
|
易出錯(座標計算)
|
✅ |
|
格式兼容
|
需處理 JPEG 質量
|
✅ 內置 |
|
維護成本
|
高(需持續修復邊緣 case)
|
✅ 社區維護,10 年穩定
|
|
許可證
|
Apache 2.0
|
✅ 商用免費
|
💡 結論:非圖像算法團隊,優先用成熟庫!
二、添加依賴(Maven)
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.20</version> <!-- 最新穩定版 -->
</dependency>
✅ 無其他依賴,僅 200KB JAR 包
三、企業級工具類封裝
import net.coobird.thumbnailator.Thumbnails;
import net.coobird.thumbnailator.geometry.Positions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* 基於 Thumbnailator 的企業級縮略圖工具類
* 特點:配置化、異常安全、日誌追蹤、支持流/文件雙模式
*/
public class EnterpriseThumbnailUtils {
private static final Logger log = LoggerFactory.getLogger(EnterpriseThumbnailUtils.class);
// 默認配置(可通過 Spring @ConfigurationProperties 注入)
private static float DEFAULT_QUALITY = 0.85f;
private static String DEFAULT_FORMAT = "jpg";
/**
* 生成等比縮放縮略圖(Fit 模式)
*/
public static void createThumbnailFit(
InputStream input,
OutputStream output,
int maxWidth,
int maxHeight) throws ThumbnailException {
try {
Thumbnails.of(input)
.size(maxWidth, maxHeight)
.keepAspectRatio(true)
.outputFormat(DEFAULT_FORMAT)
.outputQuality(DEFAULT_QUALITY)
.toOutputStream(output);
log.debug("縮略圖生成成功: {}x{}", maxWidth, maxHeight);
} catch (IOException e) {
log.error("縮略圖生成失敗", e);
throw new ThumbnailException("生成縮略圖時發生IO錯誤", e);
} catch (Exception e) {
log.error("縮略圖生成異常", e);
throw new ThumbnailException("縮略圖處理失敗", e);
}
}
/**
* 生成中心裁剪縮略圖(Crop 模式,適合頭像)
*/
public static void createThumbnailCrop(
File sourceFile,
File targetFile,
int width,
int height) throws ThumbnailException {
try {
Thumbnails.of(sourceFile)
.sourceRegion(Positions.CENTER, width * 2, height * 2) // 先放大區域避免拉伸
.size(width, height)
.crop(Positions.CENTER)
.outputFormat(DEFAULT_FORMAT)
.outputQuality(DEFAULT_QUALITY)
.toFile(targetFile);
log.info("裁剪縮略圖已保存: {}", targetFile.getAbsolutePath());
} catch (IOException e) {
log.error("裁剪縮略圖失敗: {} -> {}", sourceFile, targetFile, e);
throw new ThumbnailException("裁剪縮略圖失敗", e);
}
}
/**
* 添加水印的縮略圖(示例)
*/
public static void createThumbnailWithWatermark(
File source,
File target,
File watermark,
int size) throws ThumbnailException {
try {
Thumbnails.of(source)
.size(size, size)
.watermark(Positions.BOTTOM_RIGHT, ImageIO.read(watermark), 0.5f)
.outputQuality(DEFAULT_QUALITY)
.toFile(target);
} catch (Exception e) {
throw new ThumbnailException("添加水印失敗", e);
}
}
// 自定義異常,便於上層統一處理
public static class ThumbnailException extends Exception {
public ThumbnailException(String message, Throwable cause) {
super(message, cause);
}
}
}
四、單元測試(JUnit 5 + AssertJ)
class EnterpriseThumbnailUtilsTest {
@TempDir
Path tempDir;
@Test
void shouldGenerateFitThumbnail() throws Exception {
// Given
InputStream input = getClass().getResourceAsStream("/test.jpg");
Path output = tempDir.resolve("fit_thumb.jpg");
// When
try (OutputStream out = Files.newOutputStream(output)) {
EnterpriseThumbnailUtils.createThumbnailFit(input, out, 200, 200);
}
// Then
assertThat(output).exists().hasSizeGreaterThan(0);
BufferedImage img = ImageIO.read(output.toFile());
assertThat(img.getWidth()).isEqualTo(200);
assertThat(img.getHeight()).isLessThanOrEqualTo(200); // 等比縮放
}
@Test
void shouldThrowExceptionOnInvalidImage() {
assertThatThrownBy(() -> {
try (InputStream badInput = new ByteArrayInputStream("not an image".getBytes());
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
EnterpriseThumbnailUtils.createThumbnailFit(badInput, out, 100, 100);
}
}).isInstanceOf(EnterpriseThumbnailUtils.ThumbnailException.class);
}
}
五、在 Spring Boot 中的最佳實踐
1. 配置化(application.yml)
app:
thumbnail:
quality: 0.9
format: jpeg
2. 通過 @ConfigurationProperties 注入
@Component
@ConfigurationProperties(prefix = "app.thumbnail")
public class ThumbnailConfig {
private float quality = 0.85f;
private String format = "jpg";
// getter/setter
}
3. 在 Service 中調用
@Service
public class AvatarService {
public byte[] generateAvatarThumbnail(MultipartFile file) {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
EnterpriseThumbnailUtils.createThumbnailCrop(
file.getInputStream(), out, 100, 100
);
return out.toByteArray();
} catch (ThumbnailException e) {
throw new BusinessException("頭像處理失敗,請上傳有效圖片");
}
}
}