声明式查询服务,只需定义,无需实现

2023-04-08 13:07:22

1. 概览

在日常开发中,数据查询是最为常见的需求,也是占比最大的一部分。为了降低成本提升开发效率,已经封装了两个组件:

  1. 将 QueryObject 与 Spring Data Jpa 进行集成,无需编写实现代码,只需通过注解定义查询对象,并能完成单表的普通查询、列表查询、分页查询等;
  2. 内存 Join 组件,通过注解对关联对象进行标记,框架自动完成数据的抓取,也无需编写实现代码;

两个组件,基本都能做到只“声明能力”,不“编写代码”,提升开发效率的同时,降低了bug概率。但,在两者结合使用时,就需要编写实现代码,将能力粘合起来。

1.1. 背景

在日常开发中,一个查询请求主要由以下几部分组成:

  1. 验证入参的有效性;
  2. 查询数据库获得主实体数据;
  3. 查询关联数据并完成结果的组装;

在lego框架中,三个步骤都提供了相应的组件进行支持,以一个订单分页查询为例:

  1. 主流程代码如下:
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;
}
  1. 查询参数定义如下:
@Data
public class PageByUserId {
    @NotNull(message = "user id 不能为 null")
    @FieldEqualTo("userId")
    private Long userId;
    private Pageable pageable;
}
  1. 返回结果如下:
@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,只需定义方法,无需编写实现代码,便能完成大多数场景的数据查询。

组件应具有如下特性:

  1. 只定义接口,由框架负责具体实现;
  2. 保留参数校验、单表查询和内存join等全套能力;
  3. 对于个性化需求,提供扩展点,可通过 coding 方式实现;
  4. 启动时进行有效性校验,避免运行时异常;

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

其中:

  1. @EnableJpaRepositories 开启 JpaRepository 的支持,并通过设置 JpaBasedQueryObjectRepositoryFactoryBean 完成与 QueryObject 模型的集成;basePackages 指定自动扫描的包路径;
  2. @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 接口,添加相关注解:

  1. @QueryServiceDefinition 标记该接口为查询接口,将自动为其生成代理,其中
  2. domainClass 为查询实体的类型
  3. repositoryClass 为查询服务所使用的底层仓库
  4. @Validated 注解启动验证框架,对验证注解进行处理;
  5. OrderQueryRepository 也只有定义没有实现,具体定义如下:
public interface OrderQueryRepository
        extends JpaRepository<Order, Long>,
        QueryRepository<Order, Long> {
    Order getById(Long id);
    List<Order> getByUserIdAndStatus(Long id, OrderStatus paid);
}

OrderQueryRepository 继承:

  1. JpaRepository, 拥有 JpaRepository 中基本的查询功能
  2. 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 完成数据填充。

框架自动查找模型转换方案,具体如下:

  1. 查找结果对象的静态方法,入参为实体对象,返回值为结果对象;
  2. 查找结果对象的构造函数,入参为实体对象;
  3. 查找 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 接口,并将方法调用分发给不同的实现,核心拦截器包括:

  1. DefaultMethodInvokingMethodInterceptor。拦截对默认方法的调用,将请求转发给代理对象;
  2. 基于自定义实现的 QueryServiceMethodDispatcherInterceptor,将请求转发给自定义实现类;
  3. 基于自动创建 QueryServiceMethod 的 QueryServiceMethodDispatcherInterceptor,根据方法签名自动实现查询逻辑,并将请求转发给 QueryServiceMethod;

3.2. 初始化流程

以下是整个框架的初始化流程:

 

image

通过 @EnableQueryService 注解开启 QueryService 支持后,将向 Spring 容器完成
QueryServiceBeanDefinitionRegistrar 的注册。

  1. QueryServiceBeanDefinitionScanner 根据 basePackages 设置,自动对带有@QueryServiceDefinition的接口进行扫描;
  2. 扫描到带有@QueryServiceDefinition注解的接口后,将其封装为 QueryServiceProxyFactoryBean,并将其注册到 Spring 容器;
  3. Spring 实例化 QueryServiceProxyFactoryBean 生成对应的 QueryService 代理对象;
  • 作者:财高八斗者
  • 原文链接:https://blog.csdn.net/m0_74931226/article/details/127934442
    更新时间:2023-04-08 13:07:22