更多Spring文章,欢迎点击 一灰灰Blog-Spring专题java
FactoryBean在Spring中算是一个比较有意思的存在了,虽然在平常的业务开发中,基本上不怎么会用到,但在某些场景下,若是用得好,却能够实现不少有意思的东西git
本篇博文主要介绍如何经过FactoryBean来实现一个类SPI机制的微型应用框架github
文章内涉及到的知识点spring
在看下面的内容以前,得知道一下什么是SPI,以及SPI的用处和JDK实现SPI的方式,对于这一块有兴趣了解的童鞋,能够看一下我的以前写的相关文章编程
在开始以前,有必要了解一下,咱们准备作的这个东西,到底适用于什么样的场景。设计模式
在电商中,有一个比较恰当的例子,商品详情页的展现。拿淘宝系的详情页做为背景来讲明(没有在阿里工做过,下面的东西纯粹是为了说明应用场景而展开)微信
假设有这么三个详情页,咱们设定一个大前提,底层的数据层提供方都是一套的,商品详情展现的服务彻底能够作到复用,即三个性情页中,绝大多数的东西都同样,只是不一样的详情页车重点不一样而已。数据结构
如上图中,咱们假定有细微区别的几个地方app
位置 | 淘宝详情 | 天猫详情 | 咸鱼详情 | 说明 |
---|---|---|---|---|
banner | 显示淘宝的背景墙 | 显示天猫的广告位 | 咸鱼的坑位 | 三者数据结构彻底一致,仅图片url不一样 |
推荐 | 推荐同类商品 | 推荐店家其余商品 | 推荐同类二手产品 | 数据结构相同,内容不一样 |
评价 | 商品评价 | 商品评价 | 没有评价,改成留言 | |
促销 | 优惠券 | 天猫积分券 | 没有券 | - |
根据上面的简单对比,其实只想表达一个意思,业务基本上一致,仅仅只有不多的一些东西不一样,须要定制化,这个时候能够考虑用SPI来支持定制化的服务框架
SPI的全名为Service Provider Interface,简单的总结下java spi机制的思想。咱们系统里抽象的各个模块,每每有不少不一样的实现方案,好比日志模块的方案,xml解析模块、jdbc模块的方案等。面向的对象的设计里,咱们通常推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,若是须要替换一种实现,就须要修改代码。为了实如今模块装配的时候能不在程序里动态指明,这就须要一种服务发现机制。 java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制
上面是相对正视一点的介绍,简单一点,符合本文设计目标的介绍以下
经过上面的描述,能够发现一个最大的优势就是:
一个简单的应用场景以下
这个报警系统中,对于使用者而言,经过 IAlarm#sendMsg(level, msg)
来执行报警发送的方式,然而这一行的具体执行者是(忽略,日志报警,邮件报警仍是短信报警)不肯定的,经过SPI的实现方式将是以下
若是咱们想新添加一种报警方式呢?那也很简单,新建一个报警的实现
而后对于使用者而言,其余的地方都不用改,只是在传入的level参数换成5就能够了
代理模式,在Spring中能够说是很是很是很是常见的一种设计模式了,大名鼎鼎的AOP就是这个实现的一个经典case,常见的代理有两种实现方式
简单说一下,代理模式的定义和说明以下,更多详情能够参考: 实现MVC: 3. AOP实现准备篇代理模式
其实在现实生活中代理模式仍是很是多得,这里引入一个代理商的概念来加以描述,原本一个水果园直接卖水果就行了,如今中间来了一个水果超市,水果园的代销商,对水果进行分类,包装,而后再卖给用户,这其实也算是一种代理
百科定义:为其余对象提供一种代理以控制对这个对象的访问。在某些状况下,一个对象不适合或者不能直接引用另外一个对象,而代理对象能够在客户端和目标对象之间起到中介的做用。
了解完上面的前提以后,咱们能够考虑下如何实现一个Spring容器中的SPI工具包
首先肯定大的生态环境为Spring,咱们针对Bean作SPI功能的扩展,即定义一个SPI的接口,而后能够有多个实现类,而且所有都声明为Bean;
SPI的一个重要特色就是能够选中不一样的实现来执行具体的代码,那么放在这里,就会有两种方案
方案对比
方案一 | 方案二 |
---|---|
接近JDK的SPI使用方式 | 代理方式选中匹配的实例 |
优势:简单,使用以及后续维护简单 | 灵活, 支持更富想象力的扩展 |
缺点:一对一,复用性不够,不能支持前面的case | 实现和调用方式跟繁琐一点,须要传入用于选择具体实例条件参数 <br/> 每次选择子类都须要额外计算 |
对比上面的两个方案以后,选中第二个(固然主要缘由是为了演示FactoryBean和代理实现SPI机制,若是选择方案一就没有这两个什么事情了)
选中方案以后,目标拆分就比较清晰了
针对前面拆分的目标,进行方案设计,第一步就是接口相关的定义了
设计的SPI微型框架的核心为:在执行的时候,根据传入的参数来决定具体的实例来执行,所以咱们的接口设计中,至少有一个根据传入的参数来判断是否选中这个实例的接口
public interface ISpi<T> { boolean verify(T condition); }
看到上面的实现以后,就会有一个疑问,若是有多个子类都知足这个条件怎么办?所以能够加一个排序的接口,返回优先级最高的匹配者
public interface ISpi<T> { boolean verify(T condition); /** * 排序,数字越小,优先级越高 * @return */ default int order() { return 10; } }
接口定义以后,使用者应该怎么用呢?
spi实现的约束
基于JDK的代理模式,一个最大的前提就是,只能根据接口来生成代理类,所以在使用SPI的时候,咱们但愿使用者先定义一个接口来继承ISpi
,而后具体的SPI实现这个接口便可
其次就是在Spring的生态下,要求全部的SPI实现都是Bean,须要自动扫描或者配置注解方式声明,否者代理类就不太好获取全部的SPI实现了
spi使用的约束
在使用SPI接口时,经过接口的方式来引入,由于咱们实际注入的会是代理类,所以不要写具体的实现类
单独看上面的说明,可能不太好理解,建议结合下面的实例演示对比
这个属于最核心的地方了(虽然说重要性为No1,但实现其实很是很是简单)
代理类主要目的就是在具体调用执行时,根据传入的参数来选中具体的执行者,执行后并返回对应的结果
org.springframework.beans.factory.ListableBeanFactory#getBeansOfType(java.lang.Class<T>)
)将上面的步骤具体实现,也就比较简单了
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; } }
话说方案设计以后,应该就是实现了,然而由于实现过于简单,设计的过程当中,也就顺手写了,就是上面的一个接口定义 ISpi
和一个用来生成动态代理类的SpiFactoryBean
接下来写一个简单的实例用于功能演示,定义一个IPrint
用于文本输出,并给两个实现,一个控制台输出,一个日志输出
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
时选择控制台输出;不然选则日志文件方式输出
@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; } }
前面的步骤和通常的写法没有什么区别,使用的姿式又是怎样的呢?
@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
注解,这样就能够确保注入的是咱们声明的代理类了
@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(); } }
上面的使用逻辑,涉及到的知识点在前面的博文中分别有过介绍,更多详情能够参考
Configuration
声明的方式,参考:181012-SpringBoot基础篇Bean之自动加载接下来就是实际执行看下结果如何了
基础篇
应用篇
一灰灰的我的博客,记录全部学习和工做中的博文,欢迎你们前去逛逛
尽信书则不如,以上内容,纯属一家之言,因我的能力有限,不免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激
一灰灰blog
知识星球