【基础系列】FactoryBean及代理实现SPI机制的实例(应用篇)

文章目录
  1. I. 相关知识点
    1. 1. demo背景说明
    2. 2. SPI简述
      1. a. 基本定义
      2. b. demo辅助说明
    3. 3. 代理模式简述
  2. II. 方案设计与实现
    1. 1. 目标拆分
    2. 2. 方案设计
      1. a. 接口定义
      2. b. 使用约束
      3. c. 代理类生成
    3. 3. 实例演示
  3. III. 其他
    1. 0. 项目相关
      1. a. 更多博文
      2. b. 项目源码
    2. 1. 一灰灰Blog
    3. 2. 声明
    4. 3. 扫描关注

FactoryBean在Spring中算是一个比较有意思的存在了,虽然在日常的业务开发中,基本上不怎么会用到,但在某些场景下,如果用得好,却可以实现很多有意思的东西

本篇博文主要介绍如何通过FactoryBean来实现一个类SPI机制的微型应用框架

文章内涉及到的知识点

  • SPI机制
  • FactoryBean
  • JDK动态代理

I. 相关知识点

在看下面的内容之前,得知道一下什么是SPI,以及SPI的用处和JDK实现SPI的方式,对于这一块有兴趣了解的童鞋,可以看一下个人之前写的相关文章

1. demo背景说明

在开始之前,有必要了解一下,我们准备做的这个东西,到底适用于什么样的场景。

在电商中,有一个比较恰当的例子,商品详情页的展示。拿淘宝系的详情页作为背景来说明(没有在阿里工作过,下面的东西纯粹是为了说明应用场景而展开)

商品详情页

假设有这么三个详情页,我们设定一个大前提,底层的数据层提供方都是一套的,商品详情展示的服务完全可以做到复用,即三个性情页中,绝大多数的东西都一样,只是不同的详情页车重点不同而已。

如上图中,我们假定有细微区别的几个地方

位置 淘宝详情 天猫详情 咸鱼详情 说明
banner 显示淘宝的背景墙 显示天猫的广告位 咸鱼的坑位 三者数据结构完全一致,仅图片url不同
推荐 推荐同类商品 推荐店家其他商品 推荐同类二手产品 数据结构相同,内容不同
评价 商品评价 商品评价 没有评价,改为留言
促销 优惠券 天猫积分券 没有券 -

根据上面的简单对比,其实只想表达一个意思,业务基本上一致,仅仅只有很少的一些东西不同,需要定制化,这个时候可以考虑用SPI来支持定制化的服务

2. SPI简述

a. 基本定义

SPI的全名为Service Provider Interface,简单的总结下java spi机制的思想。我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制

上面是相对正视一点的介绍,简单一点,符合本文设计目标的介绍如下

  • 接口方式引用
  • 具体执行时,根据某些条件,选中实际的子类执行

通过上面的描述,可以发现一个最大的优点就是:

  • 通过扩展接口的实现,就可以实现服务扩展;而不需要改原来的业务代码

b. demo辅助说明

一个简单的应用场景如下

报警系统demo

这个报警系统中,对于使用者而言,通过 IAlarm#sendMsg(level, msg) 来执行报警发送的方式,然而这一行的具体执行者是(忽略,日志报警,邮件报警还是短信报警)不确定的,通过SPI的实现方式将是如下

  • 如果level为1,则忽略报警内容
  • 如果level为2,则采用日志报警的方式来报警

如果我们想新添加一种报警方式呢?那也很简单,新建一个报警的实现

  • level == 5, 则采用微信报警

然后对于使用者而言,其他的地方都不用改,只是在传入的level参数换成5就可以了

3. 代理模式简述

代理模式,在Spring中可以说是非常非常非常常见的一种设计模式了,大名鼎鼎的AOP就是这个实现的一个经典case,常见的代理有两种实现方式

  • JDK方式
  • CGLIB方式

简单说一下,代理模式的定义和说明如下

其实在现实生活中代理模式还是非常多得,这里引入一个代理商的概念来加以描述,本来一个水果园直接卖水果就好了,现在中间来了一个水果超市,水果园的代销商,对水果进行分类,包装,然后再卖给用户,这其实也算是一种代理

百科定义:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

II. 方案设计与实现

了解完上面的前提之后,我们可以考虑下如何实现一个Spring容器中的SPI工具包

1. 目标拆分

首先确定大的生态环境为Spring,我们针对Bean做SPI功能的扩展,即定义一个SPI的接口,然后可以有多个实现类,并且全部都声明为Bean;

SPI的一个重要特点就是可以选中不同的实现来执行具体的代码,那么放在这里,就会有两种方案

  • 方案一:依赖注入时,直接根据选择条件,注入一个满足的实例,后续所有的SPI调用,都将走这个具体的实例调用执行
  • 方案二:依赖注入时,不注入具体的实例,反而注册一个代理类,在代理类中,根据调用的参数来选择具体匹配的实例来执行,因此后续的调用具体选中的实例将与传入的参数有关

方案对比

方案一 方案二
接近JDK的SPI使用方式 代理方式选中匹配的实例
优点:简单,使用以及后续维护简单 灵活, 支持更富想象力的扩展
缺点:一对一,复用性不够,不能支持前面的case 实现和调用方式跟繁琐一点,需要传入用于选择具体实例条件参数
每次选择子类都需要额外计算

对比上面的两个方案之后,选中第二个(当然主要原因是为了演示FactoryBean和代理实现SPI机制,如果选择方案一就没有这两个什么事情了)

选中方案之后,目标拆分就比较清晰了

  • 定义SPI接口,以及SPI的使用姿势(前提)
  • 一个生成代理类的FactoryBean (核心)

2. 方案设计

针对前面拆分的目标,进行方案设计,第一步就是接口相关的定义了

a. 接口定义

设计的SPI微型框架的核心为:在执行的时候,根据传入的参数来决定具体的实例来执行,因此我们的接口设计中,至少有一个根据传入的参数来判断是否选中这个实例的接口

1
2
3
public interface ISpi<T> {
boolean verify(T condition);
}

看到上面的实现之后,就会有一个疑问,如果有多个子类都满足这个条件怎么办?因此可以加一个排序的接口,返回优先级最高的匹配者

1
2
3
4
5
6
7
8
9
10
11
public interface ISpi<T> {
boolean verify(T condition);

/**
* 排序,数字越小,优先级越高
* @return
*/
default int order() {
return 10;
}
}

接口定义之后,使用者应该怎么用呢?

b. 使用约束

spi实现的约束

基于JDK的代理模式,一个最大的前提就是,只能根据接口来生成代理类,因此在使用SPI的时候,我们希望使用者先定义一个接口来继承ISpi,然后具体的SPI实现这个接口即可

其次就是在Spring的生态下,要求所有的SPI实现都是Bean,需要自动扫描或者配置注解方式声明,否者代理类就不太好获取所有的SPI实现了

spi使用的约束

在使用SPI接口时,通过接口的方式来引入,因为我们实际注入的会是代理类,因此不要写具体的实现类

单独看上面的说明,可能不太好理解,建议结合下面的实例演示对比

c. 代理类生成

这个属于最核心的地方了(虽说重要性为No1,但实现其实非常非常简单)

代理类主要目的就是在具体调用执行时,根据传入的参数来选中具体的执行者,执行后并返回对应的结果

  • 获取所有的SPI实现类(org.springframework.beans.factory.ListableBeanFactory#getBeansOfType(java.lang.Class<T>)
  • 通过jdk生成代理类,代理类中,遍历所有的SPI实现,根据传入的第一个参数作为条件进行匹配,找出首个命中的SPI实现类,执行

将上面的步骤具体实现,也就比较简单了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class SpiFactoryBean<T> implements FactoryBean<T> {
private Class<? extends ISpi> spiClz;

private List<ISpi> list;

public SpiFactoryBean(ApplicationContext applicationContext, Class<? extends ISpi> clz) {
this.spiClz = clz;

Map<String, ? extends ISpi> map = applicationContext.getBeansOfType(spiClz);
list = new ArrayList<>(map.values());
list.sort(Comparator.comparingInt(ISpi::order));
}

@Override
@SuppressWarnings("unchecked")
public T getObject() throws Exception {
// jdk动态代理类生成
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
for (ISpi spi : list) {
if (spi.verify(args[0])) {
// 第一个参数作为条件选择
return method.invoke(spi, args);
}
}

throw new NoSpiChooseException("no spi server can execute! spiList: " + list);
}
};

return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{spiClz},
invocationHandler);
}

@Override
public Class<?> getObjectType() {
return spiClz;
}
}

3. 实例演示

话说方案设计之后,应该就是实现了,然而因为实现过于简单,设计的过程中,也就顺手写了,就是上面的一个接口定义 ISpi 和一个用来生成动态代理类的SpiFactoryBean

接下来写一个简单的实例用于功能演示,定义一个IPrint用于文本输出,并给两个实现,一个控制台输出,一个日志输出

1
2
3
4
5
6
7
8
public interface IPrint extends ISpi<Integer> {

default void execute(Integer level, Object... msg) {
print(msg.length > 0 ? (String) msg[0] : null);
}

void print(String msg);
}

具体的实现类如下,外部使用者通过execute方法实现调用,其中level<=0时选择控制台输出;否则选则日志文件方式输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Component
public class ConsolePrint implements IPrint {
@Override
public void print(String msg) {
System.out.println("console print: " + msg);
}

@Override
public boolean verify(Integer condition) {
return condition <= 0;
}
}

@Slf4j
@Component
public class LogPrint implements IPrint {
@Override
public void print(String msg) {
log.info("log print: {}", msg);
}

@Override
public boolean verify(Integer condition) {
return condition > 0;
}
}

前面的步骤和一般的写法没有什么区别,使用的姿势又是怎样的呢?

1
2
3
4
5
6
7
8
9
10
11
12
@SpringBootApplication
public class Application {

public Application(IPrint printProxy) {
printProxy.execute(10, " log print ");
printProxy.execute(0, " console print ");
}

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

看上面的Application的构造方法,要求传入一个IPrint参数,Spring会从容器中找到一个bean作为参数传入,而这个bean就是我们生成的代理类,这样才可以根据不同的参数来选中具体的实现类

所以问题就是如何声明这个代理类了,配置如下,通过FactoryBean的方式来声明Bean,并添加上@Primary注解,这样就可以确保注入的是我们声明的代理类了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class PrintAutoConfig {

@Bean
public SpiFactoryBean printSpiPoxy(ApplicationContext applicationContext) {
return new SpiFactoryBean(applicationContext, IPrint.class);
}

@Bean
@Primary
public IPrint printProxy(SpiFactoryBean spiFactoryBean) throws Exception {
return (IPrint) spiFactoryBean.getObject();
}
}

上面的使用逻辑,涉及到的知识点在前面的博文中分别有过介绍,更多详情可以参考

接下来就是实际执行看下结果如何了

演示demo

III. 其他

0. 项目相关

a. 更多博文

基础篇

应用篇

b. 项目源码

1. 一灰灰Blog

一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

2. 声明

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

3. 扫描关注

一灰灰blog

QrCode

知识星球

goals


打赏 如果觉得我的文章对您有帮助,请随意打赏。
分享到