1. 概览
在各大框架中,延迟加载是一种重要的性能优化手段,所依赖的数据按需逐步完成加载(比如 Hibernate 的延迟加载)。一来,避免了全部加载带来的性能损失;二来,降低业务人员频繁进行 null 判断 和 手工加载的工作量;
1.1. 背景
在微内核架构中,有一个重要的组件 “Context”,作为一个容器,它贯穿于整个处理流程,为各个插件提供数据和服务的共享。
微内核架构示意如下:
有一种典型场景,比如,Plugin-A 和 Plugin-a 在不同流程分支中都使用到了 User 这个对象,通常情况下两个插件都会这样写:
User user = null;
if(context.getUser() == nll){
// 自己进行加载
user = userRepository.getByUserId(context.getUserId());
context.setUser(user);
}else{
// 直接使用其他组件已经加载的数据,避免重复加载
user = context.getUser();
}
其中,重复逻辑如下:
-
首先,判断 context 中是否存在 user 对象;
-
如果不存在,则从 仓库 里获取,并写回到 context,以便后续流程直接使用;
-
如果存在,则直接使用;
由于流程的动态性,难以保障在自己使用时, user 已经被其他组件完成了加载,为了避免 npe 自己不得不重新做一遍。
那结果就是:
-
到处都是重复代码,不便于维护;
-
散弹式修改,加载逻辑变化,修改点过于分散;
-
与业务逻辑耦合在一起,重点不够突出;
-
研发被细节羁绊,无法将精力放到核心逻辑上;
1.2. 目标
问题很明显,解决方案也很清晰,期望能实现:
-
支持延迟加载,在调用时进行自动加载;
-
支持数据缓存,避免数据的多次加载;
-
支持级联加载,所需数据尚未加载,则自动触发依赖数据的加载;
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)}”)
-
#{….} 为 SpEL 占位符;
-
@userRepository.getById 指的是调用 userRepository Bean 的 getById 方法;
-
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
对照测试代码可得,框架具有 “级联加载” 和 “数据缓存” 能力,具体如下:
-
级联加载
-
c.g.lego.service.price.PriceService : Get Price for User 1 and Product 2
-
c.g.lego.service.user.UserRepository : Get User By Id 1
-
c.g.l.service.product.ProductRepository : Get Product By Id 2
-
user.id
-
product.id
-
c.g.l.loader.LazyLoadProxyFactoryTest : Get Price
-
调用 getPice 方法
-
由于 price 依赖 user 和 product(”#{@priceService.getByUserAndProduct(user.id, product.id)}”))
-
会先获取 user 和 product
-
最后获取 Price
数据缓存
-
c.g.l.loader.LazyLoadProxyFactoryTest : Get Product
-
c.g.l.loader.LazyLoadProxyFactoryTest : Get User
-
在调用 getPrice 时已经完成了 user 和 product 的加载
-
后续的 getUser 和 getProduct 没有触发加载操作
2.4. 使用自定义注解
@LazyLoadBy 注解过于底层,使用起来比较麻烦,存在明显的缺陷:
-
需要熟知加载逻辑,比如调用哪个 bean 的哪个方法;
-
形成散弹式修改,如果 bean 或 方法 发生变化,需要修改多处;
-
缺乏强约束,比如需要哪几个参数,他们都是什么;
对此,可以使用 自定义注解 来规避上述问题。
示例如下:
首先,创建自定义注解
// 只能标记在 字段上
@Target({ElementType.FIELD})
// 运行时生效
@Retention(RetentionPolicy.RUNTIME)
// LazyLoadBy 注解
@LazyLoadBy("#{@userRepository.getById(${userId})}")
public @interface LazyLoadUserById {
String userId();
}
核心点有:
-
将 @LazyLoadBy 注解放在自定义注解上,用于声明所使用的加载器;
-
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. 整体设计
核心架构如下:
整体运行流程如下:
-
LazyLoadProxyFactory 将一个普通的 JavaBean 封装为具有 加载能力的 Proxy 对象;
-
LazyLoaderInterceptor 对 getter 方法进行拦截,完成属性加载任务;
3.2. 初始化流程
核心对象初始化流程如下:
具体流程如下:
-
PropertyLazyLoaderFactory 依次遍历 Class 的所有 Field,读取 @LazyLoadBy 注解,并将其封装成 PropertyLoazyLoader;
-
LazyLoaderInterceptorFactory 将 PropertyLoazyLoader 和 target 对象 封装为 LazyLoaderInterceptor;
-
最后由 LazyLoadProxyFactory 将 LazyLoaderInterceptor 应用于 Proxy 对象,使其具备延迟加载能力;
3.3. 加载流程
加载流程比较简单,具体如下:
4. 项目信息
项目仓库地址:https://gitee.com/litao851025/lego
项目文档地址:https://gitee.com/litao851025/lego/wikis/support/Loader