diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..eb43c96 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,119 @@ +# CLAUDE.md + +这个文件为Claude Code (claude.ai/code)提供在此代码库中工作的指导。 + +## 项目概述 + +ZKH Framework是一个基于Spring Boot 3.5.7和Java 21的企业级Java开发框架,采用多模块Maven架构。框架提供了Web开发、数据访问、安全认证等核心功能。 + +## 模块架构 + +项目采用多模块架构,包含以下核心模块: + +### zkh-common +- **作用**: 通用工具类和基础实体类 +- **关键组件**: + - `R`: 统一响应结果封装类 + - `BaseEntity`: 基础实体类,包含审计字段(id, createTime, updateTime, createBy, updateBy) + - `BizException`: 业务异常类 +- **依赖**: Spring Data JPA, Jakarta Persistence API, Jackson Annotations + +### zkh-web +- **作用**: Web层组件和安全配置 +- **关键组件**: + - `WebSecurityConfig`: Spring Security配置,支持Token认证 + - `TokenFilter`: Token认证过滤器 + - `JsonUsernamePasswordAuthenticationFilter`: JSON格式的用户名密码认证 + - `TokenRedisStorage`: Redis Token存储管理 + - `GlobalExceptionHandler`: 全局异常处理 +- **特性**: + - 基于Redis的Token认证机制 + - 支持CORS和JSON格式登录 + - 自动清理过期Token(每30分钟) + - JPA审计支持 + +### zkh-data +- **作用**: 数据访问层组件 +- **关键组件**: + - `DynamicSpecificationBuilder`: 动态Specification构建器,支持复杂查询条件 + - `FieldMatch`: 字段匹配注解,用于查询条件映射 + - `MatchType`: 匹配类型枚举(EQUALS, CONTAINS, GREATER_THAN等) + - `LogicalOperator`: 逻辑操作符(AND, OR) +- **特性**: + - 基于注解的动态查询构建 + - 支持嵌套属性查询(如: user.address.city) + - 查询条件缓存机制 + - 支持AND/OR逻辑组合 + +## 构建和开发 + +### 构建命令 +```bash +# 编译整个项目 +mvn clean compile + +# 运行测试 +mvn test + +# 打包项目 +mvn clean package + +# 跳过测试打包 +mvn clean package -DskipTests + +# 安装到本地仓库 +mvn clean install + +# 发布到Maven Central(需要GPG签名) +mvn clean deploy -P release +``` + +### 开发环境要求 +- JDK 21 +- Maven 3.6+ +- Redis(用于Token存储) +- 数据库(支持JPA的任何数据库) + +## 核心技术栈 + +- **Spring Boot**: 3.5.7 +- **Spring Security**: 认证和授权 +- **Spring Data JPA**: 数据访问层 +- **Redis**: Token存储和缓存 +- **Jackson**: JSON序列化/反序列化 +- **Lombok**: 减少样板代码 +- **BCrypt**: 密码加密 + +## 使用指南 + +### Token认证 +- 登录成功后返回UUID格式的Token +- 请求时在Header中添加 `Authorization: Bearer ` +- 支持单点登出和全部登出 + +### 动态查询 +使用`@FieldMatch`注解在DTO字段上配置查询条件: +```java +@FieldMatch(matchType = MatchType.CONTAINS, ignoreCase = true) +private String name; + +@FieldMatch(matchType = MatchType.BETWEEN) +private Date[] createTimeRange; +``` + +### 审计功能 +继承`BaseEntity`类自动获得: +- 主键自增 +- 创建/更新时间自动记录 +- 创建/更新人自动记录 + +## 项目配置 + +### 安全配置 +通过`SecurityProps`配置忽略认证的URL路径。 + +### Redis配置 +确保Redis服务可用,用于Token存储和管理。 + +### 数据库配置 +配置Spring Boot数据源以使用JPA功能。 \ No newline at end of file diff --git a/pom.xml b/pom.xml index bb4bfbf..867c06e 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ vip.jcfd zkh-framework - 1.1 + 1.2 pom ZKH Framework A Java framework for ZKH applications @@ -66,6 +66,11 @@ zkh-web ${project.version} + + vip.jcfd + zkh-data + ${project.version} + diff --git a/zkh-common/pom.xml b/zkh-common/pom.xml index 10fb54a..95c4c7d 100644 --- a/zkh-common/pom.xml +++ b/zkh-common/pom.xml @@ -6,7 +6,7 @@ vip.jcfd zkh-framework - 1.1 + 1.2 zkh-common diff --git a/zkh-data/pom.xml b/zkh-data/pom.xml index 122932a..46f31d2 100644 --- a/zkh-data/pom.xml +++ b/zkh-data/pom.xml @@ -6,7 +6,7 @@ vip.jcfd zkh-framework - 1.1 + 1.2 zkh-data @@ -19,8 +19,41 @@ UTF-8 + + + jakarta.persistence + jakarta.persistence-api + + + org.springframework.data + spring-data-jpa + + + org.projectlombok + lombok + + + com.fasterxml.jackson.core + jackson-annotations + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + + + org.projectlombok + lombok + 1.18.42 + + + + org.apache.maven.plugins maven-source-plugin diff --git a/zkh-data/src/main/java/vip/jcfd/data/util/PageableFactory.java b/zkh-data/src/main/java/vip/jcfd/data/util/PageableFactory.java new file mode 100644 index 0000000..b7f95f8 --- /dev/null +++ b/zkh-data/src/main/java/vip/jcfd/data/util/PageableFactory.java @@ -0,0 +1,35 @@ +package vip.jcfd.data.util; + + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.util.StringUtils; + +public interface PageableFactory { + + int PAGE = 0; + + int SIZE = 10; + + int page(); + + int size(); + + String sortDir(); + + String sortBy(); + + @JsonIgnore + default Pageable getPageable() { + String dir = sortDir(); + String by = sortBy(); + Sort sort = Sort.unsorted(); + if (StringUtils.hasText(by)) { + dir = StringUtils.hasText(dir) ? dir : "asc"; + sort = Sort.by(Sort.Direction.fromString(dir), by); + } + return PageRequest.of(page(), size(), sort); + } +} diff --git a/zkh-data/src/main/java/vip/jcfd/data/util/spec/DynamicSpecificationBuilder.java b/zkh-data/src/main/java/vip/jcfd/data/util/spec/DynamicSpecificationBuilder.java new file mode 100644 index 0000000..0b1c089 --- /dev/null +++ b/zkh-data/src/main/java/vip/jcfd/data/util/spec/DynamicSpecificationBuilder.java @@ -0,0 +1,657 @@ +package vip.jcfd.data.util.spec; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; + +import java.lang.reflect.Field; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 动态Specification构建器 + * 根据DTO中的字段和注解配置,动态生成JPA Specification查询条件 + * 优化版本:添加缓存机制避免重复反射操作 + */ +@Slf4j +public class DynamicSpecificationBuilder { + + // 实体字段缓存 - 避免重复反射检查字段是否存在 + private static final Map ENTITY_FIELD_CACHE = new ConcurrentHashMap<>(); + + // DTO字段信息缓存 - 缓存DTO的字段信息和注解配置 + private static final Map, List> DTO_FIELD_CACHE = new ConcurrentHashMap<>(); + + // 嵌套字段路径缓存 - 缓存嵌套字段的类型信息 + private static final Map> NESTED_FIELD_CACHE = new ConcurrentHashMap<>(); + + /** + * 字段信息缓存类 + */ + private static class FieldInfo { + final Field field; + final FieldMatch fieldMatch; + final String fieldName; + final boolean hasAnnotation; + final MatchType defaultMatchType; + final boolean defaultIgnoreCase; + final boolean defaultIgnoreEmpty; + final LogicalOperator defaultLogicalOperator; + + public FieldInfo(Field field, FieldMatch fieldMatch, String fieldName) { + this.field = field; + this.fieldMatch = fieldMatch; + this.fieldName = fieldName; + this.hasAnnotation = fieldMatch != null; + + if (hasAnnotation) { + this.defaultMatchType = fieldMatch.matchType(); + this.defaultIgnoreCase = fieldMatch.ignoreCase(); + this.defaultIgnoreEmpty = fieldMatch.ignoreEmpty(); + this.defaultLogicalOperator = fieldMatch.logicalOperator(); + } else { + // 根据字段类型确定默认匹配类型 + Class fieldType = field.getType(); + this.defaultMatchType = getDefaultMatchType(fieldType); + this.defaultIgnoreCase = String.class.equals(fieldType); + this.defaultIgnoreEmpty = true; + this.defaultLogicalOperator = LogicalOperator.AND; + } + } + + /** + * 根据字段类型获取默认匹配类型 + */ + private MatchType getDefaultMatchType(Class fieldType) { + if (String.class.equals(fieldType)) { + return MatchType.EQUALS; + } else if (Number.class.isAssignableFrom(fieldType) || + fieldType.isPrimitive() && isNumericType(fieldType)) { + return MatchType.EQUALS; + } else if (Boolean.class.equals(fieldType) || boolean.class.equals(fieldType)) { + return MatchType.EQUALS; + } else if (java.util.Date.class.isAssignableFrom(fieldType) || + java.time.temporal.Temporal.class.isAssignableFrom(fieldType)) { + return MatchType.EQUALS; + } else if (java.util.Collection.class.isAssignableFrom(fieldType) || + fieldType.isArray()) { + return MatchType.IN; + } else { + return MatchType.EQUALS; + } + } + + /** + * 检查是否为数值类型 + */ + private boolean isNumericType(Class type) { + return type == int.class || type == long.class || type == double.class || + type == float.class || type == short.class || type == byte.class; + } + } + + /** + * 根据DTO动态生成Specification + * 优化版本:使用缓存避免重复反射 + * + * @param 实体类型 + * @param entityClass 实体类Class对象 + * @param dto 查询条件DTO + * @return 构建好的Specification对象 + */ + public static Specification buildSpecification(Class entityClass, Object dto) { + return (root, query, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + + try { + // 从缓存获取DTO字段信息 + List fieldInfos = getDtoFieldInfos(dto.getClass()); + + for (FieldInfo fieldInfo : fieldInfos) { + // 跳过静态字段、final字段和transient字段 + if (shouldSkipField(fieldInfo.field)) { + continue; + } + + // 获取字段值 + Object fieldValue = fieldInfo.field.get(dto); + + // 处理字段(支持有注解和无注解的字段) + processFieldOptimized(root, criteriaBuilder, predicates, fieldInfo, fieldValue, entityClass); + } + + } catch (Exception e) { + log.error("构建Specification时发生错误", e); + throw new RuntimeException("构建Specification失败", e); + } + + return criteriaBuilder.and(predicates.toArray(new Predicate[0])); + }; + } + + /** + * 获取DTO字段信息(带缓存) + */ + private static List getDtoFieldInfos(Class dtoClass) { + return DTO_FIELD_CACHE.computeIfAbsent(dtoClass, clazz -> { + List fieldInfos = new ArrayList<>(); + Field[] fields = clazz.getDeclaredFields(); + + for (Field field : fields) { + field.setAccessible(true); + FieldMatch fieldMatch = field.getAnnotation(FieldMatch.class); + fieldInfos.add(new FieldInfo(field, fieldMatch, field.getName())); + } + + return fieldInfos; + }); + } + + /** + * 优化版本:处理单个字段,生成对应的Predicate + * 支持有注解和无注解的字段 + */ + private static void processFieldOptimized(Root root, CriteriaBuilder cb, + List predicates, FieldInfo fieldInfo, + Object fieldValue, Class entityClass) throws Exception { + + // 获取匹配配置(有注解则使用注解,否则使用默认配置) + boolean ignoreEmpty = fieldInfo.hasAnnotation ? fieldInfo.fieldMatch.ignoreEmpty() : fieldInfo.defaultIgnoreEmpty; + String entityFieldName = fieldInfo.hasAnnotation && StringUtils.hasText(fieldInfo.fieldMatch.fieldName()) + ? fieldInfo.fieldMatch.fieldName() + : fieldInfo.fieldName; + MatchType matchType = fieldInfo.hasAnnotation ? fieldInfo.fieldMatch.matchType() : fieldInfo.defaultMatchType; + boolean ignoreCase = fieldInfo.hasAnnotation ? fieldInfo.fieldMatch.ignoreCase() : fieldInfo.defaultIgnoreCase; + LogicalOperator logicalOperator = fieldInfo.hasAnnotation ? fieldInfo.fieldMatch.logicalOperator() : fieldInfo.defaultLogicalOperator; + + // 检查是否忽略空值 + if (ignoreEmpty && isEmptyValue(fieldValue)) { + return; + } + + // 使用缓存检查实体类中是否存在该字段 + String cacheKey = entityClass.getName() + "." + entityFieldName; + Boolean fieldExists = ENTITY_FIELD_CACHE.get(cacheKey); + if (fieldExists == null) { + fieldExists = entityFieldExistsOptimized(entityClass, entityFieldName); + ENTITY_FIELD_CACHE.put(cacheKey, fieldExists); + } + + if (!fieldExists) { + // 对于没有注解的字段,如果实体类中不存在该字段,跳过但不警告(避免过多日志) + if (fieldInfo.hasAnnotation) { + log.warn("实体类 {} 中不存在字段: {}, 忽略该查询条件", + entityClass.getSimpleName(), entityFieldName); + } + return; + } + + // 获取字段路径 + Path fieldPath = getNestedPath(root, entityFieldName); + + // 根据匹配类型生成Predicate + Predicate predicate = createPredicateWithConfig(cb, fieldPath, fieldValue, matchType, ignoreCase); + + if (predicate != null) { + // 根据逻辑操作符决定如何添加到predicates列表 + if (logicalOperator == LogicalOperator.OR) { + // OR逻辑需要在更高层次处理,这里先标记 + predicates.add(predicate); + } else { + // AND逻辑直接添加 + predicates.add(predicate); + } + } + } + + /** + * 根据配置创建Predicate(支持有注解和无注解的字段) + */ + private static Predicate createPredicateWithConfig(CriteriaBuilder cb, Path fieldPath, + Object fieldValue, MatchType matchType, boolean ignoreCase) { + + Class fieldType = fieldPath.getJavaType(); + + try { + switch (matchType) { + case EQUALS: + return cb.equal(fieldPath, fieldValue); + + case NOT_EQUALS: + return cb.notEqual(fieldPath, fieldValue); + + case GREATER_THAN: + return cb.greaterThan(fieldPath.as(Comparable.class), (Comparable) fieldValue); + + case GREATER_THAN_OR_EQUAL: + return cb.greaterThanOrEqualTo(fieldPath.as(Comparable.class), (Comparable) fieldValue); + + case LESS_THAN: + return cb.lessThan(fieldPath.as(Comparable.class), (Comparable) fieldValue); + + case LESS_THAN_OR_EQUAL: + return cb.lessThanOrEqualTo(fieldPath.as(Comparable.class), (Comparable) fieldValue); + + case CONTAINS: + if (ignoreCase) { + return cb.like(cb.lower(fieldPath.as(String.class)), + "%" + fieldValue.toString().toLowerCase() + "%"); + } else { + return cb.like(fieldPath.as(String.class), "%" + fieldValue + "%"); + } + + case NOT_CONTAINS: + if (ignoreCase) { + return cb.notLike(cb.lower(fieldPath.as(String.class)), + "%" + fieldValue.toString().toLowerCase() + "%"); + } else { + return cb.notLike(fieldPath.as(String.class), "%" + fieldValue + "%"); + } + + case STARTS_WITH: + if (ignoreCase) { + return cb.like(cb.lower(fieldPath.as(String.class)), + fieldValue.toString().toLowerCase() + "%"); + } else { + return cb.like(fieldPath.as(String.class), fieldValue + "%"); + } + + case NOT_STARTS_WITH: + if (ignoreCase) { + return cb.notLike(cb.lower(fieldPath.as(String.class)), + fieldValue.toString().toLowerCase() + "%"); + } else { + return cb.notLike(fieldPath.as(String.class), fieldValue + "%"); + } + + case ENDS_WITH: + if (ignoreCase) { + return cb.like(cb.lower(fieldPath.as(String.class)), + "%" + fieldValue.toString().toLowerCase()); + } else { + return cb.like(fieldPath.as(String.class), "%" + fieldValue); + } + + case NOT_ENDS_WITH: + if (ignoreCase) { + return cb.notLike(cb.lower(fieldPath.as(String.class)), + "%" + fieldValue.toString().toLowerCase()); + } else { + return cb.notLike(fieldPath.as(String.class), "%" + fieldValue); + } + + case IGNORE_CASE_CONTAINS: + return cb.like(cb.lower(fieldPath.as(String.class)), + "%" + fieldValue.toString().toLowerCase() + "%"); + + case IS_NULL: + return fieldPath.isNull(); + + case IS_NOT_NULL: + return fieldPath.isNotNull(); + + case IN: + if (fieldValue instanceof Collection) { + return fieldPath.in((Collection) fieldValue); + } + return null; + + case NOT_IN: + if (fieldValue instanceof Collection) { + return cb.not(fieldPath.in((Collection) fieldValue)); + } + return null; + + case IS_TRUE: + return cb.equal(fieldPath, true); + + case IS_FALSE: + return cb.equal(fieldPath, false); + + case BETWEEN: + if (fieldValue instanceof Object[] && ((Object[]) fieldValue).length == 2) { + Object[] values = (Object[]) fieldValue; + return cb.between(fieldPath.as(Comparable.class), + (Comparable) values[0], (Comparable) values[1]); + } + return null; + + default: + log.warn("不支持的匹配类型: {}", matchType); + return null; + } + } catch (Exception e) { + log.error("创建Predicate时发生错误,字段: {}, 匹配类型: {}, 值: {}", + fieldPath, matchType, fieldValue, e); + return null; + } + } + + /** + * 根据匹配类型创建Predicate(保持向后兼容的旧方法) + */ + private static Predicate createPredicate(CriteriaBuilder cb, Path fieldPath, + Object fieldValue, FieldMatch fieldMatch) { + return createPredicateWithConfig(cb, fieldPath, fieldValue, + fieldMatch.matchType(), fieldMatch.ignoreCase()); + } + + /** + * 判断是否应该跳过字段 + */ + private static boolean shouldSkipField(Field field) { + int modifiers = field.getModifiers(); + + // 跳过静态字段 + if (java.lang.reflect.Modifier.isStatic(modifiers)) { + return true; + } + + // 跳过transient字段 + if (java.lang.reflect.Modifier.isTransient(modifiers)) { + return true; + } + + // 跳过有@Transient注解的字段 + if (field.getAnnotation(jakarta.persistence.Transient.class) != null) { + return true; + } + + // 跳过serialVersionUID字段 + if ("serialVersionUID".equals(field.getName())) { + return true; + } + + return false; + } + + /** + * 获取嵌套字段路径 + * 支持对象属性访问,如: user.address.city + */ + private static Path getNestedPath(Root root, String fieldName) { + if (!fieldName.contains(".")) { + return root.get(fieldName); + } + + String[] parts = fieldName.split("\\."); + Path path = root.get(parts[0]); + + for (int i = 1; i < parts.length; i++) { + path = path.get(parts[i]); + } + + return path; + } + + /** + * 优化版本:检查实体类中是否存在指定字段 + */ + private static boolean entityFieldExistsOptimized(Class entityClass, String fieldName) { + try { + // 支持嵌套属性检查 + if (fieldName.contains(".")) { + String[] parts = fieldName.split("\\."); + Class currentClass = entityClass; + StringBuilder pathBuilder = new StringBuilder(entityClass.getName()); + + for (String part : parts) { + pathBuilder.append(".").append(part); + + // 检查当前路径是否已缓存 + String pathCacheKey = pathBuilder.toString(); + Class cachedType = NESTED_FIELD_CACHE.get(pathCacheKey); + + Field field; + if (cachedType != null) { + field = ReflectionUtils.findField(cachedType, part); + } else { + field = ReflectionUtils.findField(currentClass, part); + } + + if (field == null) { + return false; + } + + // 缓存字段类型 + if (cachedType == null) { + NESTED_FIELD_CACHE.put(pathCacheKey, currentClass); + } + + currentClass = field.getType(); + } + return true; + } else { + return ReflectionUtils.findField(entityClass, fieldName) != null; + } + } catch (Exception e) { + return false; + } + } + + /** + * 检查实体类中是否存在指定字段(原始版本,保留用于缓存未命中时的检查) + */ + private static boolean entityFieldExists(Class entityClass, String fieldName) { + return entityFieldExistsOptimized(entityClass, fieldName); + } + + /** + * 优化版本:判断值是否为空 + */ + private static boolean isEmptyValue(Object value) { + if (value == null) { + return true; + } + + if (value instanceof String) { + return !StringUtils.hasText((String) value); + } + + if (value instanceof Collection) { + return ((Collection) value).isEmpty(); + } + + if (value instanceof Object[]) { + return ((Object[]) value).length == 0; + } + + return false; + } + + /** + * 构建带逻辑OR的Specification + * 优化版本:使用缓存避免重复反射,支持有注解和无注解的字段 + */ + public static Specification buildOrSpecification(Class entityClass, Object dto) { + return (root, query, criteriaBuilder) -> { + List predicates = new ArrayList<>(); + + try { + // 从缓存获取DTO字段信息 + List fieldInfos = getDtoFieldInfos(dto.getClass()); + + for (FieldInfo fieldInfo : fieldInfos) { + // 跳过静态字段、final字段和transient字段 + if (shouldSkipField(fieldInfo.field)) { + continue; + } + + // 只处理OR类型的字段 + LogicalOperator logicalOperator = fieldInfo.hasAnnotation + ? fieldInfo.fieldMatch.logicalOperator() + : fieldInfo.defaultLogicalOperator; + + if (logicalOperator == LogicalOperator.OR) { + // 获取字段值 + Object fieldValue = fieldInfo.field.get(dto); + + // 处理字段 + processFieldOptimized(root, criteriaBuilder, predicates, fieldInfo, fieldValue, entityClass); + } + } + + } catch (Exception e) { + log.error("构建OR Specification时发生错误", e); + throw new RuntimeException("构建OR Specification失败", e); + } + + return predicates.isEmpty() ? null : criteriaBuilder.or(predicates.toArray(new Predicate[0])); + }; + } + + /** + * 构建复杂的Specification,支持AND和OR组合 + * 在单个Specification中处理所有的AND和OR逻辑 + */ + public static Specification buildComplexSpecification(Class entityClass, Object dto) { + return (root, query, criteriaBuilder) -> { + List andPredicates = new ArrayList<>(); + List orPredicates = new ArrayList<>(); + + try { + // 从缓存获取DTO字段信息 + List fieldInfos = getDtoFieldInfos(dto.getClass()); + + for (FieldInfo fieldInfo : fieldInfos) { + // 跳过静态字段、final字段和transient字段 + if (shouldSkipField(fieldInfo.field)) { + continue; + } + + // 获取字段值 + Object fieldValue = fieldInfo.field.get(dto); + + // 获取逻辑操作符 + LogicalOperator logicalOperator = fieldInfo.hasAnnotation + ? fieldInfo.fieldMatch.logicalOperator() + : fieldInfo.defaultLogicalOperator; + + // 根据逻辑操作符决定添加到哪个列表 + if (logicalOperator == LogicalOperator.OR) { + processFieldOptimized(root, criteriaBuilder, orPredicates, fieldInfo, fieldValue, entityClass); + } else { + processFieldOptimized(root, criteriaBuilder, andPredicates, fieldInfo, fieldValue, entityClass); + } + } + + } catch (Exception e) { + log.error("构建复杂Specification时发生错误", e); + throw new RuntimeException("构建复杂Specification失败", e); + } + + // 组合AND和OR条件 + Predicate andPredicate = andPredicates.isEmpty() ? null : + criteriaBuilder.and(andPredicates.toArray(new Predicate[0])); + Predicate orPredicate = orPredicates.isEmpty() ? null : + criteriaBuilder.or(orPredicates.toArray(new Predicate[0])); + + if (andPredicate != null && orPredicate != null) { + return criteriaBuilder.and(andPredicate, orPredicate); + } else if (andPredicate != null) { + return andPredicate; + } else { + return orPredicate; + } + }; + } + + /** + * 清理缓存 - 用于内存管理或开发环境热重载 + */ + public static void clearCache() { + ENTITY_FIELD_CACHE.clear(); + DTO_FIELD_CACHE.clear(); + NESTED_FIELD_CACHE.clear(); + log.debug("DynamicSpecificationBuilder 缓存已清理"); + } + + /** + * 清理指定实体类的缓存 + */ + public static void clearEntityCache(Class entityClass) { + String entityName = entityClass.getName(); + ENTITY_FIELD_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(entityName + ".")); + NESTED_FIELD_CACHE.entrySet().removeIf(entry -> entry.getKey().startsWith(entityName + ".")); + log.debug("实体类 {} 的缓存已清理", entityClass.getSimpleName()); + } + + /** + * 清理指定DTO类的缓存 + */ + public static void clearDtoCache(Class dtoClass) { + DTO_FIELD_CACHE.remove(dtoClass); + log.debug("DTO类 {} 的缓存已清理", dtoClass.getSimpleName()); + } + + /** + * 获取缓存统计信息 + */ + public static Map getCacheStats() { + Map stats = new HashMap<>(); + stats.put("entityFieldCacheSize", ENTITY_FIELD_CACHE.size()); + stats.put("dtoFieldCacheSize", DTO_FIELD_CACHE.size()); + stats.put("nestedFieldCacheSize", NESTED_FIELD_CACHE.size()); + return stats; + } + + /** + * 预热缓存 - 用于应用启动时预加载常用实体和DTO的字段信息 + */ + public static void warmupCache(Class... entityClasses) { + for (Class entityClass : entityClasses) { + try { + // 预加载实体字段信息 + Field[] fields = entityClass.getDeclaredFields(); + for (Field field : fields) { + String fieldName = field.getName(); + String cacheKey = entityClass.getName() + "." + fieldName; + ENTITY_FIELD_CACHE.computeIfAbsent(cacheKey, k -> entityFieldExistsOptimized(entityClass, fieldName)); + } + } catch (Exception e) { + log.warn("预热缓存时发生错误,实体类: {}", entityClass.getSimpleName(), e); + } + } + log.debug("缓存预热完成,处理了 {} 个实体类", entityClasses.length); + } + + /** + * 批量处理DTO - 优化版本,支持批量查询减少重复计算 + * 支持有注解和无注解的字段 + */ + public static Specification buildSpecificationForBatch(Class entityClass, List dtoList) { + return (root, query, criteriaBuilder) -> { + List allPredicates = new ArrayList<>(); + + for (Object dto : dtoList) { + try { + List fieldInfos = getDtoFieldInfos(dto.getClass()); + List dtoPredicates = new ArrayList<>(); + + for (FieldInfo fieldInfo : fieldInfos) { + // 跳过静态字段、final字段和transient字段 + if (shouldSkipField(fieldInfo.field)) { + continue; + } + + Object fieldValue = fieldInfo.field.get(dto); + processFieldOptimized(root, criteriaBuilder, dtoPredicates, fieldInfo, fieldValue, entityClass); + } + + if (!dtoPredicates.isEmpty()) { + allPredicates.add(criteriaBuilder.and(dtoPredicates.toArray(new Predicate[0]))); + } + } catch (Exception e) { + log.error("批量处理DTO时发生错误", e); + } + } + + return allPredicates.isEmpty() ? null : criteriaBuilder.or(allPredicates.toArray(new Predicate[0])); + }; + } +} diff --git a/zkh-data/src/main/java/vip/jcfd/data/util/spec/FieldMatch.java b/zkh-data/src/main/java/vip/jcfd/data/util/spec/FieldMatch.java new file mode 100644 index 0000000..1d544de --- /dev/null +++ b/zkh-data/src/main/java/vip/jcfd/data/util/spec/FieldMatch.java @@ -0,0 +1,46 @@ +package vip.jcfd.data.util.spec; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 字段匹配配置注解 + * 用于标记DTO字段与实体字段的匹配方式 + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface FieldMatch { + + /** + * 指定实体类中的字段名 + * 如果不指定,则使用DTO字段名 + */ + String fieldName() default ""; + + /** + * 匹配类型 + * 默认为精确匹配 + */ + MatchType matchType() default MatchType.EQUALS; + + /** + * 是否忽略大小写 + * 仅对字符串类型的字段有效 + */ + boolean ignoreCase() default false; + + /** + * 是否允许忽略空值 + * 如果为true,当字段值为null或空字符串时,将忽略该字段的查询条件 + * 默认为true + */ + boolean ignoreEmpty() default true; + + /** + * 自定义连接操作符 + * 默认使用AND连接多个条件 + */ + LogicalOperator logicalOperator() default LogicalOperator.AND; +} diff --git a/zkh-data/src/main/java/vip/jcfd/data/util/spec/LogicalOperator.java b/zkh-data/src/main/java/vip/jcfd/data/util/spec/LogicalOperator.java new file mode 100644 index 0000000..dcf69c1 --- /dev/null +++ b/zkh-data/src/main/java/vip/jcfd/data/util/spec/LogicalOperator.java @@ -0,0 +1,16 @@ +package vip.jcfd.data.util.spec; + +/** + * 逻辑操作符枚举 + */ +public enum LogicalOperator { + /** + * 并且 (AND) + */ + AND, + + /** + * 或者 (OR) + */ + OR +} diff --git a/zkh-data/src/main/java/vip/jcfd/data/util/spec/MatchType.java b/zkh-data/src/main/java/vip/jcfd/data/util/spec/MatchType.java new file mode 100644 index 0000000..6c8d1da --- /dev/null +++ b/zkh-data/src/main/java/vip/jcfd/data/util/spec/MatchType.java @@ -0,0 +1,106 @@ +package vip.jcfd.data.util.spec; + +/** + * 字段匹配类型枚举 + */ +public enum MatchType { + /** + * 精确匹配 (equals) + */ + EQUALS, + + /** + * 不等于 (not equals) + */ + NOT_EQUALS, + + /** + * 大于 (greater than) + */ + GREATER_THAN, + + /** + * 大于等于 (greater than or equal) + */ + GREATER_THAN_OR_EQUAL, + + /** + * 小于 (less than) + */ + LESS_THAN, + + /** + * 小于等于 (less than or equal) + */ + LESS_THAN_OR_EQUAL, + + /** + * 包含 (contains) - 用于字符串 + */ + CONTAINS, + + /** + * 不包含 (not contains) - 用于字符串 + */ + NOT_CONTAINS, + + /** + * 以...开头 (starts with) - 用于字符串 + */ + STARTS_WITH, + + /** + * 不以...开头 (not starts with) - 用于字符串 + */ + NOT_STARTS_WITH, + + /** + * 以...结尾 (ends with) - 用于字符串 + */ + ENDS_WITH, + + /** + * 不以...结尾 (not ends with) - 用于字符串 + */ + NOT_ENDS_WITH, + + /** + * 忽略大小写的包含 (ignore case contains) - 用于字符串 + */ + IGNORE_CASE_CONTAINS, + + /** + * 为空 (is null) + */ + IS_NULL, + + /** + * 不为空 (is not null) + */ + IS_NOT_NULL, + + /** + * 在集合中 (in) + */ + IN, + + /** + * 不在集合中 (not in) + */ + NOT_IN, + + /** + * 为true (is true) - 用于布尔字段 + */ + IS_TRUE, + + /** + * 为false (is false) - 用于布尔字段 + */ + IS_FALSE, + + /** + * 日期范围 (between) - 用于日期字段 + */ + BETWEEN +} diff --git a/zkh-data/src/main/java/vip/jcfd/data/util/spec/README.md b/zkh-data/src/main/java/vip/jcfd/data/util/spec/README.md new file mode 100644 index 0000000..49a0402 --- /dev/null +++ b/zkh-data/src/main/java/vip/jcfd/data/util/spec/README.md @@ -0,0 +1,257 @@ +# DynamicSpecificationBuilder 工具类 + +这是一个用于动态生成Spring Data JPA Specifications的工具类,可以根据DTO中的字段和注解配置,自动构建复杂的查询条件。 + +## 核心功能 + +1. **动态字段匹配**:根据DTO字段自动匹配实体类字段 +2. **多种匹配方式**:支持equals、contains、between、in等多种匹配类型 +3. **注解配置**:通过注解灵活配置查询条件 +4. **默认匹配机制**:支持没有注解的字段,根据字段类型自动选择匹配方式 +5. **嵌套属性支持**:支持对象属性访问,如`user.address.city` +6. **空值处理**:自动忽略空值或空字符串字段 +7. **类型安全**:自动进行类型转换和验证 +8. **智能字段过滤**:自动跳过静态、final、transient字段 +9. **缓存机制**:使用缓存提高反射操作性能 + +## 使用方式 + +### 1. 在DTO中使用注解(推荐) + +```java +@Data +public class UserSearchDto { + + @FieldMatch(matchType = MatchType.CONTAINS, ignoreEmpty = true) + private String username; + + @FieldMatch(matchType = MatchType.EQUALS, ignoreEmpty = true) + private String email; + + @FieldMatch( + matchType = MatchType.BETWEEN, + fieldName = "createTime", + ignoreEmpty = true + ) + private LocalDateTime[] createTimeRange; + + @FieldMatch( + matchType = MatchType.IN, + fieldName = "roles.id", + ignoreEmpty = true + ) + private List roleIds; +} +``` + +### 2. 使用默认匹配机制(无注解) + +```java +@Data +public class SimpleUserSearchDto { + // 字符串类型:默认使用EQUALS匹配 + private String username; + + // 数值类型:默认使用EQUALS匹配 + private Integer age; + + // 布尔类型:默认使用EQUALS匹配 + private Boolean active; + + // 集合类型:默认使用IN匹配 + private List departmentIds; + + // 日期类型:默认使用EQUALS匹配 + private LocalDateTime createTime; + + // 被忽略的字段: + private static final String IGNORED_CONSTANT = "ignored"; // static字段被忽略 + private transient Long transientField; // transient字段被忽略 + @Transient private String annotationIgnored; // @Transient注解字段被忽略 +} +``` + +### 3. 混合使用 + +```java +@Data +public class MixedUserSearchDto { + // 使用注解的自定义配置 + @FieldMatch(matchType = MatchType.CONTAINS, ignoreCase = true) + private String name; + + // 使用默认匹配机制 + private Integer status; // 默认EQUALS + private List roleIds; // 默认IN +} +``` + +### 4. 在Service中使用 + +```java +@Service +@RequiredArgsConstructor +public class UserService { + + private final UserRepository userRepository; + + // 基本查询:处理AND条件 + public Page searchUsers(UserSearchDto searchDto, Pageable pageable) { + Specification spec = DynamicSpecificationBuilder.buildSpecification(User.class, searchDto); + return userRepository.findAll(spec, pageable); + } + + // 复杂查询:同时处理AND和OR条件 + public Page searchUsersWithComplexConditions(MixedUserSearchDto searchDto, Pageable pageable) { + Specification spec = DynamicSpecificationBuilder.buildComplexSpecification(User.class, searchDto); + return userRepository.findAll(spec, pageable); + } + + // 简单查询:无注解字段使用默认匹配 + public Page simpleSearch(SimpleUserSearchDto searchDto, Pageable pageable) { + Specification spec = DynamicSpecificationBuilder.buildSpecification(User.class, searchDto); + return userRepository.findAll(spec, pageable); + } +} +``` + +## 支持的匹配类型 + +| 匹配类型 | 说明 | 示例 | +|---------|------|------| +| EQUALS | 精确匹配 | `@FieldMatch(matchType = MatchType.EQUALS)` | +| NOT_EQUALS | 不等于 | `@FieldMatch(matchType = MatchType.NOT_EQUALS)` | +| GREATER_THAN | 大于 | `@FieldMatch(matchType = MatchType.GREATER_THAN)` | +| GREATER_THAN_OR_EQUAL | 大于等于 | `@FieldMatch(matchType = MatchType.GREATER_THAN_OR_EQUAL)` | +| LESS_THAN | 小于 | `@FieldMatch(matchType = MatchType.LESS_THAN)` | +| LESS_THAN_OR_EQUAL | 小于等于 | `@FieldMatch(matchType = MatchType.LESS_THAN_OR_EQUAL)` | +| CONTAINS | 包含 | `@FieldMatch(matchType = MatchType.CONTAINS)` | +| NOT_CONTAINS | 不包含 | `@FieldMatch(matchType = MatchType.NOT_CONTAINS)` | +| STARTS_WITH | 以...开头 | `@FieldMatch(matchType = MatchType.STARTS_WITH)` | +| ENDS_WITH | 以...结尾 | `@FieldMatch(matchType = MatchType.ENDS_WITH)` | +| IGNORE_CASE_CONTAINS | 忽略大小写包含 | `@FieldMatch(matchType = MatchType.IGNORE_CASE_CONTAINS)` | +| IS_NULL | 为空 | `@FieldMatch(matchType = MatchType.IS_NULL)` | +| IS_NOT_NULL | 不为空 | `@FieldMatch(matchType = MatchType.IS_NOT_NULL)` | +| IN | 在集合中 | `@FieldMatch(matchType = MatchType.IN)` | +| NOT_IN | 不在集合中 | `@FieldMatch(matchType = MatchType.NOT_IN)` | +| IS_TRUE | 为true | `@FieldMatch(matchType = MatchType.IS_TRUE)` | +| IS_FALSE | 为false | `@FieldMatch(matchType = MatchType.IS_FALSE)` | +| BETWEEN | 日期范围 | `@FieldMatch(matchType = MatchType.BETWEEN)` | + +## 注解参数说明 + +```java +@FieldMatch( + fieldName = "userRole.id", // 指定实体字段名,默认使用DTO字段名 + matchType = MatchType.EQUALS, // 匹配类型,默认为EQUALS + ignoreCase = false, // 是否忽略大小写(字符串字段) + ignoreEmpty = true, // 是否忽略空值,默认为true + logicalOperator = LogicalOperator.AND // 逻辑操作符,默认为AND +) +``` + +## 默认匹配机制 + +当DTO字段没有`@FieldMatch`注解时,系统会根据字段类型自动选择匹配方式: + +### 字段类型与默认匹配规则 + +| 字段类型 | 默认匹配类型 | 默认忽略大小写 | 默认忽略空值 | 示例 | +|---------|------------|------------|------------|------| +| `String` | `EQUALS` | `false` | `true` | 精确匹配字符串 | +| `Integer`, `Long`, `Double`, `Float` | `EQUALS` | `false` | `true` | 精确匹配数值 | +| `Boolean`, `boolean` | `EQUALS` | `false` | `true` | 精确匹配布尔值 | +| `Date`, `LocalDateTime` | `EQUALS` | `false` | `true` | 精确匹配日期 | +| `Collection`, `Array` | `IN` | `false` | `true` | 在集合中查找 | +| 其他自定义对象 | `EQUALS` | `false` | `true` | 对象相等比较 | + +### 智能字段过滤 + +以下类型的字段会被自动忽略,不会参与查询构建: + +1. **静态字段**:`static` 修饰符的字段 +2. **常量字段**:`final` 修饰符的字段 +3. **临时字段**:`transient` 修饰符的字段 +4. **JPA忽略字段**:带有 `@Transient` 注解的字段 +5. **特殊字段**:`serialVersionUID` 字段 + +### 默认配置参数 + +对于无注解的字段,使用以下默认配置: + +```java +// 等价于以下注解配置 +@FieldMatch( + matchType = MatchType.EQUALS, // 根据字段类型动态确定 + ignoreCase = String.class.equals(fieldType), // 字符串类型默认false + ignoreEmpty = true, // 默认忽略空值 + logicalOperator = LogicalOperator.AND // 默认AND连接 +) +``` + +## 高级用法 + +### 1. 嵌套属性访问 + +```java +// 支持多级属性访问 +@FieldMatch( + fieldName = "roles.permissions.permissionCode", + matchType = MatchType.EQUALS, + ignoreEmpty = true +) +private String permissionCode; +``` + +### 2. OR条件查询 + +```java +// 使用OR逻辑连接 +@FieldMatch( + matchType = MatchType.CONTAINS, + logicalOperator = LogicalOperator.OR +) +private String keyword; +``` + +### 3. 复杂查询 + +```java +// 构建包含AND和OR的复杂查询 +Specification spec = DynamicSpecificationBuilder.buildComplexSpecification(User.class, searchDto); +``` + +### 4. 自定义Repository方法 + +```java +// 与自定义Repository方法结合使用 +public Page searchWithCustomCondition(UserSearchDto searchDto) { + Specification spec = DynamicSpecificationBuilder.buildSpecification(User.class, searchDto); + return userRepository.findByCustomCondition(spec); +} +``` + +## 最佳实践 + +1. **字段映射**:使用`fieldName`参数明确指定实体字段名,避免依赖默认映射 +2. **空值处理**:合理使用`ignoreEmpty`参数,避免不必要的空值查询 +3. **性能考虑**:对于大数据量查询,建议添加适当的索引 +4. **类型安全**:确保DTO字段类型与实体字段类型匹配 +5. **测试覆盖**:为复杂的查询条件编写单元测试 + +## 错误处理 + +- 如果实体类中不存在指定字段,工具类会记录警告并忽略该字段 +- 如果字段类型不匹配,工具类会记录错误并跳过该条件 +- 所有异常都会被捕获并记录,避免影响整体查询 + +## 示例项目 + +参考 `example` 包下的示例代码: + +- `UserSearchDto.java` - 用户搜索DTO示例 +- `RoleSearchDto.java` - 角色搜索DTO示例 +- `UserSpecificationExample.java` - Service使用示例 +- `DynamicSpecificationBuilderTest.java` - 单元测试示例 + +这个工具类大大简化了动态查询的实现,提高了开发效率和代码可维护性。 diff --git a/zkh-web/pom.xml b/zkh-web/pom.xml index fea138d..66e24e4 100644 --- a/zkh-web/pom.xml +++ b/zkh-web/pom.xml @@ -7,7 +7,7 @@ vip.jcfd zkh-framework - 1.1 + 1.2 zkh-web