feat(data): 添加动态Specification构建器及配套工具类

- 新增DynamicSpecificationBuilder工具类,支持根据DTO动态生成JPA Specification查询条件
- 添加FieldMatch注解,用于配置字段匹配方式和查询条件
- 添加MatchType枚举,定义多种字段匹配类型如EQUALS、CONTAINS、BETWEEN等
- 添加LogicalOperator枚举,支持AND和OR逻辑操作符
- 实现缓存机制提升反射操作性能,包括实体字段缓存、DTO字段信息缓存等
- 支持嵌套属性访问,如user.address.city格式的字段路径
- 提供默认匹配机制,根据字段类型自动选择合适的匹配方式
- 添加PageableFactory接口,简化分页参数处理
- 更新zkh-data模块依赖,引入Jakarta Persistence API和Spring Data JPA
- 升级项目版本至1.2,统一管理各模块版本号
- 添加详细的使用文档README.md,包含使用示例和最佳实践
This commit is contained in:
zkh
2025-11-21 18:27:58 +08:00
parent eb66fe7810
commit 5029ae6664
11 changed files with 1278 additions and 4 deletions

View File

@ -6,7 +6,7 @@
<parent>
<groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId>
<version>1.1</version>
<version>1.2</version>
</parent>
<artifactId>zkh-data</artifactId>
@ -19,8 +19,41 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.42</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>

View File

@ -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);
}
}

View File

@ -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<String, Boolean> ENTITY_FIELD_CACHE = new ConcurrentHashMap<>();
// DTO字段信息缓存 - 缓存DTO的字段信息和注解配置
private static final Map<Class<?>, List<FieldInfo>> DTO_FIELD_CACHE = new ConcurrentHashMap<>();
// 嵌套字段路径缓存 - 缓存嵌套字段的类型信息
private static final Map<String, Class<?>> 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 <T> 实体类型
* @param entityClass 实体类Class对象
* @param dto 查询条件DTO
* @return 构建好的Specification对象
*/
public static <T> Specification<T> buildSpecification(Class<T> entityClass, Object dto) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
try {
// 从缓存获取DTO字段信息
List<FieldInfo> 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<FieldInfo> getDtoFieldInfos(Class<?> dtoClass) {
return DTO_FIELD_CACHE.computeIfAbsent(dtoClass, clazz -> {
List<FieldInfo> 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 <T> void processFieldOptimized(Root<T> root, CriteriaBuilder cb,
List<Predicate> predicates, FieldInfo fieldInfo,
Object fieldValue, Class<T> 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<Object> 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<Object> 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<Object> 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<Object> getNestedPath(Root<?> root, String fieldName) {
if (!fieldName.contains(".")) {
return root.get(fieldName);
}
String[] parts = fieldName.split("\\.");
Path<Object> 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 <T> Specification<T> buildOrSpecification(Class<T> entityClass, Object dto) {
return (root, query, criteriaBuilder) -> {
List<Predicate> predicates = new ArrayList<>();
try {
// 从缓存获取DTO字段信息
List<FieldInfo> 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 <T> Specification<T> buildComplexSpecification(Class<T> entityClass, Object dto) {
return (root, query, criteriaBuilder) -> {
List<Predicate> andPredicates = new ArrayList<>();
List<Predicate> orPredicates = new ArrayList<>();
try {
// 从缓存获取DTO字段信息
List<FieldInfo> 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<String, Object> getCacheStats() {
Map<String, Object> 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 <T> Specification<T> buildSpecificationForBatch(Class<T> entityClass, List<Object> dtoList) {
return (root, query, criteriaBuilder) -> {
List<Predicate> allPredicates = new ArrayList<>();
for (Object dto : dtoList) {
try {
List<FieldInfo> fieldInfos = getDtoFieldInfos(dto.getClass());
List<Predicate> 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]));
};
}
}

View File

@ -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;
}

View File

@ -0,0 +1,16 @@
package vip.jcfd.data.util.spec;
/**
* 逻辑操作符枚举
*/
public enum LogicalOperator {
/**
* 并且 (AND)
*/
AND,
/**
* 或者 (OR)
*/
OR
}

View File

@ -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
}

View File

@ -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<Long> roleIds;
}
```
### 2. 使用默认匹配机制(无注解)
```java
@Data
public class SimpleUserSearchDto {
// 字符串类型默认使用EQUALS匹配
private String username;
// 数值类型默认使用EQUALS匹配
private Integer age;
// 布尔类型默认使用EQUALS匹配
private Boolean active;
// 集合类型默认使用IN匹配
private List<Long> 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<Long> roleIds; // 默认IN
}
```
### 4. 在Service中使用
```java
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
// 基本查询处理AND条件
public Page<User> searchUsers(UserSearchDto searchDto, Pageable pageable) {
Specification<User> spec = DynamicSpecificationBuilder.buildSpecification(User.class, searchDto);
return userRepository.findAll(spec, pageable);
}
// 复杂查询同时处理AND和OR条件
public Page<User> searchUsersWithComplexConditions(MixedUserSearchDto searchDto, Pageable pageable) {
Specification<User> spec = DynamicSpecificationBuilder.buildComplexSpecification(User.class, searchDto);
return userRepository.findAll(spec, pageable);
}
// 简单查询:无注解字段使用默认匹配
public Page<User> simpleSearch(SimpleUserSearchDto searchDto, Pageable pageable) {
Specification<User> 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<User> spec = DynamicSpecificationBuilder.buildComplexSpecification(User.class, searchDto);
```
### 4. 自定义Repository方法
```java
// 与自定义Repository方法结合使用
public Page<User> searchWithCustomCondition(UserSearchDto searchDto) {
Specification<User> 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` - 单元测试示例
这个工具类大大简化了动态查询的实现,提高了开发效率和代码可维护性。