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:
119
CLAUDE.md
Normal file
119
CLAUDE.md
Normal file
@ -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<T>`: 统一响应结果封装类
|
||||
- `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 <token>`
|
||||
- 支持单点登出和全部登出
|
||||
|
||||
### 动态查询
|
||||
使用`@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功能。
|
||||
7
pom.xml
7
pom.xml
@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-framework</artifactId>
|
||||
<version>1.1</version>
|
||||
<version>1.2</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>ZKH Framework</name>
|
||||
<description>A Java framework for ZKH applications</description>
|
||||
@ -66,6 +66,11 @@
|
||||
<artifactId>zkh-web</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-data</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-framework</artifactId>
|
||||
<version>1.1</version>
|
||||
<version>1.2</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>zkh-common</artifactId>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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]));
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package vip.jcfd.data.util.spec;
|
||||
|
||||
/**
|
||||
* 逻辑操作符枚举
|
||||
*/
|
||||
public enum LogicalOperator {
|
||||
/**
|
||||
* 并且 (AND)
|
||||
*/
|
||||
AND,
|
||||
|
||||
/**
|
||||
* 或者 (OR)
|
||||
*/
|
||||
OR
|
||||
}
|
||||
106
zkh-data/src/main/java/vip/jcfd/data/util/spec/MatchType.java
Normal file
106
zkh-data/src/main/java/vip/jcfd/data/util/spec/MatchType.java
Normal 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
|
||||
}
|
||||
257
zkh-data/src/main/java/vip/jcfd/data/util/spec/README.md
Normal file
257
zkh-data/src/main/java/vip/jcfd/data/util/spec/README.md
Normal 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` - 单元测试示例
|
||||
|
||||
这个工具类大大简化了动态查询的实现,提高了开发效率和代码可维护性。
|
||||
@ -7,7 +7,7 @@
|
||||
<parent>
|
||||
<groupId>vip.jcfd</groupId>
|
||||
<artifactId>zkh-framework</artifactId>
|
||||
<version>1.1</version>
|
||||
<version>1.2</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>zkh-web</artifactId>
|
||||
|
||||
Reference in New Issue
Block a user