springfox 源码分析(三) 初探Spring Plugin插件系统
时间:2019-5-22 12:46:50
地点:单位、家中
前言
同MapStuct
组件一样,因为springfox中运用到了Spring Plugin插件系统,我们对研究springfox源码之前,先来学习一下Spring Plugin插件的机制
因为在工作中很少使用到Spring Plugin
,所以学习记录下
Spring Plugin
Github:https://github.com/spring-projects/spring-plugin
可以说作为Spring项目中的Spring Plugin
,确实相对小众,并没有像Spring其他的项目那么流行,甚至在其他流行的框架中,都很少见到他的身影.
截止目前(2019-5-22 13:54:08),Github 的Star为222,fork数66
Spring Plugin是世界上最小规模的插件系统
如今构建可扩展的体系结构是创建可维护应用程序的核心原则。 这就是像OSGi这样的完全成熟的插件环境如今如此受欢迎的原因。 不幸的是,OSGi的引入给项目带来了很多复杂性。
Spring Plugin
通过提供扩展核心系统功能的插件实现的核心灵活性,但不提供动态类加载或运行时安装和插件部署等核心OSGi功能,同时为插件开发提供了更实用的方法。 虽然Spring Plugin并不像OSGi那样强大,但它可以满足穷人构建模块化可扩展应用程序的要求。
假如你希望构建一个可扩展的应用系统,你可能需要从以下几点进行考虑:
- 无论出于何种原因,您都无法将OSGi用作完全成熟的插件架构
- 提供专用的插件接口来满足可扩展性
- 通过简单地提供捆绑在JAR文件中并在类路径中可用的插件接口的实现来扩展核心系统
- 使用Spring来构建应用系统
示例
我们通过一个小示例,来对Spring Plugin系统有一个初步的了解
Spring Plugin提供一个标准的Plugin<S>
接口供开发人员继承使用声明自己的插件机制,然后通过@EnablePluginRegistries
注解依赖注入到Spring的容器中,Spring容器会为我们自动匹配到插件的所有实现子对象,最终我们在代码中使用时,通过依赖注入注解,注入PluginRegistry<T extends Plugin<S>, S>
对象拿到插件实例进行操作。
Plugin<S>
接口声明了一个接口实现,标注实现该插件是否支持,因为有可能存在多个接口实现的情况
我们在使用时,可能这样调用:
List<Plugin<S>> plugins=plugin.getPlugins();
S delimiter;
for(Plugin<S> p:plugins){
if(p.supports(delimiter)){
p.doSomeThing();//
}
}
从应用程序的扩展性来说,开发灵活的插件系统是我们每个开发人员都需考虑的
假设目前我们有一个移动电话充值系统,在业务初期发展中,业务的目标是保证稳定性,拥有充值业务
在maven配置中先来引入相关的jar包
<properties>
<logback.version>1.2.3</logback.version>
<org.slf4j.version>1.7.21</org.slf4j.version>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-core -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.0.9.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.plugin/spring-plugin-core -->
<dependency>
<groupId>org.springframework.plugin</groupId>
<artifactId>spring-plugin-core</artifactId>
<version>1.2.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${org.slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${org.slf4j.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
<exclusions>
<exclusion>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
</exclusion>
<exclusion>
<groupId>javax.jms</groupId>
<artifactId>jms</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jdmk</groupId>
<artifactId>jmxtools</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jmx</groupId>
<artifactId>jmxri</artifactId>
</exclusion>
</exclusions>
<scope>runtime</scope>
</dependency>
</dependencies>
先来看我们的客户属性:
MobileCustomer
/***
*
* @since:spring-plugin-demo 1.0
* @author <a href="mailto:xiaoymin@foxmail.com">xiaoymin@foxmail.com</a>
* 2019/05/22 14:41
*/
public class MobileCustomer {
/***
* 电话号码
*/
private String tel;
//setter getter
/***
* 是否老用户
*/
private boolean old=false;
}
声明我们的充值接口:
/***
* 我们有电话增值业务,业务中有充值方法
* @since:spring-plugin-demo 1.0
* @author <a href="mailto:xiaoymin@foxmail.com">xiaoymin@foxmail.com</a>
* 2019/05/22 14:42
*/
public interface MobileIncrementBusiness{
/***
* 电话充值
* @param mobileCustomer
* @param money 金额
*/
void increment(MobileCustomer mobileCustomer, int money);
}
充值接口目前有一个接口,充值,根据客户和充值金额进行充值的方法
接下来,我们来实现充值的业务逻辑,假设当前我们叫他V1版本
/***
* 第一版本的充值系统
* @since:spring-plugin-demo 1.0
* @author <a href="mailto:xiaoymin@foxmail.com">xiaoymin@foxmail.com</a>
* 2019/05/22 14:44
*/
public class MobileIncrementV1 implements MobileIncrementBusiness {
Logger logger= LoggerFactory.getLogger(MobileIncrementV1.class);
@Override
public void increment(MobileCustomer mobileCustomer, int money) {
logger.info("给{}充值电话费,充值金额:{}",mobileCustomer.getTel(),money);
logger.info("充值完成.");
}
}
此时,我们在系统中加入充值插件的配置
@Configuration
public class MobileConfig {
@Bean
public MobileIncrementV1 mobileIncrementV1(){
return new MobileIncrementV1();
}
}
我们在通过对外提供一个业务Service,来调用我们的充值方法
/***
*
* @since:spring-plugin-demo 1.0
* @author <a href="mailto:xiaoymin@foxmail.com">xiaoymin@foxmail.com</a>
* 2019/05/22 15:00
*/
@Component
public class CustomerService {
@Autowired
MobileIncrementV1 mobileIncrementV1;
public void increments(MobileCustomer mobileCustomer,int money){
//对人员进行充值
mobileIncrementV1.increment(mobileCustomer,money);
}
}
通过CustomerService
方法,就可以调用我们的充值插件进行话费的充值
我们来模拟
public class MobileTest {
public static void main(String[] args) {
AnnotationConfigApplicationContext context=
new AnnotationConfigApplicationContext("com.xiaominfo.cloud.plugin.phone");
CustomerService customerService=context.getBean(CustomerService.class);
MobileCustomer mobileCustomer=new MobileCustomer("13567662664");
mobileCustomer.setOld(true);
customerService.increments(mobileCustomer,120);
}
}
我们对电话13567662664
进行充值120元
控制台输出:
2019-05-22 15:11:21,391 INFO (MobileIncrementV1.java:27)- 给13567662664充值电话费,充值金额:120
2019-05-22 15:11:21,394 INFO (MobileIncrementV1.java:28)- 充值完成.
插件的使用到这里就完成了,此时我们或许会有疑问?不是说满足应用程序的可扩展性吗?此处并未体现出来啊?
假设随着电话公司的业务逐步扩大,此时,电话公司推出了老用户充话费折扣的活动,具体的规则是
- 当前电话号码必须是老用户(通过old字段来区分)
- 充值金额必须>100
- 折扣金额为充值金额*10%,返冲到客户的手机上
此时,针对该活动,我们为了满足以上业务,传统的做法是继续在MobileIncrementV1
代码中添加业务逻辑
代码会是这样:
public class MobileIncrementV1 implements MobileIncrementBusiness {
Logger logger= LoggerFactory.getLogger(MobileIncrementV1.class);
@Override
public void increment(MobileCustomer mobileCustomer, int money) {
logger.info("给{}充值电话费,充值金额:{}",mobileCustomer.getTel(),money);
logger.info("充值完成.");
if (mobileCustomer.isOld()){
logger.info("老用户折扣");
if (money>100){
BigDecimal big=new BigDecimal(money).multiply(new BigDecimal(0.1));
logger.info("当前充值金额>100元,返冲{}元",big.intValue());
}
}
}
@Override
public boolean supports(MobileCustomer delimiter) {
return true;
}
}
改版后的业务逻辑,我们在V1中添加了业务逻辑,满足老客户是进行返冲
运行后,控制台:
2019-05-22 15:24:50,229 INFO (MobileIncrementV1.java:29)- 给13567662664充值电话费,充值金额:120
2019-05-22 15:24:50,231 INFO (MobileIncrementV1.java:30)- 充值完成.
2019-05-22 15:24:50,232 INFO (MobileIncrementV1.java:32)- 老用户折扣
2019-05-22 15:24:50,236 INFO (MobileIncrementV1.java:35)- 当前充值金额>100元,返冲12元
程序没有任何问题,同时也满足了活动要求,但是这样做的缺陷也是明显的,主要如下
- 在V1充值系统中,业务已经稳定,此时,如果我们的返冲活动业务比较复杂的情况下,会出现测试不到的情况,新业务逻辑代码更新后,对非老用户的充值稳定性存在影响
- 如果我们的业务规则变化越来越多,此时我们的V1中的business方法会越来越臃肿,不利于维护
- 假如我们的活动是有时效性的情况下,在某一段时间,这段业务逻辑有空,而时效性失效后,这段业务逻辑是冗余的,但是它仍然存在于我们的主业务方法中.
那么,针对以上问题,我们应该如何解决呢?
Spring Plugin帮助我们解决了此问题,如果用Plugin的方式,我们应该如何做呢?
首先,改进我们的增值业务MobileIncrementBusiness
,改业务接口继承Plugin<S>
,代码如下:
public interface MobileIncrementBusiness extends Plugin<MobileCustomer>{
/***
* 电话充值
* @param mobileCustomer
* @param money 金额
*/
void increment(MobileCustomer mobileCustomer, int money);
}
我们继承了Plugin的接口,所以我们的子类充值V1业务代码也需要实现Plugin的supports方法,代码如下:
public class MobileIncrementV1 implements MobileIncrementBusiness {
Logger logger= LoggerFactory.getLogger(MobileIncrementV1.class);
@Override
public void increment(MobileCustomer mobileCustomer, int money) {
logger.info("给{}充值电话费,充值金额:{}",mobileCustomer.getTel(),money);
logger.info("充值完成.");
}
@Override
public boolean supports(MobileCustomer delimiter) {
return true;
}
}
此时,我们把老用户返冲的代码移除了,我们通过Plugin的方式来帮助我们
我们新建老用户返冲的业务实现MobileIncrementDiscount
:
public class MobileIncrementDiscount implements MobileIncrementBusiness {
Logger logger= LoggerFactory.getLogger(MobileIncrementDiscount.class);
@Override
public void increment(MobileCustomer mobileCustomer, int money) {
if (supports(mobileCustomer)){
logger.info("老用户折扣");
if (money>100){
if (money>100){
BigDecimal big=new BigDecimal(money).multiply(new BigDecimal(0.1));
logger.info("当前充值金额>100元,返冲{}元",big.intValue());
}
}
}
}
/***
* 来用户才满足
* @param delimiter
* @return
*/
@Override
public boolean supports(MobileCustomer delimiter) {
return delimiter.isOld();
}
}
此时,我们启用Plugin插件系统,将我们的返冲实现业务注入到系统中
@Configuration
@EnablePluginRegistries({MobileIncrementBusiness.class})
public class MobileConfig {
@Bean
public MobileIncrementV1 mobileIncrementV1(){
return new MobileIncrementV1();
}
@Bean
public MobileIncrementDiscount mobileIncrementDiscount(){
return new MobileIncrementDiscount();
}
}
最后,我们修改我们的CustomerService
中的充值方法
@Component
public class CustomerService {
@Autowired
private PluginRegistry<MobileIncrementBusiness,MobileCustomer> mobileCustomerPluginRegistry;
public void increments(MobileCustomer mobileCustomer,int money){
//获取插件
List<MobileIncrementBusiness> plugins=mobileCustomerPluginRegistry.getPlugins();
for (MobileIncrementBusiness incrementBusiness:plugins){
//对人员进行充值
incrementBusiness.increment(mobileCustomer,money);
}
}
}
此时,我们在来运行我们的Test测试
AnnotationConfigApplicationContext context=new AnnotationConfigApplicationContext("com.xiaominfo.cloud.plugin.phone");
CustomerService customerService=context.getBean(CustomerService.class);
MobileCustomer mobileCustomer=new MobileCustomer("13567662664");
mobileCustomer.setOld(true);
customerService.increments(mobileCustomer,120);
控制台输出:
2019-05-22 15:42:01,743 INFO (MobileIncrementV1.java:29)- 给13567662664充值电话费,充值金额:120
2019-05-22 15:42:01,745 INFO (MobileIncrementV1.java:30)- 充值完成.
2019-05-22 15:42:01,746 INFO (MobileIncrementDiscount.java:28)- 老用户折扣
2019-05-22 15:42:01,752 INFO (MobileIncrementDiscount.java:32)- 当前充值金额>100元,返冲12元
通过控制台,我们发现,和在v1业务中继续新增代码的方式,效果是完全相同的,但是对于整个系统的扩展性来说,是V1方式无法比例的,主要体现在以下几个方面:
- 通过插件的方式,不需要更改原来已经稳定的业务代码,对系统稳定性来说尤为重要(系统稳定是基础)
- 与业务解耦,如果业务发生变化(在某个周期内),或者有新用户的活动,我们只需要构建我们的业务代码,核心框架层无需更改
- 程序架构更清晰,分层设计更明显.
源码分析
相信通过上面的示例,我们对Spring Plugin插件技术组件有一个初步的了解,接下来我们看看Spring Plugin的源码实现
既然是号称世界上规模最小的插件系统,通过我们的使用来看,确实也够简单,所以Spring Plugin的代码量也是很精悍.
通过GitHub下载下来的源码,总共也就三个包
这对于我们学习他的源码、设计模式来说,反而是好事情.
先来看Plugin涉及到的关键类图:
我们最终使用插件时,通过PluginRegistry来获取已实现的插件bean实例,该插件提供了几个主要方法:
Optional<T>
getPluginFor(S delimiter):根据特定条件获取插件的Optional对象(第一个)- T getRequiredPluginFor(S delimiter):根据条件获取插件对象,如果没有,则抛出异常
- getPlugins():获取所有插件
- contains(T):是否包含插件
- ...
我们在使用Spring Plugin组件的时候,主要有以下几个步骤:
- 在我们的Configuration配置类上通过注解
@EnablePluginRegistries
注入相应的Plugin接口的class - 在Configuration配置类中注入Plugin的实现类实体Bean
- 通过
@Autowired
注解,并使用PluginRegistry<T extend Plugin<S>,S>
的方式拿到我们的plugin实例,然后再业务方法中进行使用.
这里有两个关键点:
1、@EnablePluginRegistries
注解具体的作用
2、PluginRegistry是接口,通过@Antowired
注入,具体的实现类在哪儿?
带着这两个疑问点,我们先来看@EnablePluginRegistries
的代码:
EnablePluginRegistries.java
/**
* 为开启使用Plugin插件的类型应用启用PluginRegistry的实例注入
* @see #value()
* @author Oliver Gierke
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Import(PluginRegistriesBeanDefinitionRegistrar.class)
public @interface EnablePluginRegistries {
/**
*
* The {@link Plugin} types to register {@link PluginRegistry} instances for. The registries will be named after the
* uncapitalized plugin type extended with {@code Registry}. So for a plugin interface {@code SamplePlugin} the
* exposed bean name will be {@code samplePluginRegistry}. This can be used on the client side to make sure you get
* the right {@link PluginRegistry} injected by using the {@link Qualifier} annotation and referring to that bean
* name. If the auto-generated bean name collides with one already in your application you can use the
* {@link Qualifier} annotation right at the plugin interface to define a custom name.
*
* @return
*/
Class<? extends Plugin<?>>[] value();
}
通过注释我们得知该注解的作用
- 注册PluginRegistry的实例Bean,并以此命名
- 导入
PluginRegistriesBeanDefinitionRegistrar
类进行实例Bean注入
PluginRegistry的注入规则是,首字母变小写,例如SimplePluginRegistry
的实例bean,在Spring容器中的beanName为simplePluginRegistry
如果系统的命名和自动生成的名称相冲突,可以使用@Qualifier
注解来强制命名匹配以解决此问题
来看PluginRegistriesBeanDefinitionRegistrar.java
代码:
/**
* {@link ImportBeanDefinitionRegistrar} to register {@link PluginRegistryFactoryBean} instances for type listed in
* {@link EnablePluginRegistries}. Picks up {@link Qualifier} annotations used on the plugin interface and forwards them
* to the bean definition for the factory.
* 为pluginRegistry接口注入动态实例bean对象
*
* @author Oliver Gierke
*/
public class PluginRegistriesBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
private static final Logger LOG = LoggerFactory.getLogger(PluginRegistriesBeanDefinitionRegistrar.class);
/*
* importingClassMetadata:此参数为通过@EnablePluginRegistries注解标注的类型注解元数据信息对象
* registry:注入bean对象
*
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
//获取当前enablePluginRegistries注解类信息
Map<String, Object> annotationAttributes = importingClassMetadata
.getAnnotationAttributes(EnablePluginRegistries.class.getName());
//判断是否为空
if (annotationAttributes == null) {
LOG.info("No EnablePluginRegistries annotation found on type {}!", importingClassMetadata.getClassName());
return;
}
//获取什么的类型集合
//例如我们在示例中使用的@EnablePluginRegistries({MobileIncrementBusiness.class})
//此处会拿到MobileIncrementBusiness.class这个type,types.length=1
Class<?>[] types = (Class<?>[]) annotationAttributes.get("value");
//循环遍历
for (Class<?> type : types) {
//获取PluginRegistryFactoryBean类的实体bean定义builder
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(PluginRegistryFactoryBean.class);
builder.addPropertyValue("type", type);
RootBeanDefinition beanDefinition = (RootBeanDefinition) builder.getBeanDefinition();
beanDefinition.setTargetType(getTargetType(type));
Qualifier annotation = type.getAnnotation(Qualifier.class);
// If the plugin interface has a Qualifier annotation, propagate that to the bean definition of the registry
if (annotation != null) {
AutowireCandidateQualifier qualifierMetadata = new AutowireCandidateQualifier(Qualifier.class);
qualifierMetadata.setAttribute(AutowireCandidateQualifier.VALUE_KEY, annotation.value());
beanDefinition.addQualifier(qualifierMetadata);
}
//获取bean的默认名称
// Default
String beanName = annotation == null //
? StringUtils.uncapitalize(type.getSimpleName() + "Registry") //
: annotation.value();
//动态注入
registry.registerBeanDefinition(beanName, builder.getBeanDefinition());
}
}
/**
* Returns the target type of the {@link PluginRegistry} for the given plugin type.
*
* @param pluginType must not be {@literal null}.
* @return
*/
private static ResolvableType getTargetType(Class<?> pluginClass) {
Assert.notNull(pluginClass, "Plugin type must not be null!");
ResolvableType delimiterType = ResolvableType.forClass(Plugin.class, pluginClass).getGeneric(0);
ResolvableType pluginType = ResolvableType.forClass(pluginClass);
return ResolvableType.forClassWithGenerics(OrderAwarePluginRegistry.class, pluginType, delimiterType);
}
}
通过以上代码,我们知道:
- 通过
@EnablePluginRegistries
会为我们动态注入PluginRetry的实体bean PluginRegistryFactoryBean
会产生一个目标bean的代理,此目标bean真是PluginRegistry接口的实例,首先找到容器中实现了Plugin插件接口的实体bean,最终得到一个List<Plugin<S>>
的集合- 通过拿到该Plugins的结合,在通过
OrderAwarePluginRegistry.create(List<Plugin<S>>)
的方法来创建PluginRetry接口的默认实例 - 通过上面的类图其实我们知道,PluginRetry的接口拥有他的默认子类实现,为OrderAwarePluginRegistry
回过头来看我们示例中的CustomerService
的Plugin调用方式
@Component
public class CustomerService {
@Autowired
private PluginRegistry<MobileIncrementBusiness,MobileCustomer> mobileCustomerPluginRegistry;
}
通过制定泛型T和delimiter的S,最终通过依赖注入匹配到PluginRegistry的实例bean
我们可以通过调试来查看我们最终的mobileCustomerPluginRegistry
是否和我们通过读源码的方式得到的一致:
我们通过Debug断点来跟踪
从上图中我们可以看到,PluginRegistry的最终实例是OrderAwarePluginRegistry
实体对象
整个过程也到此结束
总结
我们通过该篇文章的分析,了解到了Spring Plugin组件的工作方式,大致跟踪学习了Plugin的初始化过程
不知道通过上面的介绍,你是否会在工作中更多的使用Spring Plugin组件的,至少从目前来看,他的使用还是很简单的,对于应用程序的可扩展性也是极强的.
Springfox
的源码中大量的使用了Spring Plugin的这种方式,相信通过这篇文章,能对后面我们研究学习Springfox的源码有一个很大的提升和帮助.