修复资源泄露问题并优化资源管理

- 添加 AutoCloseable 接口实现,支持自动资源释放
   - 修复 Graphics2D、ImageOutputStream 等资源未正确关闭的问题
   - 为 Spring Bean 添加 destroyMethod 配置
   - 添加定时任务线程池的正确关闭逻辑
   - FontCache 添加缓存大小限制防止内存泄露
   - 优化 StandardWordClickImageCaptchaGenerator 代码结构
This commit is contained in:
天爱有情
2026-01-14 17:09:59 +08:00
parent db0603a124
commit 29279e8c56
24 changed files with 762 additions and 187 deletions
@@ -2,16 +2,17 @@ package cloud.tianai.captcha.application;
import cloud.tianai.captcha.application.vo.ImageCaptchaVO;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.cache.StoreCacheKeyPrefix;
import cloud.tianai.captcha.common.AnyMap;
import cloud.tianai.captcha.common.constant.CaptchaTypeConstant;
import cloud.tianai.captcha.common.exception.ImageCaptchaException;
import cloud.tianai.captcha.common.response.ApiResponse;
import cloud.tianai.captcha.common.response.ApiResponseStatusConstant;
import cloud.tianai.captcha.common.util.CollectionUtils;
import cloud.tianai.captcha.common.util.ObjectUtils;
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
import cloud.tianai.captcha.generator.common.model.dto.GenerateParam;
import cloud.tianai.captcha.generator.common.model.dto.ImageCaptchaInfo;
import cloud.tianai.captcha.generator.common.model.dto.ParamKeyEnum;
import cloud.tianai.captcha.generator.impl.CacheImageCaptchaGenerator;
import cloud.tianai.captcha.interceptor.CaptchaInterceptor;
import cloud.tianai.captcha.interceptor.EmptyCaptchaInterceptor;
@@ -42,8 +43,6 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
private CacheStore cacheStore;
/** 验证码配置属性. */
private final ImageCaptchaProperties prop;
/** 缓存key 前缀处理器. */
private StoreCacheKeyPrefix storeCacheKeyPrefix;
/** 默认的过期时间. */
private long defaultExpire = 20000L;
@@ -53,10 +52,9 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
ImageCaptchaValidator imageCaptchaValidator,
CacheStore cacheStore,
ImageCaptchaProperties prop,
CaptchaInterceptor captchaInterceptor,
StoreCacheKeyPrefix storeCacheKeyPrefix) {
CaptchaInterceptor captchaInterceptor) {
this.prop = prop;
this.storeCacheKeyPrefix = null != storeCacheKeyPrefix? storeCacheKeyPrefix: StoreCacheKeyPrefix.prefixed(prop.getPrefix());
setImageCaptchaValidator(imageCaptchaValidator);
setCacheStore(cacheStore);
// 默认过期时间
@@ -99,9 +97,13 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
if (captchaResponse != null) {
return captchaResponse;
}
String id = generatorId(param);
ImageCaptchaInfo imageCaptchaInfo = getImageCaptchaGenerator().generateCaptchaImage(param);
captchaResponse = convertToCaptchaResponse(imageCaptchaInfo);
captchaResponse = convertToCaptchaResponse(id, imageCaptchaInfo);
afterGenerateCaptcha(imageCaptchaInfo, captchaResponse);
param.removeParam(ParamKeyEnum.ID);
return captchaResponse;
}
@@ -125,14 +127,14 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
}
public ApiResponse<ImageCaptchaVO> convertToCaptchaResponse(ImageCaptchaInfo imageCaptchaInfo) {
public ApiResponse<ImageCaptchaVO> convertToCaptchaResponse(String id, ImageCaptchaInfo imageCaptchaInfo) {
if (imageCaptchaInfo == null) {
// 要是生成失败
throw new ImageCaptchaException("生成验证码失败,验证码生成为空");
}
// 生成ID
String id = generatorId(imageCaptchaInfo);
ApiResponse<ImageCaptchaVO> response = beforeGenerateImageCaptchaValidData(imageCaptchaInfo);
ApiResponse<ImageCaptchaVO> response = beforeGenerateImageCaptchaValidData( imageCaptchaInfo);
if (response != null) {
return response;
}
@@ -209,8 +211,14 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
return null;
}
protected String generatorId(ImageCaptchaInfo imageCaptchaInfo) {
return imageCaptchaInfo.getType() + ID_SPLIT + UUID.randomUUID().toString().replace("-", "");
protected String generatorId(GenerateParam param) {
String id = param.getParam(ParamKeyEnum.ID);
if (ObjectUtils.isEmpty(id)) {
id = param.getType() + ID_SPLIT + UUID.randomUUID().toString().replace("-", "");
param.addParam(ParamKeyEnum.ID, id);
}
return id;
}
/**
@@ -239,8 +247,7 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
}
protected String getKey(String id) {
// 改为通过接口扩展
return storeCacheKeyPrefix.compute(id);
return prop.getPrefix().concat(":").concat(id);
}
@Override
@@ -292,6 +299,7 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
// ============== 一些模板方法 ================
private void afterGenerateCaptcha(ImageCaptchaInfo imageCaptchaInfo, ApiResponse<ImageCaptchaVO> captchaResponse) {
captchaInterceptor.afterGenerateCaptcha(captchaInterceptor.createContext(), imageCaptchaInfo.getType(), imageCaptchaInfo, captchaResponse);
}
@@ -315,4 +323,24 @@ public class DefaultImageCaptchaApplication implements ImageCaptchaApplication {
return captchaInterceptor.afterValid(captchaInterceptor.createContext(), getCaptchaTypeById(id), matchParam, validData, basicValid);
}
/**
* 销毁应用程序,释放资源
* 建议在不再使用或应用关闭时调用
*/
@Override
public void close() {
// 如果生成器是 CacheImageCaptchaGenerator,需要关闭其定时任务
if (captchaGenerator instanceof CacheImageCaptchaGenerator) {
((CacheImageCaptchaGenerator) captchaGenerator).destroy();
}
// 如果缓存存储是 AutoCloseable,关闭它
if (cacheStore instanceof AutoCloseable) {
try {
((AutoCloseable) cacheStore).close();
} catch (Exception e) {
log.warn("关闭 CacheStore 时出错", e);
}
}
}
}
@@ -114,4 +114,9 @@ public class FilterImageCaptchaApplication implements ImageCaptchaApplication {
public CacheStore getCacheStore() {
return target.getCacheStore();
}
@Override
public void close() throws Exception {
target.close();
}
}
@@ -17,7 +17,7 @@ import cloud.tianai.captcha.validator.common.model.dto.MatchParam;
* @Date 2020/5/29 8:33
* @Description 滑块验证码应用程序
*/
public interface ImageCaptchaApplication {
public interface ImageCaptchaApplication extends AutoCloseable{
/**
* 生成滑块验证码
@@ -3,6 +3,7 @@ package cloud.tianai.captcha.application;
import cloud.tianai.captcha.cache.CacheStore;
import cloud.tianai.captcha.cache.StoreCacheKeyPrefix;
import cloud.tianai.captcha.cache.impl.LocalCacheStore;
import cloud.tianai.captcha.common.util.CollectionUtils;
import cloud.tianai.captcha.generator.ImageCaptchaGenerator;
import cloud.tianai.captcha.generator.ImageTransform;
import cloud.tianai.captcha.generator.impl.MultiImageCaptchaGenerator;
@@ -16,6 +17,9 @@ import cloud.tianai.captcha.resource.impl.LocalMemoryResourceStore;
import cloud.tianai.captcha.validator.ImageCaptchaValidator;
import cloud.tianai.captcha.validator.impl.SimpleImageCaptchaValidator;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @Author: 天爱有情
* @date 2024/7/14 16:41
@@ -30,22 +34,23 @@ public class TACBuilder {
private ImageCaptchaProperties prop = new ImageCaptchaProperties();
private ResourceStore resourceStore;
private ImageTransform imageTransform;
private StoreCacheKeyPrefix cacheKeyPrefix;
// private List<FontWrapper> fontWrappers = new ArrayList<>();
private Map<String, Resource> resourceCache;
private Map<String, ResourceMap> templateCache;
public static TACBuilder builder() {
return TACBuilder.builder(new LocalMemoryResourceStore());
return TACBuilder.builder();
}
public static TACBuilder builder(ResourceStore resourceStore) {
TACBuilder builder = new TACBuilder(resourceStore);
builder.prop = new ImageCaptchaProperties();
return builder;
}
private TACBuilder(ResourceStore resourceStore) {
this.resourceStore = resourceStore;
}
public TACBuilder setResourceStore(ResourceStore resourceStore) {
this.resourceStore = resourceStore;
return this;
}
public TACBuilder addDefaultTemplate(String defaultPathPrefix) {
DefaultBuiltInResources defaultBuiltInResources = new DefaultBuiltInResources(defaultPathPrefix);
@@ -77,10 +82,6 @@ public class TACBuilder {
return this;
}
public TACBuilder setCacheKeyPrefix(StoreCacheKeyPrefix cacheKeyPrefix) {
this.cacheKeyPrefix = cacheKeyPrefix;
return this;
}
public TACBuilder addFont(Resource resource) {
this.addResource(FontCache.FONT_TYPE, resource);
return this;
@@ -118,16 +119,18 @@ public class TACBuilder {
public TACBuilder addResource(String captchaType, Resource imageResource) {
if (resourceStore instanceof CrudResourceStore) {
((CrudResourceStore) resourceStore).addResource(captchaType, imageResource);
}
// if (resourceStore instanceof CrudResourceStore) {
// ((CrudResourceStore) resourceStore).addResource(captchaType, imageResource);
// }
cacheResource(captchaType, imageResource);
return this;
}
public TACBuilder addTemplate(String captchaType, ResourceMap resourceMap) {
if (resourceStore instanceof CrudResourceStore) {
((CrudResourceStore) resourceStore).addTemplate(captchaType, resourceMap);
}
// if (resourceStore instanceof CrudResourceStore) {
// ((CrudResourceStore) resourceStore).addTemplate(captchaType, resourceMap);
// }
cacheTemplate(captchaType, resourceMap);
return this;
}
@@ -140,6 +143,20 @@ public class TACBuilder {
if (cacheStore == null) {
cacheStore = new LocalCacheStore();
}
if (resourceStore == null) {
resourceStore = new LocalMemoryResourceStore();
}
if (resourceStore instanceof CrudResourceStore) {
CrudResourceStore crudResourceStore = (CrudResourceStore) resourceStore;
if (!CollectionUtils.isEmpty(resourceCache)) {
resourceCache.forEach(crudResourceStore::addResource);
resourceCache = null;
}
if (!CollectionUtils.isEmpty(templateCache)) {
templateCache.forEach(crudResourceStore::addTemplate);
templateCache = null;
}
}
if (generator == null) {
ResourceProviders resourceProviders = new ResourceProviders();
DefaultImageCaptchaResourceManager resourceManager = new DefaultImageCaptchaResourceManager(resourceStore, resourceProviders);
@@ -155,7 +172,20 @@ public class TACBuilder {
interceptor = EmptyCaptchaInterceptor.INSTANCE;
}
// 增加前缀处理接口
DefaultImageCaptchaApplication application = new DefaultImageCaptchaApplication(generator, validator, cacheStore, prop, interceptor, cacheKeyPrefix);
DefaultImageCaptchaApplication application = new DefaultImageCaptchaApplication(generator, validator, cacheStore, prop, interceptor);
return application;
}
private void cacheResource(String captchaType, Resource imageResource) {
if (resourceCache == null) {
resourceCache = new LinkedHashMap<>(8);
}
resourceCache.put(captchaType, imageResource);
}
private void cacheTemplate(String captchaType, ResourceMap resourceMap) {
if (templateCache == null) {
templateCache = new LinkedHashMap<>(8);
}
templateCache.put(captchaType, resourceMap);
}
}
@@ -9,7 +9,7 @@ import java.util.concurrent.TimeUnit;
* @date 2022/3/2 14:35
* @Description 提取出用于缓存的接口
*/
public interface CacheStore {
public interface CacheStore extends AutoCloseable {
/**
* 读取缓存数据通过key
@@ -12,7 +12,6 @@ import java.util.stream.Collectors;
/**
* @Author: 天爱有情
* @date 2020/10/12 10:02
* @Description 给予本人以前写的 expiring-map(redis淘汰策略的java实现) 项目进行改造
*/
@Slf4j
@Accessors(chain = true)
@@ -198,6 +197,28 @@ public class ConCurrentExpiringMap<K, V> implements ExpiringMap<K, V> {
throw new IllegalArgumentException("timemap not impl entrySet.");
}
/**
* 销毁方法,关闭定时任务线程池
* 应该在不再使用此 Map 时调用,以防止线程泄露
*
* @since 0.0.3
*/
public void destroy() {
try {
scheduledExecutor.shutdown();
if (!scheduledExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
scheduledExecutor.shutdownNow();
if (!scheduledExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
log.warn("ConCurrentExpiringMap 定时任务线程池未能正常关闭");
}
}
} catch (InterruptedException e) {
scheduledExecutor.shutdownNow();
Thread.currentThread().interrupt();
log.warn("ConCurrentExpiringMap 定时任务线程池关闭时被中断", e);
}
}
/**
* 定时执行任务
*
@@ -63,4 +63,15 @@ public class LocalCacheStore implements CacheStore {
}
return null;
}
/**
* 关闭缓存存储,释放资源
* 建议在不再使用时调用,或在 Spring Bean 销毁时自动调用
*/
@Override
public void close() {
if (cache instanceof ConCurrentExpiringMap) {
((ConCurrentExpiringMap<?, ?>) cache).destroy();
}
}
}
@@ -5,28 +5,34 @@ import lombok.Data;
import lombok.NoArgsConstructor;
import java.awt.*;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class FontWrapper {
private Font font;
private Float currentFontTopCoef;
// private Font font;
// private Float currentFontTopCoef;
private Map<Float, Font> fontCache = new ConcurrentHashMap<>(4);
private Font baseFont;
public static final int DEFAULT_FONT_SIZE = 70;
public Font getFont() {
return getFont(DEFAULT_FONT_SIZE);
}
public Font getFont(float size) {
return fontCache.computeIfAbsent(size, k -> baseFont.deriveFont(Font.BOLD, size));
}
public FontWrapper(Font font) {
this(font, 70);
this.baseFont = font;
}
public FontWrapper(Font font, int fontSize) {
this.font = font;
this.font = font.deriveFont(Font.BOLD, fontSize);
}
public float getCurrentFontTopCoef() {
if (currentFontTopCoef != null) {
return currentFontTopCoef;
}
currentFontTopCoef = 0.14645833f * font.getSize() + 0.39583333f;
return currentFontTopCoef;
public float getFontTopCoef(Font font) {
return 0.14645833f * font.getSize() + 0.39583333f;
}
}
@@ -65,6 +65,9 @@ public class GenerateParam {
}
return param.remove(key);
}
public <T> Object removeParam(ParamKey<T> paramKey) {
return removeParam(paramKey.getKey());
}
public Object getOrDefault(String key, Object defaultValue) {
if (param == null) {
@@ -15,6 +15,10 @@ public class ParamKeyEnum<T> implements ParamKey<T> {
public static final ParamKey<Integer> CLICK_INTERFERENCE_COUNT = new ParamKeyEnum<>("interferenceCount");
/** 读取字体时,可指定字体TAG,可用于给不同的验证码指定不同的字体包.*/
public static final ParamKey<String> FONT_TAG = new ParamKeyEnum<>("fontTag");
/** 验证码ID,内部使用.*/
public static final ParamKey<String> ID = new ParamKeyEnum<>("_id");
private String key;
}
@@ -4,6 +4,7 @@ import lombok.SneakyThrows;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.geom.CubicCurve2D;
import java.awt.geom.QuadCurve2D;
@@ -387,24 +388,29 @@ public class CaptchaImageUtils {
int interferencePointNum) {
BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g = bufferedImage.createGraphics();
ThreadLocalRandom random = ThreadLocalRandom.current();
g.setFont(font);
char[] chars = data.toCharArray();
try {
ThreadLocalRandom random = ThreadLocalRandom.current();
g.setFont(font);
char[] chars = data.toCharArray();
for (int i = 0; i < chars.length; i++) {
g.setColor(Color.gray);
g.drawString(String.valueOf(chars[i]), startX + i * font.getSize(), startY);
for (int i = 0; i < chars.length; i++) {
g.setColor(Color.gray);
g.drawString(String.valueOf(chars[i]), startX + i * font.getSize(), startY);
}
// 干扰点
if (interferencePointNum > 0) {
drawOval(interferencePointNum, null, g, width, height, random);
}
if (interferencePointNum > 0) {
g.setStroke(new BasicStroke(1.2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
// 干扰线
drawBesselLine(interferenceLineNum, null, g, width, height, random);
}
return bufferedImage;
} finally {
// fixme #IDIG16
g.dispose();
}
// 干扰点
if (interferencePointNum > 0) {
drawOval(interferencePointNum, null, g, width, height, random);
}
if (interferencePointNum > 0) {
g.setStroke(new BasicStroke(1.2f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
// 干扰线
drawBesselLine(interferenceLineNum, null, g, width, height, random);
}
return bufferedImage;
}
@@ -520,5 +526,81 @@ public class CaptchaImageUtils {
return TYPE_PNG.equalsIgnoreCase(type);
}
}
/**
* 绘制水印
* @param bgImage 背景图片
* @param watermark 水印文字
* @param x x坐标起始位置
* @param y y坐标起始位置
* @param color 水印颜色
* @param font 水印字体
*/
public static void drawWatermark(BufferedImage bgImage, String watermark, int x, int y, Color color, Font font) {
// 参数校验
if (bgImage == null || watermark == null || watermark.isEmpty() || font == null) {
return;
}
// 获取Graphics2D对象
Graphics2D g2d = bgImage.createGraphics();
try {
// 设置渲染质量
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
g2d.setColor(color);
// 设置字体
g2d.setFont(font);
// 计算文字尺寸
FontMetrics fontMetrics = g2d.getFontMetrics();
int textWidth = fontMetrics.stringWidth(watermark);
int textHeight = fontMetrics.getHeight();
// 计算旋转角度(45度角倾斜)
double angle = Math.toRadians(45);
// 计算水印间距,确保铺满整个图片
int spacingX = textWidth + 50;
int spacingY = textHeight + 50;
// 获取图片尺寸
int imageWidth = bgImage.getWidth();
int imageHeight = bgImage.getHeight();
// 计算需要绘制的水印数量
int rows = (int) Math.ceil((double) imageHeight / spacingY) + 2;
int cols = (int) Math.ceil((double) imageWidth / spacingX) + 2;
// 绘制水印,铺满整个图片
for (int row = 0; row < rows; row++) {
for (int col = 0; col < cols; col++) {
// 计算当前水印位置
int currentX = col * spacingX + x;
int currentY = row * spacingY + y;
// 保存当前坐标系
AffineTransform originalTransform = g2d.getTransform();
try {
// 平移到当前位置
g2d.translate(currentX, currentY);
// 旋转文字
g2d.rotate(angle);
// 绘制文字
g2d.drawString(watermark, 0, fontMetrics.getAscent());
} finally {
// 恢复原始坐标系
g2d.setTransform(originalTransform);
}
}
}
} finally {
// 释放资源
g2d.dispose();
}
}
}
@@ -34,10 +34,22 @@ public class ImgWriter {
if (ObjectUtils.isEmpty(imageType)) {
imageType = CaptchaImageUtils.TYPE_JPG;
}
ImageOutputStream imageOutputStream = transformImageOutputStream(destImageStream);
final BufferedImage bufferedImage = CaptchaImageUtils.toBufferedImage(image, imageType);
final ImageWriter writer = getWriter(bufferedImage, imageType);
return write(bufferedImage, writer, imageOutputStream, quality);
ImageOutputStream imageOutputStream = null;
try {
imageOutputStream = transformImageOutputStream(destImageStream);
final BufferedImage bufferedImage = CaptchaImageUtils.toBufferedImage(image, imageType);
final ImageWriter writer = getWriter(bufferedImage, imageType);
return write(bufferedImage, writer, imageOutputStream, quality);
} finally {
// 关闭 ImageOutputStream 防止资源泄露
if (imageOutputStream != null) {
try {
imageOutputStream.close();
} catch (IOException e) {
// 忽略关闭异常
}
}
}
}
/**
@@ -212,4 +212,28 @@ public class CacheImageCaptchaGenerator implements ImageCaptchaGenerator {
target.setInterceptor(interceptor);
}
/**
* 销毁方法,关闭定时任务线程池并清理缓存
* 应该在不再使用此 Generator 时调用,以防止线程和内存泄露
*/
public void destroy() {
try {
scheduledExecutor.shutdown();
if (!scheduledExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
scheduledExecutor.shutdownNow();
if (!scheduledExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
log.warn("CacheImageCaptchaGenerator 定时任务线程池未能正常关闭");
}
}
} catch (InterruptedException e) {
scheduledExecutor.shutdownNow();
Thread.currentThread().interrupt();
log.warn("CacheImageCaptchaGenerator 定时任务线程池关闭时被中断", e);
}
// 清理缓存
queueMap.clear();
posMap.clear();
lastUpdateMap.clear();
}
}
@@ -113,11 +113,15 @@ public class StandardSliderImageCaptchaGenerator extends AbstractImageCaptchaGen
int type = fixedImage.getColorModel().getTransparency();
BufferedImage image = new BufferedImage(width, height, type);
Graphics2D graphics = image.createGraphics();
// 透明度
double alpha = ThreadLocalRandom.current().nextDouble(0.5, 0.8);
AlphaComposite alphaComposite = AlphaComposite.Src.derive((float) alpha);
graphics.setComposite(alphaComposite);
graphics.drawImage(fixedImage, 0, 0, width, height, null);
try {
// 透明度
double alpha = ThreadLocalRandom.current().nextDouble(0.5, 0.8);
AlphaComposite alphaComposite = AlphaComposite.Src.derive((float) alpha);
graphics.setComposite(alphaComposite);
graphics.drawImage(fixedImage, 0, 0, width, height, null);
} finally {
graphics.dispose();
}
return image;
}
@@ -35,11 +35,7 @@ public class StandardWordClickImageCaptchaGenerator extends AbstractClickImageCa
// @Getter
// @Setter
// protected List<FontWrapper> fonts = new ArrayList<>();
@Getter
@Setter
protected Integer clickImgWidth = 100;
@Getter
@Setter
protected Integer clickImgHeight = 100;
@Getter
@Setter
@@ -95,6 +91,33 @@ public class StandardWordClickImageCaptchaGenerator extends AbstractClickImageCa
return tipList;
}
@Override
public ClickImageCheckDefinition.ImgWrapper getClickImg(GenerateParam param, Resource tip, Color randomColor) {
if (randomColor == null) {
ThreadLocalRandom random = ThreadLocalRandom.current();
randomColor = CaptchaImageUtils.getRandomColor(random);
}
// 随机角度
int randomDeg = randomInt(0, 85);
// 缩放
double factor = 600d;
FontWrapper fontWrapper = randomFont(param);
Font font = fontWrapper.getFont((float) (FontWrapper.DEFAULT_FONT_SIZE * factor));
float currentFontTopCoef = fontWrapper.getFontTopCoef(font);
// 图片点击宽度
int clickImgWidth = (int) (font.getSize() * 1.428571428571429);
BufferedImage fontImage = CaptchaImageUtils.drawWordImg(randomColor,
tip.getData(),
font,
currentFontTopCoef,
clickImgWidth,
clickImgWidth,
randomDeg);
return new ClickImageCheckDefinition.ImgWrapper(fontImage, tip, randomColor);
}
@Override
protected void doInit() {
// if (CollectionUtils.isEmpty(fonts)) {
@@ -115,10 +138,12 @@ public class StandardWordClickImageCaptchaGenerator extends AbstractClickImageCa
throw new ImageCaptchaException("随机获取字体失败, resource中没有读到字体包, resource=" + resource);
}
public ClickImageCheckDefinition.ImgWrapper genTipImage(List<ClickImageCheckDefinition> imageCheckDefinitions, GenerateParam param) {
FontWrapper fontWrapper = randomFont(param);
Font font = fontWrapper.getFont();
float currentFontTopCoef = fontWrapper.getCurrentFontTopCoef();
float currentFontTopCoef = fontWrapper.getFontTopCoef(font);
String tips = imageCheckDefinitions.stream().map(c -> c.getTip().getData()).collect(Collectors.joining());
// 生成随机颜色
int fontWidth = tips.length() * font.getSize();
@@ -140,27 +165,6 @@ public class StandardWordClickImageCaptchaGenerator extends AbstractClickImageCa
// }
@Override
public ClickImageCheckDefinition.ImgWrapper getClickImg(GenerateParam param, Resource tip, Color randomColor) {
if (randomColor == null) {
ThreadLocalRandom random = ThreadLocalRandom.current();
randomColor = CaptchaImageUtils.getRandomColor(random);
}
// 随机角度
int randomDeg = randomInt(0, 85);
FontWrapper fontWrapper = randomFont(param);
Font font = fontWrapper.getFont();
float currentFontTopCoef = fontWrapper.getCurrentFontTopCoef();
BufferedImage fontImage = CaptchaImageUtils.drawWordImg(randomColor,
tip.getData(),
font,
currentFontTopCoef,
clickImgWidth,
clickImgHeight,
randomDeg);
return new ClickImageCheckDefinition.ImgWrapper(fontImage, tip, randomColor);
}
@Override
protected List<ClickImageCheckDefinition> filterAndSortClickImageCheckDefinition(CaptchaExchange captchaExchange, List<ClickImageCheckDefinition> allCheckDefinitionList) {
GenerateParam param = captchaExchange.getParam();
@@ -29,16 +29,24 @@ public class Base64ImageTransform implements ImageTransform {
return result;
}
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
if (CaptchaImageUtils.isPng(transformType) || CaptchaImageUtils.isJpeg(transformType)) {
// 如果是 jpg 或者 png图片的话 用hutool的生成
ImgWriter.write(bufferedImage, transformType, byteArrayOutputStream, -1);
} else {
ImageIO.write(bufferedImage, transformType, byteArrayOutputStream);
try {
if (CaptchaImageUtils.isPng(transformType) || CaptchaImageUtils.isJpeg(transformType)) {
// 如果是 jpg 或者 png图片的话 用hutool的生成
ImgWriter.write(bufferedImage, transformType, byteArrayOutputStream, -1);
} else {
ImageIO.write(bufferedImage, transformType, byteArrayOutputStream);
}
//转换成字节码
byte[] data = byteArrayOutputStream.toByteArray();
String base64 = Base64.getEncoder().encodeToString(data);
return "data:image/" + transformType + ";base64,".concat(base64);
} finally {
try {
byteArrayOutputStream.close();
} catch (IOException e) {
// 忽略关闭异常
}
}
//转换成字节码
byte[] data = byteArrayOutputStream.toByteArray();
String base64 = Base64.getEncoder().encodeToString(data);
return "data:image/" + transformType + ";base64,".concat(base64);
}
public String beforeTransform(BufferedImage bufferedImage, String formatType) {
@@ -3,8 +3,6 @@ package cloud.tianai.captcha.resource;
import cloud.tianai.captcha.generator.common.FontWrapper;
import cloud.tianai.captcha.resource.common.model.dto.Resource;
import cloud.tianai.captcha.resource.common.model.dto.ResourceMap;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import java.awt.*;
@@ -24,15 +22,14 @@ public class FontCache implements ResourceStore {
public static final String FONT_TYPE = "font";
/**
* 字体缓存最大数量,防止内存泄露
*/
private static final int MAX_FONT_CACHE_SIZE = 100;
private final Map<String, FontWrapper> fontMap = new ConcurrentHashMap<>();
private ResourceStore resourceStore;
private ImageCaptchaResourceManager resourceManager;
@Setter
@Getter
private int fontSize = 70;
public FontCache(ResourceStore resourceStore) {
this.resourceStore = resourceStore;
@@ -48,7 +45,7 @@ public class FontCache implements ResourceStore {
public FontWrapper getFont(Resource resource) {
try (InputStream stream = resourceManager.getResourceInputStream(resource)) {
Font font = Font.createFont(0, stream);
return new FontWrapper(font, fontSize);
return new FontWrapper(font);
} catch (FontFormatException | IOException e) {
throw new RuntimeException(e);
}
@@ -60,11 +57,25 @@ public class FontCache implements ResourceStore {
return resource.getType() + "_" + resource.getData();
}
/**
* 检查缓存大小,如果超过限制则清理
*/
private void checkCacheSize() {
if (fontMap.size() > MAX_FONT_CACHE_SIZE) {
log.warn("字体缓存超过限制大小: {},执行清理", fontMap.size());
// 简单清理策略:清空缓存
// 如果需要更精细的 LRU 策略,可以使用 LinkedHashMap 或第三方缓存库
fontMap.clear();
}
}
@Override
public List<Resource> randomGetResourceByTypeAndTag(String type, String tag, Integer quantity) {
List<Resource> resources = resourceStore.randomGetResourceByTypeAndTag(type, tag, quantity);
// 字体增强
if (FONT_TYPE.equalsIgnoreCase(type)) {
// 在添加新字体前检查缓存大小
checkCacheSize();
for (Resource resource : resources) {
FontWrapper fontWrapper = fontMap.computeIfAbsent(calcId(resource), v -> getFont(resource));
resource.setExtra(fontWrapper);
@@ -66,7 +66,7 @@ public class ApplicationTest {
ImageCaptchaProperties prop = new ImageCaptchaProperties();
// application 验证码封装, prop为所需的一些扩展参数
ImageCaptchaApplication application = new DefaultImageCaptchaApplication(generator, imageCaptchaValidator, cacheStore, prop, group, null);
ImageCaptchaApplication application = new DefaultImageCaptchaApplication(generator, imageCaptchaValidator, cacheStore, prop, group);
return application;
}
}