Chapter Index
OG

為Spring項目啟用插件化開發

為Spring項目啟用插件化開發

Spring Plugin 項目地址

https://github.com/spring-projects/spring-plugin

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方式无法比例的,主要体现在以下几个方面:

  • 通过插件的方式,不需要更改原来已经稳定的业务代码,对系统稳定性来说尤为重要(系统稳定是基础)
  • 与业务解耦,如果业务发生变化(在某个周期内),或者有新用户的活动,我们只需要构建我们的业务代码,核心框架层无需更改
  • 程序架构更清晰,分层设计更明显.