3分钟将LazyLoad应用于业务开发

1. 概览

在各大框架中,延迟加载是一种重要的性能优化手段,所依赖的数据按需逐步完成加载(比如 Hibernate 的延迟加载)。一来,避免了全部加载带来的性能损失;二来,降低业务人员频繁进行 null 判断 和 手工加载的工作量;

1.1. 背景

在微内核架构中,有一个重要的组件 “Context”,作为一个容器,它贯穿于整个处理流程,为各个插件提供数据和服务的共享。

微内核架构示意如下:

3分钟将LazyLoad应用于业务开发
微内核架构

有一种典型场景,比如,Plugin-A 和 Plugin-a 在不同流程分支中都使用到了 User 这个对象,通常情况下两个插件都会这样写:

User user = null;
if(context.getUser() == nll){
    // 自己进行加载
    user = userRepository.getByUserId(context.getUserId());
    context.setUser(user);
}else{
    // 直接使用其他组件已经加载的数据,避免重复加载
    user = context.getUser();
}

其中,重复逻辑如下:

  1. 首先,判断 context 中是否存在 user 对象;

  2. 如果不存在,则从 仓库 里获取,并写回到 context,以便后续流程直接使用;

  3. 如果存在,则直接使用;

由于流程的动态性,难以保障在自己使用时, user 已经被其他组件完成了加载,为了避免 npe 自己不得不重新做一遍。

那结果就是:

  1. 到处都是重复代码,不便于维护;

  2. 散弹式修改,加载逻辑变化,修改点过于分散;

  3. 与业务逻辑耦合在一起,重点不够突出;

  4. 研发被细节羁绊,无法将精力放到核心逻辑上;

1.2. 目标

问题很明显,解决方案也很清晰,期望能实现:

  1. 支持延迟加载,在调用时进行自动加载;

  2. 支持数据缓存,避免数据的多次加载;

  3. 支持级联加载,所需数据尚未加载,则自动触发依赖数据的加载;

2. 快速入门

2.1. 添加 starter

Spring boot 项目的 pom 中增加如下依赖:

<dependency>
    <groupId>com.geekhalo.lego</groupId>
    <artifactId>lego-starter-loader</artifactId>
    <version>0.1.1</version>
</dependency>

2.2. 使用 LazyLoadProxyFactory 创建 Proxy

lego-starter-loader 将自动完成 LazyLoadProxyFactory Bean 的注册,在项目中直接引用即可。

示例代码如下:

@Autowired
private LazyLoadProxyFactory lazyLoadProxyFactory;

// 创建代理对象
CreateOrderContext proxyContext = this.lazyLoadProxyFactory.createProxyFor(context);

2.3. 使用 @LazyLoadBy 注解

在 Context 上增加 @LazyLoadBy 注解,并指定加载器。

示例代码如下:

@Data
public class CreateOrderContextV2 implements CreateOrderContext{

    private CreateOrderCmd cmd;

    @LazyLoadBy("#{@userRepository.getById(cmd.userId)}")
    private User user;

    @LazyLoadBy("#{@productRepository.getById(cmd.productId)}")
    private Product product;

    @LazyLoadBy("#{@addressRepository.getDefaultAddressByUserId(user.id)}")
    private Address defAddress;

    @LazyLoadBy("#{@stockRepository.getByProductId(product.id)}")
    private Stock stock;

    @LazyLoadBy("#{@priceService.getByUserAndProduct(user.id, product.id)}")
    private Price price;
}

@LazyLoadBy 使用 SpEL 对加载器进行描述,简单解释如下:
@LazyLoadBy(“#{@userRepository.getById(cmd.userId)}”)

  1. #{….} 为 SpEL 占位符;

  2. @userRepository.getById 指的是调用 userRepository Bean 的 getById 方法;

  3. cmd.userId 指的是,cmd 字段的 userId 属性。

整体含义为:以 cmd 中的 userId 作为参数,调用 userRepository Bean 的 getById 方法。

单元测试如下:

CreateOrderContext proxyContext = this.lazyLoadProxyFactory.createProxyFor(context);
Assertions.assertNotNull(proxyContext);
log.info("Get Command");
Assertions.assertNotNull(proxyContext.getCmd());

log.info("Get Price");
Assertions.assertNotNull(proxyContext.getPrice());

log.info("Get Stock");
Assertions.assertNotNull(proxyContext.getStock());

log.info("Get Default Address");
Assertions.assertNotNull(proxyContext.getDefAddress());

log.info("Get Product");
Assertions.assertNotNull(proxyContext.getProduct());

log.info("Get User");
Assertions.assertNotNull(proxyContext.getUser());

运行测试,获取如下结果:

c.g.l.loader.LazyLoadProxyFactoryTest    : Get Command
c.g.l.loader.LazyLoadProxyFactoryTest    : Get Price
c.g.lego.service.user.UserRepository     : Get User By Id 1
c.g.l.service.product.ProductRepository  : Get Product By Id 2
c.g.lego.service.price.PriceService      : Get Price for User 1 and Product 2
c.g.l.loader.LazyLoadProxyFactoryTest    : Get Stock
c.g.lego.service.stock.StockRepository   : Get Stock By Product Id 2
c.g.l.loader.LazyLoadProxyFactoryTest    : Get Default Address
c.g.l.service.address.AddressRepository  : Load Default Address For User 1
c.g.l.loader.LazyLoadProxyFactoryTest    : Get Product
c.g.l.loader.LazyLoadProxyFactoryTest    : Get User

对照测试代码可得,框架具有 “级联加载” 和 “数据缓存” 能力,具体如下:

  1. 级联加载

    • 调用 getPice 方法

      • c.g.l.loader.LazyLoadProxyFactoryTest    : Get Price

    • 由于 price 依赖 user 和 product(”#{@priceService.getByUserAndProduct(user.id, product.id)}”))

    • 会先获取 user 和 product

      • c.g.lego.service.user.UserRepository     : Get User By Id 1

      • c.g.l.service.product.ProductRepository  : Get Product By Id 2

    • 最后获取 Price

      • c.g.lego.service.price.PriceService      : Get Price for User 1 and Product 2

  2. 数据缓存

    • 在调用 getPrice 时已经完成了 user 和 product 的加载

    • 后续的 getUser 和 getProduct 没有触发加载操作

      • c.g.l.loader.LazyLoadProxyFactoryTest    : Get Product

      • c.g.l.loader.LazyLoadProxyFactoryTest    : Get User

2.4. 使用自定义注解

@LazyLoadBy 注解过于底层,使用起来比较麻烦,存在明显的缺陷:

  1. 需要熟知加载逻辑,比如调用哪个 bean 的哪个方法;

  2. 形成散弹式修改,如果 bean 或 方法 发生变化,需要修改多处;

  3. 缺乏强约束,比如需要哪几个参数,他们都是什么;

对此,可以使用 自定义注解 来规避上述问题。

示例如下:

首先,创建自定义注解

// 只能标记在 字段上
@Target({ElementType.FIELD})
// 运行时生效
@Retention(RetentionPolicy.RUNTIME)
// LazyLoadBy 注解
@LazyLoadBy("#{@userRepository.getById(${userId})}")
public @interface LazyLoadUserById {

    String userId();
}

核心点有:

  1. 将 @LazyLoadBy 注解放在自定义注解上,用于声明所使用的加载器;

  2. LazyLoadBy 入参使用 ${methodName} 替代,其中 methodName 为自定义注解的方法名;

类似的,多参数示例如下:

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@LazyLoadBy("#{@priceService.getByUserAndProduct(${userId}${productId})}")
public @interface LazyLoadPriceByUserAndProduct {

    String userId();

    String productId();
}

在 Context 中使用自定义注解,具体如下:

@Data
public class CreateOrderContextV1 implements CreateOrderContext{

    private CreateOrderCmd cmd;

    @LazyLoadUserById(userId = "cmd.userId")
    private User user;

    @LazyLoadProduceById(productId = "cmd.productId")
    private Product product;

    @LazyLoadDefaultAddressByUserId(userId = "user.id")
    private Address defAddress;

    @LazyLoadStockByProductId(productId = "product.id")
    private Stock stock;

    @LazyLoadPriceByUserAndProduct(userId = "user.id",
            productId = "product.id")

    private Price price;
}

新的 CreateOrderContext 只关注入参即可,无需关注具体的加载细节。

运行单元测试,获取同样的结果如下:

c.g.l.loader.LazyLoadProxyFactoryTest    : Get Command
c.g.l.loader.LazyLoadProxyFactoryTest    : Get Price
c.g.lego.service.user.UserRepository     : Get User By Id 1
c.g.l.service.product.ProductRepository  : Get Product By Id 2
c.g.lego.service.price.PriceService      : Get Price for User 1 and Product 2
c.g.l.loader.LazyLoadProxyFactoryTest    : Get Stock
c.g.lego.service.stock.StockRepository   : Get Stock By Product Id 2
c.g.l.loader.LazyLoadProxyFactoryTest    : Get Default Address
c.g.l.service.address.AddressRepository  : Load Default Address For User 1
c.g.l.loader.LazyLoadProxyFactoryTest    : Get Product
c.g.l.loader.LazyLoadProxyFactoryTest    : Get User

3. 设计&扩展

3.1. 整体设计

核心架构如下:

3分钟将LazyLoad应用于业务开发
Load整体架构

整体运行流程如下:

  1. LazyLoadProxyFactory 将一个普通的 JavaBean 封装为具有 加载能力的 Proxy 对象;

  2. LazyLoaderInterceptor 对 getter 方法进行拦截,完成属性加载任务;

3.2. 初始化流程

核心对象初始化流程如下:

3分钟将LazyLoad应用于业务开发
初始化流程

具体流程如下:

  1. PropertyLazyLoaderFactory 依次遍历 Class 的所有 Field,读取 @LazyLoadBy 注解,并将其封装成 PropertyLoazyLoader;

  2. LazyLoaderInterceptorFactory 将 PropertyLoazyLoader 和 target 对象 封装为 LazyLoaderInterceptor;

  3. 最后由 LazyLoadProxyFactory 将 LazyLoaderInterceptor 应用于 Proxy 对象,使其具备延迟加载能力;

3.3. 加载流程

加载流程比较简单,具体如下:

3分钟将LazyLoad应用于业务开发
初始化流程

4. 项目信息

项目仓库地址:https://gitee.com/litao851025/lego

项目文档地址:https://gitee.com/litao851025/lego/wikis/support/Loader


原文始发于微信公众号(geekhalo):3分钟将LazyLoad应用于业务开发

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/60486.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!