1. 概览
在日常开发中,数据查询是最为常见的需求,也是占比最大的一部分。为了降低成本提升开发效率,已经封装了两个组件:
- 将 QueryObject 与 Spring Data Jpa 进行集成,无需编写实现代码,只需通过注解定义查询对象,并能完成单表的普通查询、列表查询、分页查询等;
- 内存 Join 组件,通过注解对关联对象进行标记,框架自动完成数据的抓取,也无需编写实现代码;
两个组件,基本都能做到只“声明能力”,不“编写代码”,提升开发效率的同时,降低了bug概率。但,在两者结合使用时,就需要编写实现代码,将能力粘合起来。
1.1. 背景
在日常开发中,一个查询请求主要由以下几部分组成:
- 验证入参的有效性;
- 查询数据库获得主实体数据;
- 查询关联数据并完成结果的组装;
在lego框架中,三个步骤都提供了相应的组件进行支持,以一个订单分页查询为例:
- 主流程代码如下:
public Page<OrderDetail> pageByUserId(@Valid @NotNull(message = "查询参数不能为 null") PageByUserId query) {
Page<OrderDetail> orderDetailPage = this.orderQueryRepository.pageOf(query, OrderDetail::new);
if (orderDetailPage.hasContent()){
this.joinService.joinInMemory(orderDetailPage.getContent());
}
return orderDetailPage;
}
- 查询参数定义如下:
@Data
public class PageByUserId {
@NotNull(message = "user id 不能为 null")
@FieldEqualTo("userId")
private Long userId;
private Pageable pageable;
}
- 返回结果如下:
@Data
public class OrderDetail {
private Order order;
@JoinItemByOrder(keyFromSourceData = "#{order.id}")
private List<OrderItem> orderItems;
public OrderDetail(Order order){
this.order = order;
}
}
查询三大步骤均基于“声明式注解”通过描述的方式进行实现,然后通过 编码 的方式完成主流程。
仔细观察主流程,会发现这是一套标准的“模板代码”,重复枯燥、没有业务价值,像这样有规律的“重复”代码,就应交由框架实现。
1.2. 目标
构建声明式 QueryService,只需定义方法,无需编写实现代码,便能完成大多数场景的数据查询。
组件应具有如下特性:
- 只定义接口,由框架负责具体实现;
- 保留参数校验、单表查询和内存join等全套能力;
- 对于个性化需求,提供扩展点,可通过 coding 方式实现;
- 启动时进行有效性校验,避免运行时异常;
2. 快速入门
设计目标与 Spring Data 的设计理念高度相似,QueryService 组件在实现上进行了借鉴,在使用上也与 Spring Data 保存一致,以降低使用门槛。
2.1. 环境搭建
首先,在项目中引入 lego-starter,具体如下:
<dependency>
<groupId>com.geekhalo.lego</groupId>
<artifactId>lego-starter</artifactId>
<version>0.1.9-query_proxy-SNAPSHOT</version>
</dependency>
然后,依次引入 validation 和 spring data jpa 支持
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
在 application 文件中添加 Datasource 配置:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/lego
username: root
password: root
jpa:
hibernate:
ddl-auto: update
show-sql: true
在启动类上通过注解开启 JpaRepository 和 QueryService 支持
@SpringBootApplication
@EnableJpaRepositories(basePackages = {
"com.geekhalo.lego.query"
}, repositoryFactoryBeanClass = JpaBasedQueryObjectRepositoryFactoryBean.class)
@EnableQueryService(basePackages = "com.geekhalo.lego.query")
public class DemoApplication {
public static void main(String[] args){
SpringApplication.run(DemoApplication.class, args);
}
}
其中:
- @EnableJpaRepositories 开启 JpaRepository 的支持,并通过设置 JpaBasedQueryObjectRepositoryFactoryBean 完成与 QueryObject 模型的集成;basePackages 指定自动扫描的包路径;
- @EnableQueryService 开启 QueryService 的支持,并通过basePackages指定自动扫描的包路径;
2.2. 定义 OrderQueryService
@QueryServiceDefinition(domainClass = Order.class,
repositoryClass = OrderQueryRepository.class)
@Validated
public interface OrderQueryServiceProxy extends OrderQueryService {
OrderDetail getById(@Valid @NotNull(message = "订单号不能为null") Long id);
Page<OrderDetail> pageByUserId(@Valid @NotNull(message = "查询参数不能为 null") PageByUserId query);
List<OrderDetail> getByUserId(@Valid @NotNull(message = "查询参数不能为 null") GetByUserId getByUserId);
Long countByUser(@Valid @NotNull(message = "查询参数不能为 null") CountByUserId countByUserId);
List<OrderDetail> getPaidByUserId(Long id);
}
定义 OrderQueryService 接口,添加相关注解:
- @QueryServiceDefinition 标记该接口为查询接口,将自动为其生成代理,其中
- domainClass 为查询实体的类型
- repositoryClass 为查询服务所使用的底层仓库
- @Validated 注解启动验证框架,对验证注解进行处理;
- OrderQueryRepository 也只有定义没有实现,具体定义如下:
public interface OrderQueryRepository
extends JpaRepository<Order, Long>,
QueryRepository<Order, Long> {
Order getById(Long id);
List<Order> getByUserIdAndStatus(Long id, OrderStatus paid);
}
OrderQueryRepository 继承:
- JpaRepository, 拥有 JpaRepository 中基本的查询功能
- QueryRepository,拥有 QueryObject 查询功能
2.3. 常见功能
2.3.1. 入参校验
框架对 validation 验证体系进行集成,只需在接口和方法上增加注解,该接口便拥有参数验证能力。
接口定义如下:
List<OrderDetail> getByUserId(@Valid @NotNull(message = "查询参数不能为 null") GetByUserId getByUserId);
执行如下代码:
this.getQueryService().getByUserId(null);
抛出验证异常
javax.validation.ConstraintViolationException: getByUserId.getByUserId: 查询参数不能为 null
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
at com.sun.proxy.$Proxy142.getByUserId(Unknown Source)
2.3.2. 模型转换和数据填充
通常情况下,repository 查询方法只会返回实体对象,框架会自动将其转化为结果对象,并使用 JoinService 完成数据填充。
框架自动查找模型转换方案,具体如下:
- 查找结果对象的静态方法,入参为实体对象,返回值为结果对象;
- 查找结果对象的构造函数,入参为实体对象;
- 查找 Spring 中的 QueryResultConverter 实现;
QueryResultConverter 的定义如下:
/**
* 结构转化器,对查询结果进行封装
* @param <I>
* @param <O>
*/
public interface QueryResultConverter<I, O> {
/**
* 是否能支持对应类型的转换
* @param input
* 输入类型
* @param output
* 输出类型
* @return
*/
boolean support(Class<I> input, Class<O> output);
/**
* 进行模型转换
* @param input
* @return
*/
O convert(I input);
}
其中,OrderDetail 通过构造函数进行模型转换,代码如下:
@Data
public class OrderDetail {
private Order order;
@JoinItemByOrder(keyFromSourceData = "#{order.id}")
private List<OrderItem> orderItems;
public OrderDetail(Order order){
this.order = order;
}
}
2.3.3. 使用 Repositry 方法
QueryService 可以直接使用 QueryRepository 中的方法进行实体查询。
OrderQueryRepository 存在一个 getById 方法,具体如下:
Order getById(Long id);
OrderQueryServiceProxy 也存在一个 getById 方法,具体如下:
OrderDetail getById(@Valid @NotNull(message = "订单号不能为null") Long id);
两者,除返回值不同,方法名和入参均相同,框架会根据 方法名+入参 选择合适的查询方法。
2.3.4. 使用通用方法
除了直接使用 QueryRepository 方法外,QueryService 也会使用 QueryObject 完成查询。
OrderQueryServiceProxy 存在一个分页查询 pageByUserId,具体定义如下:
Page<OrderDetail> pageByUserId(@Valid @NotNull(message = "查询参数不能为 null") PageByUserId query);
而在 OrderQueryRepository 中并未定义 pageByUserId 方法,此时 QueryService 会直接使用 QueryObjectRepository 中的 pageOf 完成数据查询。
QueryObjectRepository 的 pageOf 定义如下:
<Q> Page<E> pageOf(Q query);
QueryService 将忽略方法名,基于入参和返回结果的兼容性对方法进行筛选。
2.3.5. 自定义查询
当业务非常复杂,QueryService 默认实现无法满足时,可以通过自定义方式对实现进行扩展。
首先,需要定义一个 自定义接口,如:
public interface CustomOrderQueryService {
List<OrderDetail> getPaidByUserId(Long id);
}
其次,根据业务逻辑实现自定义接口,如:
@Service
public class CustomOrderQueryServiceImpl implements CustomOrderQueryService{
@Autowired
private JoinService joinService;
@Autowired
private OrderQueryRepository orderQueryRepository;
@Override
public List<OrderDetail> getPaidByUserId(Long id) {
List<Order> orders = orderQueryRepository.getByUserIdAndStatus(id, OrderStatus.PAID);
List<OrderDetail> orderDetails = orders.stream()
.map(OrderDetail::new)
.collect(Collectors.toList());
this.joinService.joinInMemory(orderDetails);
return orderDetails;
}
}
最后,让 QueryService 继承自定义接口即可:
public interface OrderQueryServiceProxy extends CustomOrderQueryService{
}
在调用 getPaidByUserId 方法时,会将请求转发给
CustomOrderQueryServiceImpl 的 getPaidByUserId 实现。
对于自定义接口的实现类,默认使用 Impl 作为后置,如有必要,可通过 @EnableQueryService 的
queryImplementationPostfix 进行调整。
3. 核心设计
3.1. Proxy 结构
为 QueryService 自动实现的 Proxy 结构如下:
image
Proxy 实现 自定义的QueryService 接口,并将方法调用分发给不同的实现,核心拦截器包括:
- DefaultMethodInvokingMethodInterceptor。拦截对默认方法的调用,将请求转发给代理对象;
- 基于自定义实现的 QueryServiceMethodDispatcherInterceptor,将请求转发给自定义实现类;
- 基于自动创建 QueryServiceMethod 的 QueryServiceMethodDispatcherInterceptor,根据方法签名自动实现查询逻辑,并将请求转发给 QueryServiceMethod;
3.2. 初始化流程
以下是整个框架的初始化流程:
image
通过 @EnableQueryService 注解开启 QueryService 支持后,将向 Spring 容器完成
QueryServiceBeanDefinitionRegistrar 的注册。
- QueryServiceBeanDefinitionScanner 根据 basePackages 设置,自动对带有@QueryServiceDefinition的接口进行扫描;
- 扫描到带有@QueryServiceDefinition注解的接口后,将其封装为 QueryServiceProxyFactoryBean,并将其注册到 Spring 容器;
- Spring 实例化 QueryServiceProxyFactoryBean 生成对应的 QueryService 代理对象;