SpringBoot应用篇之FactoryBean及代理实现SPI机制示例

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: FactoryBean在Spring中算是一个比较有意思的存在了,虽然在日常的业务开发中,基本上不怎么会用到,但在某些场景下,如果用得好,却可以实现很多有意思的东西本篇博文主要介绍如何通过FactoryBean来实现一个类SPI机制的微型应用框架

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

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


文章内涉及到的知识点


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


I. 相关知识点



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


  • SPI相关技术博文汇总---By一灰灰Blog


1. demo背景说明


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

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


image.png

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


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


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


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


2. SPI简述


a. 基本定义


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


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


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


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


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


b. demo辅助说明


一个简单的应用场景如下


image.png

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


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


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


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


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


3. 代理模式简述


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


  • JDK方式
  • CGLIB方式


简单说一下,代理模式的定义和说明如下,更多详情可以参考: 实现MVC: 3. AOP实现准备篇代理模式


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

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


II. 方案设计与实现



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


1. 目标拆分


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


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


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


方案对比


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


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


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


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


2. 方案设计


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


a. 接口定义


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


public interface ISpi<T> {
    boolean verify(T condition);
}
复制代码


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


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实现类,执行


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


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用于文本输出,并给两个实现,一个控制台输出,一个日志输出


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();
    }
}
复制代码


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


  • FactoryBean的使用姿势,参考:181009-SpringBoot基础篇Bean之基本定义与使用
  • 配置类Configuration声明的方式,参考:181012-SpringBoot基础篇Bean之自动加载
  • @Primary注解的使用,参考: 181022-SpringBoot基础篇Bean之多实例选择


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

image.png



相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
21天前
|
安全 Java 编译器
springboot 整合表达式计算引擎 Aviator 使用示例详解
本文详细介绍了Google Aviator 这款高性能、轻量级的 Java 表达式求值引擎
|
30天前
|
架构师 Java 开发者
得物面试:Springboot自动装配机制是什么?如何控制一个bean 是否加载,使用什么注解?
在40岁老架构师尼恩的读者交流群中,近期多位读者成功获得了知名互联网企业的面试机会,如得物、阿里、滴滴等。然而,面对“Spring Boot自动装配机制”等核心面试题,部分读者因准备不足而未能顺利通过。为此,尼恩团队将系统化梳理和总结这一主题,帮助大家全面提升技术水平,让面试官“爱到不能自已”。
得物面试:Springboot自动装配机制是什么?如何控制一个bean 是否加载,使用什么注解?
|
5月前
|
Java 数据安全/隐私保护 网络架构
一个简单的示例在spring boot中实现国际化
一个简单的示例在spring boot中实现国际化
|
3月前
|
Java 数据库 开发者
深入剖析 SpringBoot 的 SPI 机制
【8月更文挑战第10天】在软件开发中,SPI(Service Provider Interface)机制是一种重要的服务发现和加载机制,尤其在构建模块化、可扩展的系统时尤为重要。SpringBoot作为Spring家族的一员,其内置的SPI机制不仅继承了Java SPI的设计思想,还进行了优化和扩展,以适应Spring Boot特有的需求。本文将深入剖析SpringBoot中的SPI机制,揭示其背后的原理与应用。
85 7
|
3月前
|
Java 开发者 Spring
"揭秘SpringBoot魔法SPI机制:一键解锁服务扩展新姿势,让你的应用灵活飞天!"
【8月更文挑战第11天】SPI(Service Provider Interface)是Java的服务提供发现机制,用于运行时动态查找和加载服务实现。SpringBoot在其基础上进行了封装和优化,通过`spring.factories`文件提供更集中的配置方式,便于框架扩展和组件替换。本文通过定义接口`HelloService`及其实现类`HelloServiceImpl`,并在`spring.factories`中配置,结合`SpringFactoriesLoader`加载服务,展示了SpringBoot SPI机制的工作流程和优势。
56 5
|
3月前
|
安全 Java UED
掌握SpringBoot单点登录精髓,单点登录是一种身份认证机制
【8月更文挑战第31天】单点登录(Single Sign-On,简称SSO)是一种身份认证机制,它允许用户只需在多个相互信任的应用系统中登录一次,即可访问所有系统,而无需重复输入用户名和密码。在微服务架构日益盛行的今天,SSO成为提升用户体验和系统安全性的重要手段。本文将详细介绍如何在SpringBoot中实现SSO,并附上示例代码。
74 0
|
3月前
|
消息中间件 Java Kafka
深入SpringBoot的心脏地带:掌握其核心机制的全方位指南
【8月更文挑战第29天】这段内容介绍了在分布式系统中起到异步通信与解耦作用的消息队列,并详细探讨了三种流行的消息队列产品:RabbitMQ、RocketMQ 和 Kafka。RabbitMQ 是一个基于 AMQP 协议的开源消息队列系统,支持多种消息模型,具有高可靠性及稳定性;RocketMQ 则是由阿里巴巴开源的高性能分布式消息队列,支持事务消息等多种特性;而 Kafka 是 LinkedIn 开源的分布式流处理平台,以其高吞吐量和良好的可扩展性著称。文中还提供了使用这三种消息队列产品的示例代码。总之,这三款产品各有优势,适用于不同场景。
16 0
|
3月前
|
消息中间件 Java Kafka
SpringBoot大揭秘:如何轻松掌握其核心机制?
【8月更文挑战第29天】这段内容介绍了在分布式系统中起到异步通信与解耦作用的消息队列,并详细探讨了三种流行的消息队列产品:RabbitMQ、RocketMQ 和 Kafka。RabbitMQ 是一个基于 AMQP 协议的开源消息队列系统,支持多种消息模型,具有高可靠性及稳定性;RocketMQ 则是由阿里巴巴开源的高性能分布式消息队列,支持事务消息等多种特性;而 Kafka 是 LinkedIn 开源的分布式流处理平台,以其高吞吐量和良好的可扩展性著称。文中还提供了使用这三种消息队列产品的示例代码。
15 0
WXM
|
3月前
|
存储 缓存 Java
|
3月前
|
消息中间件 Java Kafka
SpringBoot Kafka SSL接入点PLAIN机制收发消息
SpringBoot Kafka SSL接入点PLAIN机制收发消息
38 0