docs 补充项目注释

This commit is contained in:
AprilWind
2026-06-01 13:58:13 +08:00
parent 107d3326b4
commit e49f3b2260
94 changed files with 2328 additions and 68 deletions
@@ -14,7 +14,7 @@ import java.util.concurrent.*;
* 线程池配置
*
* @author Lion Li
**/
*/
@Slf4j
@AutoConfiguration
public class ThreadPoolConfig {
@@ -20,6 +20,9 @@ import java.util.concurrent.TimeUnit;
* @author ruoyi
*/
public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
/**
* 日期解析格式集合。
*/
private static final String[] PARSE_PATTERNS = {
"yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM",
"yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM",
@@ -6,25 +6,56 @@ package org.dromara.common.core.utils.file;
* @author ruoyi
*/
public class MimeTypeUtils {
/**
* PNG 图片 MIME 类型。
*/
public static final String IMAGE_PNG = "image/png";
/**
* JPG 图片 MIME 类型。
*/
public static final String IMAGE_JPG = "image/jpg";
/**
* JPEG 图片 MIME 类型。
*/
public static final String IMAGE_JPEG = "image/jpeg";
/**
* BMP 图片 MIME 类型。
*/
public static final String IMAGE_BMP = "image/bmp";
/**
* GIF 图片 MIME 类型。
*/
public static final String IMAGE_GIF = "image/gif";
/**
* 图片扩展名集合。
*/
public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"};
/**
* Flash 扩展名集合。
*/
public static final String[] FLASH_EXTENSION = {"swf", "flv"};
/**
* 媒体扩展名集合。
*/
public static final String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg",
"asf", "rm", "rmvb"};
/**
* 视频扩展名集合。
*/
public static final String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"};
/**
* 默认允许上传扩展名集合。
*/
public static final String[] DEFAULT_ALLOWED_EXTENSION = {
// 图片
"bmp", "gif", "jpg", "jpeg", "png",
@@ -16,8 +16,16 @@ import java.util.Set;
*/
public class EnumPatternValidator implements ConstraintValidator<EnumPattern, String> {
/**
* 枚举允许值集合。
*/
private final Set<String> values = new HashSet<>();
/**
* 初始化枚举允许值集合。
*
* @param annotation 枚举校验注解
*/
@Override
public void initialize(EnumPattern annotation) {
ConstraintValidator.super.initialize(annotation);
@@ -33,6 +41,13 @@ public class EnumPatternValidator implements ConstraintValidator<EnumPattern, St
}
}
/**
* 校验字符串是否在枚举允许值集合内。
*
* @param value 待校验值
* @param constraintValidatorContext 校验上下文
* @return true 校验通过 false 校验失败
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (StringUtils.isBlank(value)) {
@@ -13,6 +13,13 @@ import org.dromara.common.core.utils.StringUtils;
*/
public class XssValidator implements ConstraintValidator<Xss, String> {
/**
* 校验字符串是否包含 HTML 标签。
*
* @param value 待校验值
* @param constraintValidatorContext 校验上下文
* @return true 校验通过 false 包含 HTML 标签
*/
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
if (StringUtils.isBlank(value)) {
@@ -12,12 +12,23 @@ import org.springframework.core.env.ConfigurableEnvironment;
*/
public class ActuatorEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
/**
* 根据 easy-es 开关同步设置 Elasticsearch 健康检查开关。
*
* @param environment Spring 环境配置
* @param application Spring 应用实例
*/
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
String enable = environment.getProperty("easy-es.enable", "false");
System.setProperty("management.health.elasticsearch.enabled", enable);
}
/**
* 返回环境后处理器执行顺序。
*
* @return 最高优先级
*/
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
@@ -56,7 +56,7 @@ public class EncryptorAutoConfiguration {
/**
* 创建加密字段处理器。
*
* @param encryptorManager 加解密管理器
* @param encryptorManager 加解密管理器
* @param encryptContextFactory 加密上下文工厂
* @return 加密字段处理器
*/
@@ -87,6 +87,11 @@ public class EncryptorAutoConfiguration {
return new MybatisDecryptInterceptor(encryptedFieldProcessor);
}
/**
* 校验字段加解密配置完整性。
*
* @param properties 字段加解密配置
*/
private void validateEncryptorProperties(EncryptorProperties properties) {
AlgorithmType algorithm = properties.getAlgorithm();
if (algorithm == AlgorithmType.AES || algorithm == AlgorithmType.SM4) {
@@ -102,5 +107,3 @@ public class EncryptorAutoConfiguration {
}
}
@@ -23,7 +23,7 @@ public class EncryptedFieldProcessor {
* 构造加密字段处理器。
*
* @param encryptorManager 加解密管理器
* @param contextFactory 加密上下文工厂
* @param contextFactory 加密上下文工厂
*/
public EncryptedFieldProcessor(EncryptorManager encryptorManager, EncryptContextFactory contextFactory) {
this.encryptorManager = encryptorManager;
@@ -58,6 +58,13 @@ public class EncryptedFieldProcessor {
field.set(target, encryptorManager.decrypt(value, contextFactory.create(field))));
}
/**
* 递归处理对象、集合或 Map 中声明加密注解的字段。
*
* @param sourceObject 待处理对象
* @param visited 已访问对象集合
* @param fieldHandler 字段处理回调
*/
private void handle(Object sourceObject, Set<Object> visited, FieldHandler fieldHandler) {
if (ObjectUtil.isNull(sourceObject) || sourceObject instanceof String || visited.contains(sourceObject)) {
return;
@@ -100,8 +107,8 @@ public class EncryptedFieldProcessor {
* 处理单个加密字段。
*
* @param target 字段所属对象
* @param field 加密字段
* @param value 字段原始字符串值
* @param field 加密字段
* @param value 字段原始字符串值
* @throws IllegalAccessException 字段访问失败时抛出
*/
void handle(Object target, Field field, String value) throws IllegalAccessException;
@@ -11,6 +11,11 @@ import org.dromara.common.encrypt.core.IEncryptor;
*/
public abstract class AbstractEncryptor implements IEncryptor {
/**
* 初始化加密执行者。
*
* @param context 加密上下文
*/
public AbstractEncryptor(EncryptContext context) {
// 用户配置校验与配置注入
}
@@ -13,6 +13,11 @@ import org.dromara.common.encrypt.utils.EncryptUtils;
*/
public class Base64Encryptor extends AbstractEncryptor {
/**
* 初始化 Base64 加密执行者。
*
* @param context 加密上下文
*/
public Base64Encryptor(EncryptContext context) {
super(context);
}
@@ -17,6 +17,11 @@ public class RsaEncryptor extends AbstractEncryptor {
private final EncryptContext context;
/**
* 构造 RSA 加密器。
*
* @param context 加密上下文
*/
public RsaEncryptor(EncryptContext context) {
super(context);
String privateKey = context.getPrivateKey();
@@ -53,7 +58,7 @@ public class RsaEncryptor extends AbstractEncryptor {
/**
* 解密
*
* @param value 待加密字符串
* @param value 待加密字符串
*/
@Override
public String decrypt(String value) {
@@ -16,6 +16,11 @@ public class Sm2Encryptor extends AbstractEncryptor {
private final EncryptContext context;
/**
* 构造 SM2 加密器。
*
* @param context 加密上下文
*/
public Sm2Encryptor(EncryptContext context) {
super(context);
String privateKey = context.getPrivateKey();
@@ -52,7 +57,7 @@ public class Sm2Encryptor extends AbstractEncryptor {
/**
* 解密
*
* @param value 待加密字符串
* @param value 待加密字符串
*/
@Override
public String decrypt(String value) {
@@ -15,6 +15,11 @@ public class Sm4Encryptor extends AbstractEncryptor {
private final EncryptContext context;
/**
* 构造 SM4 加密器。
*
* @param context 加密上下文
*/
public Sm4Encryptor(EncryptContext context) {
super(context);
this.context = context;
@@ -46,7 +51,7 @@ public class Sm4Encryptor extends AbstractEncryptor {
/**
* 解密
*
* @param value 待加密字符串
* @param value 待加密字符串
*/
@Override
public String decrypt(String value) {
@@ -32,9 +32,9 @@ public class CryptoFilter implements Filter {
/**
* 构造加解密过滤器。
*
* @param properties API 解密配置
* @param properties API 解密配置
* @param requestMappingHandlerMapping 请求映射处理器
* @param handlerExceptionResolver 异常处理器
* @param handlerExceptionResolver 异常处理器
*/
public CryptoFilter(ApiDecryptProperties properties,
RequestMappingHandlerMapping requestMappingHandlerMapping,
@@ -46,6 +46,15 @@ public class CryptoFilter implements Filter {
EncryptUtils.validateRsaPrivateKey(properties.getPrivateKey());
}
/**
* 根据接口注解与请求头执行请求解密和响应加密。
*
* @param request 原始请求
* @param response 原始响应
* @param chain 过滤器链
* @throws IOException IO 异常
* @throws ServletException Servlet 异常
*/
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest servletRequest = (HttpServletRequest) request;
@@ -96,6 +105,9 @@ public class CryptoFilter implements Filter {
/**
* 获取 ApiEncrypt 注解
*
* @param servletRequest 当前请求
* @return API 加密注解
*/
private ApiEncrypt getApiEncryptAnnotation(HttpServletRequest servletRequest) {
// 获取注解
@@ -116,6 +128,9 @@ public class CryptoFilter implements Filter {
return null;
}
/**
* 销毁过滤器。
*/
@Override
public void destroy() {
}
@@ -23,6 +23,9 @@ import java.nio.charset.StandardCharsets;
*/
public class DecryptRequestBodyWrapper extends HttpServletRequestWrapper {
/**
* 请求体字节数据。
*/
private final byte[] body;
/**
@@ -48,6 +51,11 @@ public class DecryptRequestBodyWrapper extends HttpServletRequestWrapper {
body = decryptBody.getBytes(StandardCharsets.UTF_8);
}
/**
* 基于解密后的请求体创建字符读取器。
*
* @return 字符读取器
*/
@Override
public BufferedReader getReader() {
Charset charset = Charset.forName(getCharacterEncoding());
@@ -55,46 +63,91 @@ public class DecryptRequestBodyWrapper extends HttpServletRequestWrapper {
}
/**
* 返回解密后请求体长度。
*
* @return 请求体长度
*/
@Override
public int getContentLength() {
return body.length;
}
/**
* 返回解密后请求体长度。
*
* @return 请求体长度
*/
@Override
public long getContentLengthLong() {
return body.length;
}
/**
* 返回解密后的请求体类型。
*
* @return JSON 内容类型
*/
@Override
public String getContentType() {
return MediaType.APPLICATION_JSON_VALUE;
}
/**
* 返回基于解密请求体的输入流。
*
* @return 解密请求体输入流
*/
@Override
public ServletInputStream getInputStream() {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
/**
* 读取解密请求体下一个字节。
*
* @return 下一个字节
*/
@Override
public int read() {
return bais.read();
}
/**
* 返回解密请求体剩余可读字节数。
*
* @return 剩余字节数
*/
@Override
public int available() {
return bais.available();
}
/**
* 判断解密请求体是否读取完毕。
*
* @return 是否读取完毕
*/
@Override
public boolean isFinished() {
return bais.available() == 0;
}
/**
* 判断解密请求体输入流是否可读。
*
* @return 固定为 true
*/
@Override
public boolean isReady() {
return true;
}
/**
* 设置异步读取监听器。
*
* @param readListener 读取监听器
*/
@Override
public void setReadListener(ReadListener readListener) {
@@ -6,7 +6,10 @@ import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpServletResponseWrapper;
import org.dromara.common.encrypt.utils.EncryptUtils;
import java.io.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
@@ -39,6 +42,11 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
this.charset = resolveCharset(response);
}
/**
* 返回缓存响应内容的字符输出器。
*
* @return 字符输出器
*/
@Override
public PrintWriter getWriter() {
if (printWriter == null) {
@@ -48,6 +56,11 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
return printWriter;
}
/**
* 刷新缓存的响应输出流和字符输出器。
*
* @throws IOException IO 异常
*/
@Override
public void flushBuffer() throws IOException {
if (servletOutputStream != null) {
@@ -58,11 +71,17 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
}
}
/**
* 重置已缓存的响应内容。
*/
@Override
public void reset() {
byteArrayOutputStream.reset();
}
/**
* 重置已缓存的响应缓冲区。
*/
@Override
public void resetBuffer() {
byteArrayOutputStream.reset();
@@ -122,29 +141,64 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
return encryptContent;
}
/**
* 返回缓存响应内容的二进制输出流。
*
* @return 响应输出流
*/
@Override
public ServletOutputStream getOutputStream() throws IOException {
return new ServletOutputStream() {
/**
* 判断响应输出流是否可写。
*
* @return 固定为 true
*/
@Override
public boolean isReady() {
return true;
}
/**
* 设置异步写入监听器。
*
* @param writeListener 写入监听器
*/
@Override
public void setWriteListener(WriteListener writeListener) {
}
/**
* 写入单个字节到响应缓存。
*
* @param b 待写入字节
* @throws IOException IO 异常
*/
@Override
public void write(int b) throws IOException {
byteArrayOutputStream.write(b);
}
/**
* 写入字节数组到响应缓存。
*
* @param b 待写入字节数组
* @throws IOException IO 异常
*/
@Override
public void write(byte[] b) throws IOException {
byteArrayOutputStream.write(b);
}
/**
* 写入字节数组片段到响应缓存。
*
* @param b 待写入字节数组
* @param off 起始偏移量
* @param len 写入长度
* @throws IOException IO 异常
*/
@Override
public void write(byte[] b, int off, int len) throws IOException {
byteArrayOutputStream.write(b, off, len);
@@ -152,6 +206,12 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
};
}
/**
* 解析响应字符集,未设置时默认使用 UTF-8。
*
* @param response 原始响应
* @return 响应字符集
*/
private Charset resolveCharset(HttpServletResponse response) {
String characterEncoding = response.getCharacterEncoding();
if (characterEncoding == null) {
@@ -160,6 +220,11 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
return Charset.forName(characterEncoding);
}
/**
* 生成响应内容 AES 加密密钥。
*
* @return AES 密钥
*/
private String generateAesPassword() {
byte[] bytes = new byte[24];
SECURE_RANDOM.nextBytes(bytes);
@@ -23,6 +23,13 @@ public class MybatisDecryptInterceptor implements Interceptor {
private final EncryptedFieldProcessor encryptedFieldProcessor;
/**
* 解密 MyBatis 查询结果中的加密字段。
*
* @param invocation 拦截调用信息
* @return 查询结果
* @throws Throwable 拦截处理异常
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取执行mysql执行结果
@@ -34,11 +41,22 @@ public class MybatisDecryptInterceptor implements Interceptor {
return result;
}
/**
* 包装 MyBatis 目标对象。
*
* @param target 目标对象
* @return 包装后的对象
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 设置插件属性。
*
* @param properties 插件属性
*/
@Override
public void setProperties(Properties properties) {
@@ -25,6 +25,13 @@ public class MybatisEncryptInterceptor implements Interceptor {
private final EncryptedFieldProcessor encryptedFieldProcessor;
/**
* 加密 MyBatis 入参中的加密字段,并在执行后恢复原始值。
*
* @param invocation 拦截调用信息
* @return MyBatis 执行结果
* @throws Throwable 拦截处理异常
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
List<EncryptedFieldProcessor.FieldSnapshot> snapshots = List.of();
@@ -44,11 +51,22 @@ public class MybatisEncryptInterceptor implements Interceptor {
}
}
/**
* 包装 MyBatis 目标对象。
*
* @param target 目标对象
* @return 包装后的对象
*/
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
/**
* 设置插件属性。
*
* @param properties 插件属性
*/
@Override
public void setProperties(Properties properties) {
}
@@ -381,6 +381,11 @@ public class EncryptUtils {
}
}
/**
* 校验 RSA 密钥长度是否满足最低安全要求。
*
* @param rsaKey RSA 密钥
*/
private static void validateRsaKeySize(RSAKey rsaKey) {
int keySize = rsaKey.getModulus().bitLength();
if (keySize < MIN_RSA_KEY_SIZE) {
@@ -21,21 +21,47 @@ import java.math.BigDecimal;
@Slf4j
public class ExcelBigNumberConvert implements Converter<Long> {
/**
* 支持的 Java 类型。
*
* @return Long 类型
*/
@Override
public Class<Long> supportJavaTypeKey() {
return Long.class;
}
/**
* 支持的 Excel 单元格类型。
*
* @return 默认支持全部类型
*/
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return null;
}
/**
* 将 Excel 单元格数据转换为 Long。
*
* @param cellData 单元格数据
* @param contentProperty 内容属性
* @param globalConfiguration 全局配置
* @return Long 值
*/
@Override
public Long convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
return Convert.toLong(cellData.getStringValue());
}
/**
* 将 Long 转换为 Excel 单元格数据,超长数字按字符串写出。
*
* @param object Java 值
* @param contentProperty 内容属性
* @param globalConfiguration 全局配置
* @return Excel 写入数据
*/
@Override
public WriteCellData<Object> convertToExcelData(Long object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
if (ObjectUtil.isNull(object)) {
@@ -3,17 +3,17 @@ package org.dromara.common.excel.convert;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.fesod.sheet.converters.Converter;
import org.apache.fesod.sheet.enums.CellDataTypeEnum;
import org.apache.fesod.sheet.metadata.GlobalConfiguration;
import org.apache.fesod.sheet.metadata.data.ReadCellData;
import org.apache.fesod.sheet.metadata.data.WriteCellData;
import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.core.service.DictService;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import java.lang.reflect.Field;
import java.util.LinkedHashMap;
@@ -30,16 +30,34 @@ public class ExcelDictConvert implements Converter<Object> {
private DictService dictService;
/**
* 支持的 Java 类型。
*
* @return Object 类型
*/
@Override
public Class<Object> supportJavaTypeKey() {
return Object.class;
}
/**
* 支持的 Excel 单元格类型。
*
* @return 默认支持全部类型
*/
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return null;
}
/**
* 将 Excel 字典显示值转换为 Java 字段值。
*
* @param cellData 单元格数据
* @param contentProperty 内容属性
* @param globalConfiguration 全局配置
* @return Java 字段值
*/
@Override
public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
ExcelDictFormat anno = getAnnotation(contentProperty.getField());
@@ -54,6 +72,14 @@ public class ExcelDictConvert implements Converter<Object> {
return Convert.convert(contentProperty.getField().getType(), value);
}
/**
* 将 Java 字段值转换为 Excel 字典显示值。
*
* @param object Java 字段值
* @param contentProperty 内容属性
* @param globalConfiguration 全局配置
* @return Excel 写入数据
*/
@Override
public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
if (ObjectUtil.isNull(object)) {
@@ -71,10 +97,21 @@ public class ExcelDictConvert implements Converter<Object> {
return new WriteCellData<>(label);
}
/**
* 获取字段上的字典格式注解。
*
* @param field 字段
* @return 字典格式注解
*/
private ExcelDictFormat getAnnotation(Field field) {
return AnnotationUtil.getAnnotation(field, ExcelDictFormat.class);
}
/**
* 延迟获取字典服务。
*
* @return 字典服务
*/
private DictService getDictService() {
if (dictService == null) {
dictService = SpringUtils.getBean(DictService.class);
@@ -84,6 +121,11 @@ public class ExcelDictConvert implements Converter<Object> {
/**
* 解析导出值 0=男,1=女,2=未知。
*
* @param propertyValue 字段值
* @param converterExp 转换表达式
* @param separator 分隔符
* @return 导出显示值
*/
private static String convertByExp(String propertyValue, String converterExp, String separator) {
StringBuilder propertyString = new StringBuilder();
@@ -107,6 +149,11 @@ public class ExcelDictConvert implements Converter<Object> {
/**
* 反向解析值 男=0,女=1,未知=2。
*
* @param propertyValue 显示值
* @param converterExp 转换表达式
* @param separator 分隔符
* @return 字段值
*/
private static String reverseByExp(String propertyValue, String converterExp, String separator) {
StringBuilder propertyString = new StringBuilder();
@@ -128,6 +175,12 @@ public class ExcelDictConvert implements Converter<Object> {
return StringUtils.stripEnd(propertyString.toString(), separator);
}
/**
* 解析转换表达式。
*
* @param converterExp 转换表达式
* @return 字段值与显示值映射
*/
private static Map<String, String> parseConverterExp(String converterExp) {
Map<String, String> result = new LinkedHashMap<>();
if (StringUtils.isBlank(converterExp)) {
@@ -143,6 +196,13 @@ public class ExcelDictConvert implements Converter<Object> {
return result;
}
/**
* 按分隔符拆分多值字段。
*
* @param propertyValue 字段值
* @param separator 分隔符
* @return 拆分后的字段值数组
*/
private static String[] splitPropertyValue(String propertyValue, String separator) {
return propertyValue.split(Pattern.quote(separator));
}
@@ -26,19 +26,43 @@ import java.util.concurrent.ConcurrentHashMap;
@Slf4j
public class ExcelEnumConvert implements Converter<Object> {
/**
* 枚举字典缓存。
*/
private static final Map<Field, Map<Object, String>> ENUM_MAP_CACHE = new ConcurrentHashMap<>();
/**
* 枚举反向字典缓存。
*/
private static final Map<Field, Map<String, Object>> ENUM_REVERSE_MAP_CACHE = new ConcurrentHashMap<>();
/**
* 支持的 Java 类型。
*
* @return Object 类型
*/
@Override
public Class<Object> supportJavaTypeKey() {
return Object.class;
}
/**
* 支持的 Excel 单元格类型。
*
* @return 默认支持全部类型
*/
@Override
public CellDataTypeEnum supportExcelTypeKey() {
return null;
}
/**
* 将 Excel 枚举文本转换为 Java 字段值。
*
* @param cellData 单元格数据
* @param contentProperty 内容属性
* @param globalConfiguration 全局配置
* @return Java 字段值
*/
@Override
public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
cellData.checkEmpty();
@@ -76,6 +100,14 @@ public class ExcelEnumConvert implements Converter<Object> {
return Convert.convert(contentProperty.getField().getType(), codeValue);
}
/**
* 将 Java 枚举编码转换为 Excel 显示文本。
*
* @param object Java 字段值
* @param contentProperty 内容属性
* @param globalConfiguration 全局配置
* @return Excel 写入数据
*/
@Override
public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
if (ObjectUtil.isNull(object)) {
@@ -86,6 +118,12 @@ public class ExcelEnumConvert implements Converter<Object> {
return new WriteCellData<>(value);
}
/**
* 准备枚举编码与显示文本映射。
*
* @param contentProperty 内容属性
* @return 枚举编码与显示文本映射
*/
private Map<Object, String> beforeConvert(ExcelContentProperty contentProperty) {
return ENUM_MAP_CACHE.computeIfAbsent(contentProperty.getField(), field -> {
ExcelEnumFormat anno = getAnnotation(field);
@@ -103,6 +141,12 @@ public class ExcelEnumConvert implements Converter<Object> {
});
}
/**
* 获取字段上的枚举格式注解。
*
* @param field 字段
* @return 枚举格式注解
*/
private ExcelEnumFormat getAnnotation(Field field) {
return AnnotationUtil.getAnnotation(field, ExcelEnumFormat.class);
}
@@ -21,14 +21,32 @@ import java.util.*;
*/
public class CellMergeHandler {
/**
* 是否包含标题行。
*/
private final boolean hasTitle;
/**
* 当前行索引。
*/
private int rowIndex;
/**
* 创建单元格合并处理器。
*
* @param hasTitle 是否存在标题行
*/
private CellMergeHandler(final boolean hasTitle) {
this.hasTitle = hasTitle;
// 行合并开始下标
this.rowIndex = hasTitle ? 1 : 0;
}
/**
* 创建单元格合并处理器。
*
* @param hasTitle 是否存在标题行
* @param rowIndex 起始行索引
*/
private CellMergeHandler(final boolean hasTitle, final int rowIndex) {
this.hasTitle = hasTitle;
this.rowIndex = hasTitle ? rowIndex : 0;
@@ -143,6 +161,14 @@ public class CellMergeHandler {
return mergeFields;
}
/**
* 判断当前行是否满足依赖字段合并条件。
*
* @param currentRow 当前行数据
* @param preRow 上一行数据
* @param cellMerge 合并配置
* @return 是否允许合并
*/
private boolean isMerge(Object currentRow, Object preRow, CellMerge cellMerge) {
final String[] mergeBy = cellMerge.mergeBy();
if (StrUtil.isAllNotBlank(mergeBy)) {
@@ -159,10 +185,24 @@ public class CellMergeHandler {
return true;
}
/**
* 判断单元格值是否为空。
*
* @param value 单元格值
* @return 是否为空
*/
private boolean isBlankCell(Object value) {
return value == null || StrUtil.isBlankIfStr(value);
}
/**
* 追加有效的合并区域。
*
* @param result 合并区域结果集
* @param repeatCell 连续重复单元格信息
* @param endIndex 结束行索引
* @param colNum 列索引
*/
private void appendMergeResult(List<CellRangeAddress> result, RepeatCell repeatCell, int endIndex, int colNum) {
if (repeatCell == null || endIndex <= repeatCell.current()) {
return;
@@ -174,6 +214,13 @@ public class CellMergeHandler {
* 单元格合并
*/
record RepeatCell(Object value, int current) {
/**
* 创建连续重复单元格信息。
*
* @param value 单元格值
* @param current 当前行索引
* @return 连续重复单元格信息
*/
static RepeatCell of(Object value, int current) {
return new RepeatCell(value, current);
}
@@ -183,10 +230,18 @@ public class CellMergeHandler {
* 字段列索引和合并注解信息
*/
record FieldColumnIndex(int colIndex, CellMerge cellMerge) {
/**
* 创建字段列索引和合并注解信息。
*
* @param colIndex 列索引
* @param cellMerge 合并注解
* @return 字段列索引和合并注解信息
*/
static FieldColumnIndex of(int colIndex, CellMerge cellMerge) {
return new FieldColumnIndex(colIndex, cellMerge);
}
}
/**
* 创建一个单元格合并处理器实例
*
@@ -21,20 +21,49 @@ import java.util.List;
@Slf4j
public class CellMergeStrategy extends AbstractMergeStrategy implements SheetWriteHandler {
/**
* 合并单元格区域集合。
*/
private final List<CellRangeAddress> cellList;
/**
* 创建单元格合并策略。
*
* @param cellList 合并区域列表
*/
public CellMergeStrategy(List<CellRangeAddress> cellList) {
this.cellList = cellList;
}
/**
* 根据数据列表创建单元格合并策略。
*
* @param list 数据列表
* @param hasTitle 是否存在标题行
*/
public CellMergeStrategy(List<?> list, boolean hasTitle) {
this.cellList = CellMergeHandler.of(hasTitle).handle(list);
}
/**
* 根据数据列表和起始行创建单元格合并策略。
*
* @param list 数据列表
* @param hasTitle 是否存在标题行
* @param rowIndex 起始行索引
*/
public CellMergeStrategy(List<?> list, boolean hasTitle, int rowIndex) {
this.cellList = CellMergeHandler.of(hasTitle, rowIndex).handle(list);
}
/**
* 写入单元格时清理合并区域非首行内容。
*
* @param sheet 工作表
* @param cell 当前单元格
* @param head 表头
* @param relativeRowIndex 相对行索引
*/
@Override
protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
if (CollUtil.isEmpty(cellList)) {
@@ -50,6 +79,12 @@ public class CellMergeStrategy extends AbstractMergeStrategy implements SheetWri
}
}
/**
* 工作表创建后写入合并区域。
*
* @param writeWorkbookHolder 工作簿上下文
* @param writeSheetHolder 工作表上下文
*/
@Override
public void afterSheetCreate(final WriteWorkbookHolder writeWorkbookHolder, final WriteSheetHolder writeSheetHolder) {
if (CollUtil.isEmpty(cellList)) {
@@ -103,12 +103,24 @@ public class DefaultExcelListener<T> extends AnalysisEventListener<T> implements
}
}
/**
* 记录 Excel 表头信息。
*
* @param headMap 表头映射
* @param context 解析上下文
*/
@Override
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
this.headMap = headMap;
log.debug("解析到一条表头数据: {}", JsonUtils.toJsonString(headMap));
}
/**
* 处理一行导入数据。
*
* @param data 行数据
* @param context 解析上下文
*/
@Override
public void invoke(T data, AnalysisContext context) {
if (isValidate) {
@@ -117,11 +129,21 @@ public class DefaultExcelListener<T> extends AnalysisEventListener<T> implements
excelResult.getList().add(data);
}
/**
* 全部数据解析完成回调。
*
* @param context 解析上下文
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
log.debug("所有数据解析完成!");
}
/**
* 获取 Excel 导入解析结果。
*
* @return 导入解析结果
*/
@Override
public ExcelResult<T> getExcelResult() {
return excelResult;
@@ -26,26 +26,50 @@ public class DefaultExcelResult<T> implements ExcelResult<T> {
@Setter
private List<String> errorList;
/**
* 创建默认导入结果。
*/
public DefaultExcelResult() {
this.list = new ArrayList<>();
this.errorList = new ArrayList<>();
}
/**
* 创建导入结果。
*
* @param list 成功数据列表
* @param errorList 错误信息列表
*/
public DefaultExcelResult(List<T> list, List<String> errorList) {
this.list = list;
this.errorList = errorList;
}
/**
* 复制导入结果。
*
* @param excelResult 原导入结果
*/
public DefaultExcelResult(ExcelResult<T> excelResult) {
this.list = excelResult.getList();
this.errorList = excelResult.getErrorList();
}
/**
* 获取成功数据列表。
*
* @return 成功数据列表
*/
@Override
public List<T> getList() {
return list;
}
/**
* 获取错误信息列表。
*
* @return 错误信息列表
*/
@Override
public List<String> getErrorList() {
return errorList;
@@ -6,13 +6,13 @@ import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.EnumUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.fesod.sheet.metadata.FieldCache;
import org.apache.fesod.sheet.metadata.FieldWrapper;
import org.apache.fesod.sheet.util.ClassUtils;
import org.apache.fesod.sheet.write.handler.SheetWriteHandler;
import org.apache.fesod.sheet.write.metadata.holder.WriteSheetHolder;
import org.apache.fesod.sheet.write.metadata.holder.WriteWorkbookHolder;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddressList;
import org.apache.poi.ss.util.CellReference;
@@ -41,7 +41,13 @@ import java.util.*;
@Slf4j
public class ExcelDownHandler implements SheetWriteHandler {
/**
* 首个数据行索引。
*/
private static final int FIRST_DATA_ROW_INDEX = 1;
/**
* 最后数据行索引。
*/
private static final int LAST_DATA_ROW_INDEX = 1000;
/**
* 单选数据Sheet名
@@ -411,6 +417,11 @@ public class ExcelDownHandler implements SheetWriteHandler {
return CellReference.convertNumToColString(columnIndex);
}
/**
* 延迟获取字典服务。
*
* @return 字典服务
*/
private DictService getDictService() {
if (dictService == null) {
dictService = SpringUtils.getBean(DictService.class);
@@ -9,6 +9,11 @@ import org.apache.fesod.sheet.read.listener.ReadListener;
*/
public interface ExcelListener<T> extends ReadListener<T> {
/**
* 获取 Excel 导入结果。
*
* @return Excel 导入结果
*/
ExcelResult<T> getExcelResult();
}
@@ -50,6 +50,11 @@ public class DataWriteHandler implements SheetWriteHandler, CellWriteHandler {
headColumnMap = getRequiredMap(clazz);
}
/**
* 在单元格写入后设置表头文本格式、必填样式与批注。
*
* @param context 单元格写入上下文
*/
@Override
public void afterCellDispose(CellWriteHandlerContext context) {
if (CollUtil.isEmpty(notationMap) && CollUtil.isEmpty(headColumnMap)) {
@@ -55,52 +55,127 @@ import java.util.zip.ZipOutputStream;
*/
public final class ExcelBuilder<T> {
/**
* 默认工作表名称。
*/
private static final String DEFAULT_SHEET_NAME = "sheet1";
/**
* 默认 ZIP 分页大小。
*/
private static final int DEFAULT_ZIP_PAGE_SIZE = 999;
/**
* 数据内容。
*/
private final List<T> data;
/**
* 表头类型。
*/
private final Class<T> headType;
/**
* 工作表名称。
*/
private String sheetName = DEFAULT_SHEET_NAME;
/**
* 工作表编号。
*/
private Integer sheetNo;
/**
* 是否合并单元格。
*/
private boolean merge;
/**
* 下拉选项集合。
*/
private List<DropDownOptions> options;
/**
* 是否 ZIP 打包。
*/
private boolean zip;
/**
* 分页大小。
*/
private int pageSize = DEFAULT_ZIP_PAGE_SIZE;
/**
* 认证密码。
*/
private String password;
/**
* 是否写入表头。
*/
private Boolean needHead;
/**
* 是否自动合并表头。
*/
private Boolean automaticMergeHead;
/**
* 包含字段集合。
*/
private Collection<String> includeFields;
/**
* 排除字段集合。
*/
private Collection<String> excludeFields;
/**
* 包含列索引集合。
*/
private Collection<Integer> includeIndexes;
/**
* 排除列索引集合。
*/
private Collection<Integer> excludeIndexes;
/**
* 是否按包含列排序。
*/
private Boolean orderByIncludeColumn;
/**
* 列宽。
*/
private Integer columnWidth;
/**
* 表头行高。
*/
private Short headRowHeight;
/**
* 内容行高。
*/
private Short contentRowHeight;
/**
* 写入处理器集合。
*/
private List<WriteHandler> writeHandlers;
/**
* 转换器集合。
*/
private List<Converter<?>> converters;
/**
* 创建 Excel 导出构造器。
*
* @param data 导出数据
* @param headType 表头类型
*/
private ExcelBuilder(List<T> data, Class<T> headType) {
this.data = data;
this.headType = headType;
@@ -373,6 +448,11 @@ public final class ExcelBuilder<T> {
}
}
/**
* 写入单个工作表。
*
* @param outputStream 输出流
*/
private void writeSheet(OutputStream outputStream) {
ExcelWriterSheetBuilder builder = createSheetBuilder(createWriterBuilder(outputStream));
if (merge) {
@@ -382,10 +462,22 @@ public final class ExcelBuilder<T> {
builder.doWrite(data);
}
/**
* 创建 Excel 写入器。
*
* @param outputStream 输出流
* @return Excel 写入器
*/
private ExcelWriter createWriter(OutputStream outputStream) {
return createWriterBuilder(outputStream).build();
}
/**
* 创建并配置 Excel 写入构造器。
*
* @param outputStream 输出流
* @return Excel 写入构造器
*/
private ExcelWriterBuilder createWriterBuilder(OutputStream outputStream) {
ExcelWriterBuilder builder = FesodSheet.write(outputStream, headType)
.autoCloseStream(false)
@@ -437,6 +529,12 @@ public final class ExcelBuilder<T> {
return builder;
}
/**
* 创建工作表写入构造器。
*
* @param builder Excel 写入构造器
* @return 工作表写入构造器
*/
private ExcelWriterSheetBuilder createSheetBuilder(ExcelWriterBuilder builder) {
if (sheetNo != null) {
return builder.sheet(sheetNo, sheetName);
@@ -444,6 +542,11 @@ public final class ExcelBuilder<T> {
return builder.sheet(sheetName);
}
/**
* 将大数据导出拆分为多个 Excel 文件并压缩写入响应。
*
* @param response HTTP 响应
*/
private void exportZipToResponse(HttpServletResponse response) {
if (pageSize <= 0) {
throw new IllegalArgumentException("pageSize 必须大于 0");
@@ -474,6 +577,13 @@ public final class ExcelBuilder<T> {
}
}
/**
* 构建 ZIP 内单个 Excel 文件内容。
*
* @param pageData 当前分页数据
* @param exportSheetName 导出工作表名称
* @return Excel 文件字节
*/
private byte[] buildZipEntry(List<T> pageData, String exportSheetName) {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
ExcelBuilder<T> builder = ExcelBuilder.of(pageData, headType)
@@ -486,6 +596,11 @@ public final class ExcelBuilder<T> {
}
}
/**
* 复制导出配置到分页导出构造器。
*
* @param builder 目标构造器
*/
private void copyOptionsTo(ExcelBuilder<T> builder) {
builder.merge = merge;
builder.options = options;
@@ -506,12 +621,22 @@ public final class ExcelBuilder<T> {
/**
* 重置响应体。
*
* @param filename 文件名
* @param response HTTP 响应
* @throws UnsupportedEncodingException 文件名编码异常
*/
private static void resetResponse(String filename, HttpServletResponse response) throws UnsupportedEncodingException {
FileUtils.setAttachmentResponseHeader(response, encodingFilename(filename));
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
}
/**
* 生成带随机前缀的导出文件名。
*
* @param filename 原始文件名
* @return 编码前的导出文件名
*/
private static String encodingFilename(String filename) {
return IdUtil.fastSimpleUUID() + "_" + filename + ".xlsx";
}
@@ -521,34 +646,82 @@ public final class ExcelBuilder<T> {
*/
public static final class ReadBuilder<T> {
/**
* 导入输入流。
*/
private final InputStream inputStream;
/**
* 表头类型。
*/
private final Class<T> headType;
/**
* 是否执行数据校验。
*/
private boolean validate = true;
/**
* 是否快速失败。
*/
private boolean failFast = true;
/**
* Excel 监听器。
*/
private ExcelListener<T> listener;
/**
* 工作表编号。
*/
private Integer sheetNo;
/**
* 工作表名称。
*/
private String sheetName;
/**
* 表头行数。
*/
private Integer headRowNumber;
/**
* 是否忽略空行。
*/
private Boolean ignoreEmptyRow;
/**
* 认证密码。
*/
private String password;
/**
* 是否自动裁剪空白。
*/
private Boolean autoTrim;
/**
* 是否自动清理不可见字符。
*/
private Boolean autoStrip;
/**
* 读取行数。
*/
private Integer numRows;
/**
* 转换器集合。
*/
private List<Converter<?>> converters;
/**
* 创建 Excel 导入读取构造器。
*
* @param inputStream 输入流
* @param headType 表头类型
*/
private ReadBuilder(InputStream inputStream, Class<T> headType) {
this.inputStream = inputStream;
this.headType = headType;
@@ -700,6 +873,12 @@ public final class ExcelBuilder<T> {
return createReaderBuilder(null).doReadAllSync();
}
/**
* 创建并配置 Excel 读取构造器。
*
* @param readListener 读取监听器
* @return Excel 读取构造器
*/
private ExcelReaderBuilder createReaderBuilder(ExcelListener<T> readListener) {
ExcelReaderBuilder builder = FesodSheet.read(inputStream)
.head(headType)
@@ -731,6 +910,12 @@ public final class ExcelBuilder<T> {
return builder;
}
/**
* 创建工作表读取构造器。
*
* @param builder Excel 读取构造器
* @return 工作表读取构造器
*/
private ExcelReaderSheetBuilder createSheetBuilder(ExcelReaderBuilder builder) {
ExcelReaderSheetBuilder sheetBuilder;
if (sheetNo != null && StringUtils.isNotBlank(sheetName)) {
@@ -754,14 +939,31 @@ public final class ExcelBuilder<T> {
*/
public static final class TemplateBuilder {
/**
* 模板路径。
*/
private final String templatePath;
/**
* 文件名称。
*/
private String filename = DEFAULT_SHEET_NAME;
/**
* 模板模式。
*/
private TemplateMode mode;
/**
* 数据内容。
*/
private Object data;
/**
* 创建 Excel 模板导出构造器。
*
* @param templatePath 模板路径
*/
private TemplateBuilder(String templatePath) {
this.templatePath = templatePath;
}
@@ -838,6 +1040,11 @@ public final class ExcelBuilder<T> {
}
}
/**
* 按模板模式填充数据。
*
* @param excelWriter Excel 写入器
*/
@SuppressWarnings("unchecked")
private void fill(ExcelWriter excelWriter) {
if (mode == TemplateMode.LIST) {
@@ -874,6 +1081,9 @@ public final class ExcelBuilder<T> {
}
}
/**
* 校验模板导出数据。
*/
private void validateData() {
if (mode == null || data == null) {
throw new IllegalArgumentException("数据为空");
@@ -17,6 +17,9 @@ public class BigNumberSerializer extends NumberSerializer {
* 根据 JS Number.MAX_SAFE_INTEGER 与 Number.MIN_SAFE_INTEGER 得来
*/
private static final long MAX_SAFE_INTEGER = 9007199254740991L;
/**
* JavaScript 最小安全整数。
*/
private static final long MIN_SAFE_INTEGER = -9007199254740991L;
/**
@@ -19,32 +19,74 @@ import java.util.*;
*/
public final class MailBuilder {
/**
* 邮件账户配置。
*/
private MailAccount mailAccount;
/**
* 是否使用全局邮件会话。
*/
private boolean useGlobalSession = true;
/**
* 收件人列表。
*/
private final List<String> tos = new ArrayList<>();
/**
* 抄送人列表。
*/
private final List<String> ccs = new ArrayList<>();
/**
* 密送人列表。
*/
private final List<String> bccs = new ArrayList<>();
/**
* 内嵌图片集合。
*/
private final Map<String, InputStream> images = new LinkedHashMap<>();
/**
* 附件文件集合。
*/
private File[] files = new File[0];
/**
* 发件人。
*/
private String from;
/**
* 登录用户。
*/
private String user;
/**
* 登录密码。
*/
private String pass;
/**
* 邮件主题。
*/
private String subject;
/**
* 邮件内容。
*/
private String content;
/**
* 是否 HTML 内容。
*/
private boolean html;
/**
* 私有构造器,统一通过静态工厂创建。
*/
private MailBuilder() {
}
@@ -220,7 +262,7 @@ public final class MailBuilder {
* 设置正文。
*
* @param content 正文
* @param html 是否 HTML
* @param html 是否 HTML
* @return 当前构建器
*/
public MailBuilder content(String content, boolean html) {
@@ -232,7 +274,7 @@ public final class MailBuilder {
/**
* 添加内联图片。
*
* @param cid 图片 cid
* @param cid 图片 cid
* @param inputStream 图片输入流
* @return 当前构建器
*/
@@ -297,6 +339,9 @@ public final class MailBuilder {
}
}
/**
* 校验邮件发送必填项。
*/
private void validate() {
if (CollUtil.isEmpty(tos)) {
throw new IllegalArgumentException("邮件收件人不能为空");
@@ -309,6 +354,11 @@ public final class MailBuilder {
}
}
/**
* 解析实际使用的邮件账户。
*
* @return 邮件账户
*/
private MailAccount resolveMailAccount() {
MailAccount account = mailAccount;
if (account == null) {
@@ -324,6 +374,12 @@ public final class MailBuilder {
return copy;
}
/**
* 拆分逗号或分号分隔的邮箱地址。
*
* @param addresses 邮箱地址字符串
* @return 邮箱地址列表
*/
private List<String> splitAddress(String addresses) {
if (StrUtil.isBlank(addresses)) {
return Collections.emptyList();
@@ -331,6 +387,12 @@ public final class MailBuilder {
return normalizeAddresses(StrUtil.splitTrim(addresses.replace(';', ','), ','));
}
/**
* 过滤并标准化邮箱地址集合。
*
* @param addresses 邮箱地址集合
* @return 标准化后的邮箱地址列表
*/
private List<String> normalizeAddresses(Collection<String> addresses) {
if (CollUtil.isEmpty(addresses)) {
return Collections.emptyList();
@@ -41,7 +41,7 @@ public class McpClientTemplate {
/**
* 调用所有 MCP Server 上的同名工具。
*
* @param toolName 工具名称
* @param toolName 工具名称
* @param arguments 工具参数
* @return 各 Server 的工具调用结果
*/
@@ -58,8 +58,8 @@ public class McpClientTemplate {
* 调用指定 MCP Server 上的工具。
*
* @param serverName Server 名称
* @param toolName 工具名称
* @param arguments 工具参数
* @param toolName 工具名称
* @param arguments 工具参数
* @return 工具调用结果
*/
public Optional<McpToolCallResult> callTool(String serverName, String toolName, Map<String, Object> arguments) {
@@ -86,7 +86,7 @@ public class McpClientTemplate {
* 读取指定 MCP Server 上的资源。
*
* @param serverName Server 名称
* @param uri 资源地址
* @param uri 资源地址
* @return 资源内容
*/
public Optional<McpResourceReadResult> readResource(String serverName, String uri) {
@@ -115,6 +115,12 @@ public class McpClientTemplate {
return mcpSyncClients;
}
/**
* 解析 MCP Server 名称。
*
* @param client MCP 同步客户端
* @return Server 名称
*/
private String getServerName(McpSyncClient client) {
McpSchema.Implementation serverInfo = client.getServerInfo();
if (serverInfo == null || serverInfo.name() == null) {
@@ -12,19 +12,38 @@ import org.tio.core.ChannelContext;
*/
@Slf4j
public class MqttClientConnectListener implements IMqttClientConnectListener {
//
private final MqttClientCreator mqttClientCreator;
/**
* 创建 MQTT 客户端连接监听器。
*
* @param mqttClientCreator MQTT 客户端创建器
*/
public MqttClientConnectListener(MqttClientCreator mqttClientCreator) {
this.mqttClientCreator = mqttClientCreator;
}
/**
* 客户端连接成功回调。
*
* @param context 通道上下文
* @param isReconnect 是否重连
*/
@Override
public void onConnected(ChannelContext context, boolean isReconnect) {
// 创建连接
log.info("MqttConnectedEvent:{}", context);
}
/**
* 客户端断开连接回调。
*
* @param context 通道上下文
* @param throwable 断开异常
* @param remark 断开说明
* @param isRemove 是否移除连接
*/
@Override
public void onDisconnect(ChannelContext context, Throwable throwable, String remark, boolean isRemove) {
// 离线时更新重连
@@ -15,6 +15,14 @@ import java.nio.charset.StandardCharsets;
@Slf4j
public class MqttClientGlobalMessageListener implements IMqttClientGlobalMessageListener {
/**
* 接收并记录全局 MQTT 消息。
*
* @param context 通道上下文
* @param topic 主题
* @param message MQTT 消息对象
* @param payload 消息载荷
*/
@Override
public void onMessage(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) {
log.info("MqttGlobalMessageEvent => topic: {}, msg: {}", topic, new String(payload, StandardCharsets.UTF_8));
@@ -17,7 +17,10 @@ import software.amazon.awssdk.core.async.AsyncRequestBody;
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
import software.amazon.awssdk.core.async.ResponsePublisher;
import software.amazon.awssdk.services.s3.S3AsyncClient;
import software.amazon.awssdk.services.s3.model.*;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
import software.amazon.awssdk.transfer.s3.S3TransferManager;
import software.amazon.awssdk.transfer.s3.model.CompletedUpload;
@@ -38,8 +41,8 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiFunction;
import java.util.function.Consumer;
@@ -90,7 +93,7 @@ public abstract class AbstractOssClientImpl implements OssClient {
* 构造 S3 存储客户端基础实现。
*
* @param clientId 客户端 ID
* @param config S3 存储客户端配置
* @param config S3 存储客户端配置
*/
public AbstractOssClientImpl(String clientId, OssClientConfig config) {
Assert.notNull(config, () -> S3StorageException.form("S3StorageClientConfig must not be null"));
@@ -100,22 +103,40 @@ public abstract class AbstractOssClientImpl implements OssClient {
this.initialize();
}
/**
* 获取客户端 ID。
*
* @return 客户端 ID
*/
@Override
public String clientId() {
return this.clientId;
}
/**
* 获取客户端配置副本。
*
* @return 客户端配置副本
*/
@Override
public OssClientConfig config() {
// 仅返回copy副本,防篡改
return this.config.copy();
}
/**
* 判断客户端是否已经初始化。
*
* @return 是否已初始化
*/
@Override
public boolean isInitialized() {
return initialized.get();
}
/**
* 初始化底层 S3 客户端资源。
*/
@Override
public void initialize() {
// 如果已经是初始化状态,则直接返回
@@ -131,24 +152,52 @@ public abstract class AbstractOssClientImpl implements OssClient {
}
}
/**
* 执行客户端具体初始化逻辑。
*/
abstract void doInitialize();
/**
* 使用回调校验当前客户端配置。
*
* @param verifyConfigAction 配置校验回调
* @return 是否校验通过
*/
@Override
public boolean verifyConfig(Function<OssClientConfig, Boolean> verifyConfigAction) {
OssClientConfig config = config();
return Boolean.TRUE.equals(verifyConfigAction.apply(config));
}
/**
* 校验当前客户端配置是否与指定配置一致。
*
* @param verifyConfig 待校验配置
* @return 是否一致
*/
@Override
public boolean verifyConfig(OssClientConfig verifyConfig) {
return verifyConfig((config) -> Objects.equals(config, verifyConfig));
}
/**
* 根据文件名构建对象键。
*
* @param fileName 原始文件名
* @return 对象键
*/
@Override
public String buildPathKey(String fileName) {
return buildPathKey(null, fileName);
}
/**
* 根据业务前缀和文件名构建对象键。
*
* @param businessPrefix 业务前缀
* @param fileName 原始文件名
* @return 对象键
*/
@Override
public String buildPathKey(String businessPrefix, String fileName) {
String defaultPrefix = config.prefix()
@@ -161,6 +210,16 @@ public abstract class AbstractOssClientImpl implements OssClient {
return path + suffix;
}
/**
* 执行自定义上传请求。
*
* @param body 上传请求体
* @param putObjectRequestBuilderConsumer PutObject 请求构建回调
* @param transferListeners 传输监听器集合
* @param handleAsyncAction 上传完成处理函数
* @param <T> 返回值类型
* @return 上传处理结果
*/
@Override
public <T> T doCustomUpload(AsyncRequestBody body, Consumer<PutObjectRequest.Builder> putObjectRequestBuilderConsumer, Collection<TransferListener> transferListeners, BiFunction<CompletedUpload, Throwable, T> handleAsyncAction) {
try {
@@ -177,11 +236,28 @@ public abstract class AbstractOssClientImpl implements OssClient {
}
}
/**
* 执行自定义上传请求。
*
* @param body 上传请求体
* @param putObjectRequestBuilderConsumer PutObject 请求构建回调
* @param handleAsyncAction 上传完成处理函数
* @param <T> 返回值类型
* @return 上传处理结果
*/
@Override
public <T> T doCustomUpload(AsyncRequestBody body, Consumer<PutObjectRequest.Builder> putObjectRequestBuilderConsumer, BiFunction<CompletedUpload, Throwable, T> handleAsyncAction) {
return doCustomUpload(body, putObjectRequestBuilderConsumer, null, handleAsyncAction);
}
/**
* 执行自定义上传请求并返回统一异步结果。
*
* @param body 上传请求体
* @param putObjectRequestBuilderConsumer PutObject 请求构建回调
* @param transferListeners 传输监听器集合
* @return 上传结果
*/
@Override
public HandleAsyncResult<PutObjectResponse> doCustomUpload(AsyncRequestBody body, Consumer<PutObjectRequest.Builder> putObjectRequestBuilderConsumer, Collection<TransferListener> transferListeners) {
return doCustomUpload(body, putObjectRequestBuilderConsumer, transferListeners, (completedUpload, throwable) -> {
@@ -192,6 +268,13 @@ public abstract class AbstractOssClientImpl implements OssClient {
});
}
/**
* 执行自定义上传请求并返回统一异步结果。
*
* @param body 上传请求体
* @param putObjectRequestBuilderConsumer PutObject 请求构建回调
* @return 上传结果
*/
@Override
public HandleAsyncResult<PutObjectResponse> doCustomUpload(AsyncRequestBody body, Consumer<PutObjectRequest.Builder> putObjectRequestBuilderConsumer) {
return doCustomUpload(body, putObjectRequestBuilderConsumer, null, (completedUpload, throwable) -> {
@@ -202,28 +285,71 @@ public abstract class AbstractOssClientImpl implements OssClient {
});
}
/**
* 上传本地路径文件到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param path 文件路径
* @param options 上传选项
* @return 上传结果
*/
@Override
public PutObjectResult bucketUpload(String bucket, String key, Path path, Options options) {
AsyncRequestBody body = AsyncRequestBody.fromFile(path);
return bucketUpload(bucket, key, body, options);
}
/**
* 上传本地路径文件到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param path 文件路径
* @return 上传结果
*/
@Override
public PutObjectResult bucketUpload(String bucket, String key, Path path) {
return bucketUpload(bucket, key, path, Options.builder());
}
/**
* 上传文件到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param file 文件对象
* @param options 上传选项
* @return 上传结果
*/
@Override
public PutObjectResult bucketUpload(String bucket, String key, File file, Options options) {
AsyncRequestBody body = AsyncRequestBody.fromFile(file);
return bucketUpload(bucket, key, body, options);
}
/**
* 上传文件到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param file 文件对象
* @return 上传结果
*/
@Override
public PutObjectResult bucketUpload(String bucket, String key, File file) {
return bucketUpload(bucket, key, file, Options.builder());
}
/**
* 上传随机访问文件到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param file 随机访问文件
* @param options 上传选项
* @return 上传结果
*/
@Override
public PutObjectResult bucketUpload(String bucket, String key, RandomAccessFile file, Options options) {
try {
@@ -235,11 +361,29 @@ public abstract class AbstractOssClientImpl implements OssClient {
}
}
/**
* 上传随机访问文件到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param file 随机访问文件
* @return 上传结果
*/
@Override
public PutObjectResult bucketUpload(String bucket, String key, RandomAccessFile file) {
return bucketUpload(bucket, key, file, Options.builder());
}
/**
* 上传可读通道数据到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param channel 可读通道
* @param contentLength 内容长度
* @param options 上传选项
* @return 上传结果
*/
@Override
public PutObjectResult bucketUpload(String bucket, String key, ReadableByteChannel channel, long contentLength, Options options) {
// 让调用者自行处理通道的关闭
@@ -256,11 +400,30 @@ public abstract class AbstractOssClientImpl implements OssClient {
}
}
/**
* 上传可读通道数据到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param channel 可读通道
* @param contentLength 内容长度
* @return 上传结果
*/
@Override
public PutObjectResult bucketUpload(String bucket, String key, ReadableByteChannel channel, long contentLength) {
return bucketUpload(bucket, key, channel, contentLength, Options.builder());
}
/**
* 上传输入流数据到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param in 输入流
* @param contentLength 内容长度
* @param options 上传选项
* @return 上传结果
*/
@Override
public PutObjectResult bucketUpload(String bucket, String key, InputStream in, long contentLength, Options options) {
options.setLength(contentLength);
@@ -268,11 +431,29 @@ public abstract class AbstractOssClientImpl implements OssClient {
return bucketUpload(bucket, key, body, options);
}
/**
* 上传输入流数据到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param in 输入流
* @param contentLength 内容长度
* @return 上传结果
*/
@Override
public PutObjectResult bucketUpload(String bucket, String key, InputStream in, long contentLength) {
return bucketUpload(bucket, key, in, contentLength, Options.builder());
}
/**
* 上传字节数组到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param data 字节数组
* @param options 上传选项
* @return 上传结果
*/
@Override
public PutObjectResult bucketUpload(String bucket, String key, byte[] data, Options options) {
try (ByteArrayInputStream in = new ByteArrayInputStream(data)) {
@@ -282,11 +463,28 @@ public abstract class AbstractOssClientImpl implements OssClient {
}
}
/**
* 上传字节数组到指定存储桶。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param data 字节数组
* @return 上传结果
*/
@Override
public PutObjectResult bucketUpload(String bucket, String key, byte[] data) {
return bucketUpload(bucket, key, data, Options.builder());
}
/**
* 执行指定存储桶的底层上传。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param body 上传请求体
* @param options 上传选项
* @return 上传结果
*/
@NullMarked
private PutObjectResult bucketUpload(String bucket, String key, AsyncRequestBody body, Options options) {
// 优先使用body中的内容大小,如果不存在,再获取可选项中的
@@ -321,6 +519,15 @@ public abstract class AbstractOssClientImpl implements OssClient {
return PutObjectResult.form("%s/%s".formatted(bucketUrl, key), key, response.eTag(), size);
}
/**
* 执行自定义下载请求。
*
* @param getObjectRequestBuilderConsumer GetObject 请求构建回调
* @param responseTransformer 下载响应转换器
* @param transferListeners 传输监听器集合
* @param <T> 下载结果类型
* @return 下载结果
*/
@Override
public <T> T doCustomDownload(Consumer<GetObjectRequest.Builder> getObjectRequestBuilderConsumer, AsyncResponseTransformer<GetObjectResponse, T> responseTransformer, Collection<TransferListener> transferListeners) {
try {
@@ -338,6 +545,14 @@ public abstract class AbstractOssClientImpl implements OssClient {
}
}
/**
* 将指定存储桶对象下载到订阅器。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param downloadSubscriber 下载订阅器
* @return 下载结果
*/
@Override
public GetObjectResult bucketDownload(String bucket, String key, OutputStreamDownloadSubscriber downloadSubscriber) {
try {
@@ -350,6 +565,15 @@ public abstract class AbstractOssClientImpl implements OssClient {
}
}
/**
* 将指定存储桶对象下载到转换器。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param downloadTransformer 下载转换器
* @param <T> 下载结果类型
* @return 下载结果
*/
@Override
public <T> T bucketDownload(String bucket, String key, BiFunction<GetObjectResult, InputStream, T> downloadTransformer) {
try (ResponseInputStream<GetObjectResponse> responseInputStream = doCustomDownload(builder -> builder.bucket(bucket).key(key), AsyncResponseTransformer.toBlockingInputStream(), null)) {
@@ -361,6 +585,14 @@ public abstract class AbstractOssClientImpl implements OssClient {
}
}
/**
* 将指定存储桶对象下载到本地路径。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param path 本地路径
* @return 下载结果
*/
@Override
public GetObjectResult bucketDownload(String bucket, String key, Path path) {
try (OutputStream out = Files.newOutputStream(path)) {
@@ -370,6 +602,14 @@ public abstract class AbstractOssClientImpl implements OssClient {
}
}
/**
* 将指定存储桶对象下载到文件。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param file 本地文件
* @return 下载结果
*/
@Override
public GetObjectResult bucketDownload(String bucket, String key, File file) {
try (FileOutputStream out = new FileOutputStream(file)) {
@@ -379,21 +619,52 @@ public abstract class AbstractOssClientImpl implements OssClient {
}
}
/**
* 将指定存储桶对象下载到随机访问文件。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param file 随机访问文件
* @return 下载结果
*/
@Override
public GetObjectResult bucketDownload(String bucket, String key, RandomAccessFile file) {
return bucketDownload(bucket, key, file.getChannel());
}
/**
* 将指定存储桶对象下载到可写通道。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param channel 可写通道
* @return 下载结果
*/
@Override
public GetObjectResult bucketDownload(String bucket, String key, WritableByteChannel channel) {
return bucketDownload(bucket, key, OutputStreamDownloadSubscriber.create(channel));
}
/**
* 将指定存储桶对象下载到输出流。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param out 输出流
* @return 下载结果
*/
@Override
public GetObjectResult bucketDownload(String bucket, String key, OutputStream out) {
return bucketDownload(bucket, key, OutputStreamDownloadSubscriber.create(out));
}
/**
* 根据 S3 响应构建下载结果。
*
* @param key 对象键
* @param response S3 下载响应
* @return 下载结果
*/
private GetObjectResult buildGetObjectResult(String key, GetObjectResponse response) {
return GetObjectResult.form(
key,
@@ -409,6 +680,13 @@ public abstract class AbstractOssClientImpl implements OssClient {
);
}
/**
* 删除指定存储桶中的对象。
*
* @param bucket 存储桶名称
* @param key 对象键
* @return 是否删除成功
*/
@Override
public boolean bucketDelete(String bucket, String key) {
try {
@@ -419,6 +697,14 @@ public abstract class AbstractOssClientImpl implements OssClient {
}
}
/**
* 生成指定存储桶对象的下载预签名 URL。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param expiredTime 过期时间
* @return 预签名下载 URL
*/
@Override
public String bucketPresignGetUrl(String bucket, String key, Duration expiredTime) {
try {
@@ -433,6 +719,15 @@ public abstract class AbstractOssClientImpl implements OssClient {
}
}
/**
* 生成指定存储桶对象的上传预签名 URL。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param expiredTime 过期时间
* @param metadata 对象元数据
* @return 预签名上传 URL
*/
@Override
public String bucketPresignPutUrl(String bucket, String key, Duration expiredTime, Map<String, String> metadata) {
try {
@@ -447,122 +742,299 @@ public abstract class AbstractOssClientImpl implements OssClient {
}
}
/**
* 上传本地路径文件到默认存储桶。
*
* @param key 对象键
* @param path 文件路径
* @param options 上传选项
* @return 上传结果
*/
@Override
public PutObjectResult upload(String key, Path path, Options options) {
return bucketUpload(defaultBucket(), key, path, options);
}
/**
* 上传本地路径文件到默认存储桶。
*
* @param key 对象键
* @param path 文件路径
* @return 上传结果
*/
@Override
public PutObjectResult upload(String key, Path path) {
return bucketUpload(defaultBucket(), key, path);
}
/**
* 上传文件到默认存储桶。
*
* @param key 对象键
* @param file 文件对象
* @param options 上传选项
* @return 上传结果
*/
@Override
public PutObjectResult upload(String key, File file, Options options) {
return bucketUpload(defaultBucket(), key, file, options);
}
/**
* 上传文件到默认存储桶。
*
* @param key 对象键
* @param file 文件对象
* @return 上传结果
*/
@Override
public PutObjectResult upload(String key, File file) {
return bucketUpload(defaultBucket(), key, file);
}
/**
* 上传随机访问文件到默认存储桶。
*
* @param key 对象键
* @param file 随机访问文件
* @param options 上传选项
* @return 上传结果
*/
@Override
public PutObjectResult upload(String key, RandomAccessFile file, Options options) {
return bucketUpload(defaultBucket(), key, file, options);
}
/**
* 上传随机访问文件到默认存储桶。
*
* @param key 对象键
* @param file 随机访问文件
* @return 上传结果
*/
@Override
public PutObjectResult upload(String key, RandomAccessFile file) {
return bucketUpload(defaultBucket(), key, file);
}
/**
* 上传可读通道数据到默认存储桶。
*
* @param key 对象键
* @param channel 可读通道
* @param contentLength 内容长度
* @param options 上传选项
* @return 上传结果
*/
@Override
public PutObjectResult upload(String key, ReadableByteChannel channel, long contentLength, Options options) {
return bucketUpload(defaultBucket(), key, channel, contentLength, options);
}
/**
* 上传可读通道数据到默认存储桶。
*
* @param key 对象键
* @param channel 可读通道
* @param contentLength 内容长度
* @return 上传结果
*/
@Override
public PutObjectResult upload(String key, ReadableByteChannel channel, long contentLength) {
return bucketUpload(defaultBucket(), key, channel, contentLength);
}
/**
* 上传输入流数据到默认存储桶。
*
* @param key 对象键
* @param in 输入流
* @param contentLength 内容长度
* @param options 上传选项
* @return 上传结果
*/
@Override
public PutObjectResult upload(String key, InputStream in, long contentLength, Options options) {
return bucketUpload(defaultBucket(), key, in, contentLength, options);
}
/**
* 上传输入流数据到默认存储桶。
*
* @param key 对象键
* @param in 输入流
* @param contentLength 内容长度
* @return 上传结果
*/
@Override
public PutObjectResult upload(String key, InputStream in, long contentLength) {
return bucketUpload(defaultBucket(), key, in, contentLength);
}
/**
* 上传字节数组到默认存储桶。
*
* @param key 对象键
* @param data 字节数组
* @param options 上传选项
* @return 上传结果
*/
@Override
public PutObjectResult upload(String key, byte[] data, Options options) {
return bucketUpload(defaultBucket(), key, data, options);
}
/**
* 上传字节数组到默认存储桶。
*
* @param key 对象键
* @param data 字节数组
* @return 上传结果
*/
@Override
public PutObjectResult upload(String key, byte[] data) {
return bucketUpload(defaultBucket(), key, data);
}
/**
* 将默认存储桶对象下载到订阅器。
*
* @param key 对象键
* @param downloadSubscriber 下载订阅器
* @return 下载结果
*/
@Override
public GetObjectResult download(String key, OutputStreamDownloadSubscriber downloadSubscriber) {
return bucketDownload(defaultBucket(), key, downloadSubscriber);
}
/**
* 将默认存储桶对象下载到转换器。
*
* @param key 对象键
* @param downloadTransformer 下载转换器
* @param <T> 下载结果类型
* @return 下载结果
*/
@Override
public <T> T download(String key, BiFunction<GetObjectResult, InputStream, T> downloadTransformer) {
return bucketDownload(defaultBucket(), key, downloadTransformer);
}
/**
* 将默认存储桶对象下载到本地路径。
*
* @param key 对象键
* @param path 本地路径
* @return 下载结果
*/
@Override
public GetObjectResult download(String key, Path path) {
return bucketDownload(defaultBucket(), key, path);
}
/**
* 将默认存储桶对象下载到文件。
*
* @param key 对象键
* @param file 本地文件
* @return 下载结果
*/
@Override
public GetObjectResult download(String key, File file) {
return bucketDownload(defaultBucket(), key, file);
}
/**
* 将默认存储桶对象下载到随机访问文件。
*
* @param key 对象键
* @param file 随机访问文件
* @return 下载结果
*/
@Override
public GetObjectResult download(String key, RandomAccessFile file) {
return bucketDownload(defaultBucket(), key, file);
}
/**
* 将默认存储桶对象下载到可写通道。
*
* @param key 对象键
* @param channel 可写通道
* @return 下载结果
*/
@Override
public GetObjectResult download(String key, WritableByteChannel channel) {
return bucketDownload(defaultBucket(), key, channel);
}
/**
* 将默认存储桶对象下载到输出流。
*
* @param key 对象键
* @param out 输出流
* @return 下载结果
*/
@Override
public GetObjectResult download(String key, OutputStream out) {
return bucketDownload(defaultBucket(), key, out);
}
/**
* 删除默认存储桶中的对象。
*
* @param key 对象键
* @return 是否删除成功
*/
@Override
public boolean delete(String key) {
return bucketDelete(defaultBucket(), key);
}
/**
* 生成默认存储桶对象的下载预签名 URL。
*
* @param key 对象键
* @param expiredTime 过期时间
* @return 预签名下载 URL
*/
@Override
public String presignGetUrl(String key, Duration expiredTime) {
return bucketPresignGetUrl(defaultBucket(), key, expiredTime);
}
/**
* 生成默认存储桶对象的上传预签名 URL。
*
* @param key 对象键
* @param expiredTime 过期时间
* @param metadata 对象元数据
* @return 预签名上传 URL
*/
@Override
public String presignPutUrl(String key, Duration expiredTime, Map<String, String> metadata) {
return bucketPresignPutUrl(defaultBucket(), key, expiredTime, metadata);
}
/**
* 获取默认存储桶名称。
*
* @return 默认存储桶名称
*/
private String defaultBucket() {
return config.bucket()
.filter(bucket -> !bucket.isBlank())
.orElseThrow(() -> S3StorageException.form("bucket is not configured."));
}
/**
* 合并默认前缀与业务前缀。
*
* @param defaultPrefix 默认前缀
* @param businessPrefix 业务前缀
* @return 合并后的前缀
*/
private String mergePrefix(String defaultPrefix, String businessPrefix) {
String left = normalizePrefix(defaultPrefix);
String right = normalizePrefix(businessPrefix);
@@ -575,6 +1047,12 @@ public abstract class AbstractOssClientImpl implements OssClient {
return left + StringUtils.SLASH + right;
}
/**
* 规范化对象键前缀。
*
* @param prefix 原始前缀
* @return 规范化后的前缀
*/
private String normalizePrefix(String prefix) {
if (prefix == null) {
return "";
@@ -589,6 +1067,12 @@ public abstract class AbstractOssClientImpl implements OssClient {
return normalized;
}
/**
* 提取文件扩展名。
*
* @param fileName 文件名
* @return 文件扩展名
*/
private String suffix(String fileName) {
if (fileName == null) {
return "";
@@ -600,6 +1084,12 @@ public abstract class AbstractOssClientImpl implements OssClient {
return fileName.substring(index);
}
/**
* 转换为统一的 S3 存储异常。
*
* @param e 原始异常
* @return S3 存储异常
*/
private S3StorageException toStorageException(Throwable e) {
Throwable cause = unwrapAsyncException(e);
if (cause instanceof S3StorageException ex) {
@@ -608,6 +1098,12 @@ public abstract class AbstractOssClientImpl implements OssClient {
return S3StorageException.form(cause);
}
/**
* 解包异步执行异常。
*
* @param e 原始异常
* @return 根因异常
*/
private Throwable unwrapAsyncException(Throwable e) {
Throwable cause = e;
while ((cause instanceof CompletionException || cause instanceof ExecutionException) && cause.getCause() != null) {
@@ -616,6 +1112,11 @@ public abstract class AbstractOssClientImpl implements OssClient {
return cause;
}
/**
* 关闭底层 S3 客户端资源。
*
* @throws Exception 关闭资源异常
*/
@Override
public void close() throws Exception {
if (s3TransferManager != null) {
@@ -35,6 +35,9 @@ public class DefaultOssClientImpl extends AbstractOssClientImpl {
super(clientId, config);
}
/**
* 初始化默认 S3 客户端传输管理器和预签名生成器
*/
@Override
void doInitialize() {
// 校验配置
@@ -32,17 +32,32 @@ public record AccessControlPolicyConfig(
.accessPolicy(AccessPolicy.PUBLIC_READ_WRITE)
.build();
/**
* 获取访问策略未配置时返回默认策略
*
* @return 访问策略
*/
@Override
public @NonNull AccessPolicy accessPolicy() {
return Optional.ofNullable(accessPolicy)
.orElse(AccessPolicy.PUBLIC_READ_WRITE);
}
/**
* 复制访问策略配置
*
* @return 配置副本
*/
@Override
public AccessControlPolicyConfig copy() {
return toBuilder().build();
}
/**
* 转换为构建器
*
* @return 配置构建器
*/
@Override
public AccessControlPolicyConfigBuilder toBuilder() {
return builder()
@@ -52,11 +52,21 @@ public record OssAsyncExecutorConfig(
return corePoolSize;
}
/**
* 复制异步执行器配置
*
* @return 配置副本
*/
@Override
public OssAsyncExecutorConfig copy() {
return toBuilder().build();
}
/**
* 转换为构建器
*
* @return 配置构建器
*/
@Override
public OssAsyncExecutorConfigBuilder toBuilder() {
return builder()
@@ -226,6 +226,12 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
return usePathStyleAccess ? BucketUrlUtil.getPathStyleBucketUrl(useHttps, url, bucket) : BucketUrlUtil.getSiteStyleBucketUrl(useHttps, url, bucket);
}
/**
* 解析 S3 Region
*
* @param regionString Region 字符串
* @return Region 对象
*/
private static Region parseRegion(String regionString) {
if (StringUtils.isBlank(regionString)) {
return Region.US_EAST_1;
@@ -233,11 +239,23 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
return Region.of(regionString);
}
/**
* 兼容旧配置推断是否使用路径风格访问
*
* @param properties OSS 配置属性
* @return 是否使用路径风格访问
*/
private static boolean resolvePathStyleAccess(OssProperties properties) {
// 旧配置没有显式路径风格字段只能继续按内置云厂商 endpoint 做兼容推断
return !StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE);
}
/**
* 解析 ACL 访问策略配置
*
* @param accessPolicyString 访问策略字符串
* @return ACL 访问策略配置
*/
private static AccessControlPolicyConfig resolveAccessControlPolicy(String accessPolicyString) {
// 绝大多数云厂商不允许操作 ACL默认禁用当前业务只用访问策略判断是否生成预签名 URL
if (StringUtils.isBlank(accessPolicyString)) {
@@ -249,18 +267,34 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
.build();
}
/**
* 获取用于访问对象的基础 URL
*
* @return 基础 URL
*/
private String getAccessBaseUrl() {
return domain()
.filter(OssClientConfig::hasHttpHeader)
.orElseGet(this::getEndpoint);
}
/**
* 获取 endpoint 配置
*
* @return endpoint
*/
private String getEndpoint() {
return endpoint()
.filter(s -> !s.isBlank())
.orElseThrow(() -> S3StorageException.form("endpoint is not configured."));
}
/**
* 判断 URL 是否包含 HTTP 协议头
*
* @param url URL
* @return 是否包含 HTTP 协议头
*/
private static boolean hasHttpHeader(String url) {
return HttpUtil.isHttp(url) || HttpUtil.isHttps(url);
}
@@ -92,10 +92,22 @@ public class OssFactory {
}
}
/**
* 获取指定配置键对应的客户端锁
*
* @param configKey 配置键
* @return 客户端锁
*/
private static ReentrantLock getClientLock(String configKey) {
return CLIENT_LOCKS.computeIfAbsent(configKey, key -> new ReentrantLock());
}
/**
* 关闭 OSS 客户端
*
* @param configKey 配置键
* @param client OSS 客户端
*/
private static void closeClient(String configKey, OssClient client) {
try {
client.close();
@@ -20,11 +20,23 @@ public class OutputStreamDownloadSubscriber implements Consumer<ByteBuffer>, Aut
private final boolean allowAutoClose;
/**
* 创建输出流下载订阅器
*
* @param channel 可写通道
* @param allowAutoClose 是否允许自动关闭通道
*/
private OutputStreamDownloadSubscriber(WritableByteChannel channel, boolean allowAutoClose) {
this.channel = channel;
this.allowAutoClose = allowAutoClose;
}
/**
* 创建输出流下载订阅器
*
* @param out 输出流
* @param allowAutoClose 是否允许自动关闭流
*/
private OutputStreamDownloadSubscriber(OutputStream out, boolean allowAutoClose) {
this.allowAutoClose = allowAutoClose;
// 创建可写入的字节通道
@@ -36,6 +48,11 @@ public class OutputStreamDownloadSubscriber implements Consumer<ByteBuffer>, Aut
}
}
/**
* 写入下载到的字节缓冲区
*
* @param byteBuffer 字节缓冲区
*/
@Override
public void accept(ByteBuffer byteBuffer) {
try {
@@ -47,6 +64,11 @@ public class OutputStreamDownloadSubscriber implements Consumer<ByteBuffer>, Aut
}
}
/**
* 按配置关闭底层通道
*
* @throws Exception 关闭通道异常
*/
@Override
public void close() throws Exception {
if (channel.isOpen() && allowAutoClose) {
@@ -85,6 +85,13 @@ public class RateLimiterAspect {
}
}
/**
* 组装限流缓存键
*
* @param rateLimiter 限流注解配置
* @param point 切点信息
* @return 限流缓存键
*/
private String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
String key = rateLimiter.key();
// 判断 key 不为空 不是表达式
@@ -42,7 +42,7 @@ public class RepeatSubmitAspect {
/**
* 请求进入前校验是否重复提交
*
* @param point 切点
* @param point 切点
* @param repeatSubmit 防重复提交注解
*/
@Before("@annotation(repeatSubmit)")
@@ -111,6 +111,9 @@ public class RepeatSubmitAspect {
}
}
/**
* 删除当前请求写入的防重键
*/
private void deleteRepeatKey() {
String key = KEY_CACHE.get();
if (StringUtils.isNotBlank(key)) {
@@ -120,6 +123,9 @@ public class RepeatSubmitAspect {
/**
* 参数拼装
*
* @param paramsArray 方法参数数组
* @return 拼装后的参数字符串
*/
private String argsArrayToString(Object[] paramsArray) {
StringJoiner params = new StringJoiner(" ");
@@ -157,7 +163,7 @@ public class RepeatSubmitAspect {
}
}
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|| o instanceof BindingResult;
|| o instanceof BindingResult;
}
}
@@ -18,7 +18,7 @@ public class CaffeineCacheDecorator implements Cache {
/**
* 创建带 Caffeine 一级缓存的缓存装饰器
*
* @param name 缓存名称
* @param name 缓存名称
* @param cache 被装饰的缓存实例
* @param caffeine 本地一级缓存实例
*/
@@ -73,7 +73,7 @@ public class CaffeineCacheDecorator implements Cache {
/**
* 按指定类型获取缓存值
*
* @param key 缓存键
* @param key 缓存键
* @param type 值类型
* @return 缓存值
*/
@@ -87,7 +87,7 @@ public class CaffeineCacheDecorator implements Cache {
/**
* 写入缓存值并清理本地一级缓存
*
* @param key 缓存键
* @param key 缓存键
* @param value 缓存值
*/
@Override
@@ -99,7 +99,7 @@ public class CaffeineCacheDecorator implements Cache {
/**
* 当键不存在时写入缓存值
*
* @param key 缓存键
* @param key 缓存键
* @param value 缓存值
* @return 原缓存值包装对象
*/
@@ -160,7 +160,7 @@ public class CaffeineCacheDecorator implements Cache {
/**
* 获取缓存值不存在时通过回调加载
*
* @param key 缓存键
* @param key 缓存键
* @param valueLoader 回调加载器
* @return 缓存值
*/
@@ -171,6 +171,9 @@ public class CaffeineCacheDecorator implements Cache {
return (T) o;
}
/**
* 清理当前缓存命名空间下的本地一级缓存
*/
private void clearLocalCache() {
String prefix = name + ":";
caffeine.asMap().keySet().removeIf(key -> key instanceof String cacheKey && cacheKey.startsWith(prefix));
@@ -1,12 +1,12 @@
/**
* Copyright (c) 2013-2021 Nikita Koksharov
*
* <p>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -169,6 +169,14 @@ public class PlusSpringCacheManager implements CacheManager {
return createMapCache(cacheName, name, config, local);
}
/**
* 解析缓存配置支持从模板配置复制并叠加缓存名称中的扩展参数
*
* @param cacheName 完整缓存名称
* @param name 基础缓存名称
* @param array 缓存名称拆分参数
* @return 缓存配置
*/
private CacheConfig resolveCacheConfig(String cacheName, String name, String[] array) {
CacheConfig config = configMap.get(cacheName);
if (config != null) {
@@ -196,6 +204,12 @@ public class PlusSpringCacheManager implements CacheManager {
return config;
}
/**
* 解析是否启用本地一级缓存
*
* @param array 缓存名称拆分参数
* @return 本地缓存开关
*/
private int resolveLocal(String[] array) {
int local = 1;
if (array.length > 4) {
@@ -204,6 +218,12 @@ public class PlusSpringCacheManager implements CacheManager {
return local;
}
/**
* 复制 Redisson 缓存配置避免修改模板配置
*
* @param source 模板缓存配置
* @return 新缓存配置
*/
private CacheConfig copyConfig(CacheConfig source) {
CacheConfig target = new CacheConfig();
target.setTTL(source.getTTL());
@@ -57,6 +57,12 @@ public class CacheUtils {
getRequiredCache(cacheNames).clear();
}
/**
* 获取必需存在的缓存实例
*
* @param cacheNames 缓存组名称
* @return 缓存实例
*/
private static Cache getRequiredCache(String cacheNames) {
Cache cache = CacheManagerHolder.CACHE_MANAGER.getCache(cacheNames);
if (cache == null) {
@@ -21,6 +21,11 @@ import org.springframework.context.annotation.PropertySource;
@PropertySource(value = "classpath:common-satoken.yml", factory = YmlPropertySourceFactory.class)
public class SaTokenConfig {
/**
* 创建 Sa-Token JWT 登录逻辑
*
* @return JWT 登录逻辑
*/
@Bean
public StpLogic getStpLogicJwt() {
// Sa-Token 整合 jwt (简单模式)
@@ -18,6 +18,11 @@ import java.lang.annotation.Target;
@Documented
public @interface Sensitive {
/**
* 脱敏策略
*
* @return 脱敏策略
*/
SensitiveStrategy strategy();
/**
@@ -20,11 +20,25 @@ public class SensitiveJsonFieldProcessor implements JsonFieldProcessor {
@Autowired(required = false)
private SensitiveService sensitiveService;
/**
* 判断字段是否声明了脱敏注解
*
* @param fieldContext 字段上下文
* @return 是否支持脱敏处理
*/
@Override
public boolean supports(JsonFieldContext fieldContext) {
return fieldContext.getAnnotation(Sensitive.class) != null;
}
/**
* 按字段脱敏策略处理字符串值
*
* @param fieldContext 字段上下文
* @param value 字段原始值
* @param context JSON 增强上下文
* @return 脱敏后的字段值
*/
@Override
public Object process(JsonFieldContext fieldContext, Object value, JsonEnhancementContext context) {
Sensitive sensitive = fieldContext.getAnnotation(Sensitive.class);
@@ -24,15 +24,34 @@ import me.zhyd.oauth.utils.UrlBuilder;
*/
public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultRequest {
/**
* 创建企业微信认证请求
*
* @param config 授权配置
* @param source 授权来源
*/
public AbstractAuthWeChatEnterpriseRequest(AuthConfig config, AuthSource source) {
super(config,source);
}
/**
* 创建企业微信认证请求
*
* @param config 授权配置
* @param source 授权来源
* @param authStateCache 授权状态缓存
*/
public AbstractAuthWeChatEnterpriseRequest(AuthConfig config, AuthSource source, AuthStateCache authStateCache) {
super(config, source, authStateCache);
}
/**
* 获取企业微信访问令牌
*
* @param authCallback 授权回调参数
* @return 访问令牌
*/
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
String response = doGetAuthorizationCode(accessTokenUrl(null));
@@ -46,6 +65,12 @@ public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultReq
.build();
}
/**
* 获取企业微信用户信息
*
* @param authToken 访问令牌
* @return 授权用户信息
*/
@Override
public AuthUser getUserInfo(AuthToken authToken) {
String response = doGetUserInfo(authToken);
@@ -26,14 +26,31 @@ import java.util.Map;
*/
public class AuthDingTalkV2Request extends AuthDefaultRequest {
/**
* 创建钉钉 V2 认证请求
*
* @param config 授权配置
*/
public AuthDingTalkV2Request(AuthConfig config) {
super(config, AuthDefaultSource.DINGTALK_V2);
}
/**
* 创建钉钉 V2 认证请求
*
* @param config 授权配置
* @param authStateCache 授权状态缓存
*/
public AuthDingTalkV2Request(AuthConfig config, AuthStateCache authStateCache) {
super(config, AuthDefaultSource.DINGTALK_V2, authStateCache);
}
/**
* 生成钉钉 V2 授权地址
*
* @param state 授权状态
* @return 授权地址
*/
@Override
public String authorize(String state) {
return UrlBuilder.fromBaseUrl(source.authorize())
@@ -50,6 +67,12 @@ public class AuthDingTalkV2Request extends AuthDefaultRequest {
.build();
}
/**
* 获取钉钉 V2 访问令牌
*
* @param authCallback 授权回调参数
* @return 访问令牌
*/
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
Map<String, String> params = new HashMap<>();
@@ -70,6 +93,12 @@ public class AuthDingTalkV2Request extends AuthDefaultRequest {
.build();
}
/**
* 获取钉钉 V2 用户信息
*
* @param authToken 访问令牌
* @return 授权用户信息
*/
@Override
public AuthUser getUserInfo(AuthToken authToken) {
HttpHeader header = new HttpHeader();
@@ -20,6 +20,9 @@ import org.dromara.common.json.utils.JsonUtils;
@Slf4j
public class AuthGiteaRequest extends AuthDefaultRequest {
/**
* 服务器地址
*/
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.gitea.server-url");
/**
@@ -29,10 +32,22 @@ public class AuthGiteaRequest extends AuthDefaultRequest {
super(config, AuthGiteaSource.GITEA);
}
/**
* 创建 Gitea 认证请求
*
* @param config 授权配置
* @param authStateCache 授权状态缓存
*/
public AuthGiteaRequest(AuthConfig config, AuthStateCache authStateCache) {
super(config, AuthGiteaSource.GITEA, authStateCache);
}
/**
* 获取 Gitea 访问令牌
*
* @param authCallback 授权回调参数
* @return 访问令牌
*/
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
String body = doPostAuthorizationCode(authCallback.getCode());
@@ -54,6 +69,12 @@ public class AuthGiteaRequest extends AuthDefaultRequest {
.build();
}
/**
* 使用授权码换取 Gitea 访问令牌响应
*
* @param code 授权码
* @return 令牌接口响应体
*/
@Override
protected String doPostAuthorizationCode(String code) {
HttpRequest request = HttpRequest.post(source.accessToken())
@@ -66,6 +87,12 @@ public class AuthGiteaRequest extends AuthDefaultRequest {
return response.body();
}
/**
* 获取 Gitea 用户信息
*
* @param authToken 访问令牌
* @return 授权用户信息
*/
@Override
public AuthUser getUserInfo(AuthToken authToken) {
String body = doGetUserInfo(authToken);
@@ -16,6 +16,9 @@ import org.dromara.common.json.utils.JsonUtils;
*/
public class AuthMaxKeyRequest extends AuthDefaultRequest {
/**
* 服务器地址
*/
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.maxkey.server-url");
/**
@@ -25,10 +28,22 @@ public class AuthMaxKeyRequest extends AuthDefaultRequest {
super(config, AuthMaxKeySource.MAXKEY);
}
/**
* 创建 MaxKey 认证请求
*
* @param config 授权配置
* @param authStateCache 授权状态缓存
*/
public AuthMaxKeyRequest(AuthConfig config, AuthStateCache authStateCache) {
super(config, AuthMaxKeySource.MAXKEY, authStateCache);
}
/**
* 获取 MaxKey 访问令牌
*
* @param authCallback 授权回调参数
* @return 访问令牌
*/
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
String body = doPostAuthorizationCode(authCallback.getCode());
@@ -50,6 +65,12 @@ public class AuthMaxKeyRequest extends AuthDefaultRequest {
.build();
}
/**
* 获取 MaxKey 用户信息
*
* @param authToken 访问令牌
* @return 授权用户信息
*/
@Override
public AuthUser getUserInfo(AuthToken authToken) {
String body = doGetUserInfo(authToken);
@@ -30,6 +30,9 @@ import static org.dromara.common.social.topiam.AuthTopIamSource.TOPIAM;
@Slf4j
public class AuthTopIamRequest extends AuthDefaultRequest {
/**
* 服务器地址
*/
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.topiam.server-url");
/**
@@ -39,10 +42,22 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
super(config, TOPIAM);
}
/**
* 创建 TopIAM 认证请求
*
* @param config 授权配置
* @param authStateCache 授权状态缓存
*/
public AuthTopIamRequest(AuthConfig config, AuthStateCache authStateCache) {
super(config, TOPIAM, authStateCache);
}
/**
* 获取 TopIAM 访问令牌
*
* @param authCallback 授权回调参数
* @return 访问令牌
*/
@Override
public AuthToken getAccessToken(AuthCallback authCallback) {
String body = doPostAuthorizationCode(authCallback.getCode());
@@ -57,6 +72,12 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
.build();
}
/**
* 获取 TopIAM 用户信息
*
* @param authToken 访问令牌
* @return 授权用户信息
*/
@Override
public AuthUser getUserInfo(AuthToken authToken) {
String body = doGetUserInfo(authToken);
@@ -73,6 +94,12 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
.build();
}
/**
* 使用授权码换取 TopIAM 访问令牌响应
*
* @param code 授权码
* @return 令牌接口响应体
*/
@Override
protected String doPostAuthorizationCode(String code) {
HttpRequest request = HttpRequest.post(source.accessToken())
@@ -84,6 +111,12 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
return response.body();
}
/**
* 获取 TopIAM 用户信息接口响应
*
* @param authToken 访问令牌
* @return 用户信息响应体
*/
@Override
protected String doGetUserInfo(AuthToken authToken) {
return new HttpUtils(config.getHttpConfig()).get(source.userInfo(), null, new HttpHeader()
@@ -92,6 +125,12 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
}
/**
* 生成 TopIAM 授权地址
*
* @param state 授权状态
* @return 授权地址
*/
@Override
public String authorize(String state) {
return UrlBuilder.fromBaseUrl(super.authorize(state))
@@ -99,6 +138,11 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
.build();
}
/**
* 校验 TopIAM 接口响应
*
* @param object 响应内容
*/
private static void checkResponse(Dict object) {
// oauth/token 验证异常
if (object.containsKey("error")) {
@@ -23,6 +23,7 @@ public class FilterConfig {
/**
* 注册 XSS 过滤器
*
* @param xssProperties XSS 配置
* @return XSS 请求过滤器实例
*/
@Bean
@@ -21,6 +21,9 @@ import java.nio.charset.StandardCharsets;
* @author ruoyi
*/
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
/**
* 请求体字节数据
*/
private final byte[] body;
/**
@@ -59,26 +62,53 @@ public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
/**
* 读取缓存请求体的下一个字节
*
* @return 下一个字节
* @throws IOException IO 异常
*/
@Override
public int read() throws IOException {
return bais.read();
}
/**
* 返回缓存请求体剩余可读字节数
*
* @return 剩余字节数
* @throws IOException IO 异常
*/
@Override
public int available() throws IOException {
return bais.available();
}
/**
* 判断缓存请求体是否已读取完毕
*
* @return 是否读取完毕
*/
@Override
public boolean isFinished() {
return bais.available() == 0;
}
/**
* 判断输入流是否可读
*
* @return 固定为 true
*/
@Override
public boolean isReady() {
return true;
}
/**
* 设置异步读取监听器
*
* @param readListener 读取监听器
*/
@Override
public void setReadListener(ReadListener readListener) {
@@ -124,25 +124,52 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
final ByteArrayInputStream bis = IoUtil.toStream(jsonBytes);
return new ServletInputStream() {
/**
* 判断清洗后的 JSON 流是否已读取完毕
*
* @return 是否读取完毕
*/
@Override
public boolean isFinished() {
return true;
}
/**
* 判断清洗后的 JSON 流是否可读
*
* @return 固定为 true
*/
@Override
public boolean isReady() {
return true;
}
/**
* 返回清洗后的 JSON 字节数
*
* @return JSON 字节数
* @throws IOException IO 异常
*/
@Override
public int available() throws IOException {
return jsonBytes.length;
}
/**
* 设置异步读取监听器
*
* @param readListener 读取监听器
*/
@Override
public void setReadListener(ReadListener readListener) {
}
/**
* 读取清洗后的 JSON 流下一个字节
*
* @return 下一个字节
* @throws IOException IO 异常
*/
@Override
public int read() throws IOException {
return bis.read();