装了那么多IDEA插件,你知道其中原理吗?

2,965 阅读5分钟

控制复杂性是计算机编程的本质。           ——Brian Kernighan
在我们日常使用的一些软件的过程当中,往往会装上一些插件来满足一些个性化的需求,比如说我们会在IDEA装上"Maven Helper"插件来分析依赖冲突;在Chrome浏览器上面装上"vue-devtool"来调试程序;可见基本上所有成熟的软件都做了很好的插件支持。本文将主要介绍“插件模式”的基本实现原理,以及相关的实战操作。

插件模式

    熟悉GoF的23种设计模式的同学应该清楚,插件模式其实并不在其中,但是运用非常广泛。其主要核心目的也是为了增强系统扩展性,但是与普通的面向对象扩展方式不同的地方在于:普通的扩展往往发生在系统软件内部,而插件式的扩展往往体现在软件外部。比如说我们在某个项目中使用了策略模式,当需要添加新的策略时,我们不得不重新编译代码、打包和发布,新的策略才能生效。而插件式的扩展大多以一个新的组件(比如jar包)的方式加入到软件当中,系统软件本身并不需要重新打包部署,有些插件模式甚至可以做到热部署,即在程序运行过程中实现插件的动态加载,做到真正的即插即用。

插件架构

插件架构基本功能

  • 插件的定义
  • 插件的加载
  • 插件生命周期管理(安装、卸载、启动、停止、更新)
  • 插件间的交互机制
  • 插件的扩展

两种典型的插件架构产品

image.png 如上图所示,“平扩展”架构产品中插件只是用来做局部功能的扩展,而“微核+扩展”架构其核心本身只是一个插件加载器,所有功能统一靠插件实现。

插件模式基本实现原理

在一个插件框架当中,通常会涉及到以下概念:

  • ExtensionPoint: 扩展点,用来表示可以扩展的功能点。
  • Extension: 扩展,是对ExtensionPoint的扩展实现。
  • PluginDescriptor: 插件描述,即描述插件的元数据,定义了插件的基本描述、版本、以及运行插件所需要的的依赖等信息。通常一个PluginDescriptor可能对应一个Plugin的配置文件。
  • PluginRegistry: 插件注册器,主要用来对插件的注册和存储。
  • PluginManager: 插件管理,用来安装、激活插件实例。
  • Plugin: 插件实例,PluginManager激活插件之后,就会产生一个Plugin实例。

image.png

插件框架PF4J基本使用

PF4J简介

PF4J是一个Java轻量级的插件框架,可以实现动态加载,执行,卸载外部插件(支持jar以及zip),具体可以看官网:pf4j.org/

基础示例

本次DEMO项目主要涉及到3个模块:

  • plugin-api:定义可扩展接口
  • plugins:插件项目,可以包含多个插件,需要实现plugin-api中定义的接口
  • plugin-app:主程序,需要依赖plugin-api,加载并执行plugins

引入PF4J依赖

<dependency>
  <groupId>org.pf4j</groupId>
  <artifactId>pf4j</artifactId>
  <version>3.0.1</version>
</dependency>

定义可扩展接口(plugin-api)

首先定一个可扩展接口,需要继承ExtensionPoint(标记接口):

public interface Greeting extends ExtensionPoint {

    String getGreeting();
}

扩展接口实现(plugins)

插件需要实现plugin-api定义的接口,并且使用@Extension标记:

@Extension
public class WelcomeGreeting implements Greeting {

    public String getGreeting() {
        return "Welcome,xs!!!";
    }
}

如果需要关注插件的一些生命周期事件,可以通过继承Plugin类来实现:

public class WelcomePlugin extends Plugin {
    
    //从wrapper对象我们可以拿到Plugin manager、descriptor 等上下文信息
    public WelcomePlugin(PluginWrapper wrapper) {
        super(wrapper);
    }

    @Override
    public void start() {
        System.out.println("WelcomePlugin.start()");
    }

    @Override
    public void stop() {
        System.out.println("WelcomePlugin.stop()");
    }
    
    @Override
    public void delete() {
        System.out.println("WelcomePlugin.delete()");
    }
    
}

插件打包(plugins)

插件打包时,需要往MANIFEST.MF写入插件信息,此处使用maven插件(打包命令为package):

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-jar-plugin</artifactId>
  <version>2.3.1</version>
  <configuration>
    <archive>
      <manifestEntries>
        <Plugin-Id>welcome-plugin</Plugin-Id>
        <Plugin-Version>0.0.1</Plugin-Version>
         <!-- 关注插件生命周期时需要声明 -->
        <Plugin-Class>plugins.WelcomePlugin</Plugin-Class>
      </manifestEntries>
    </archive>
  </configuration>
</plugin>

注意:MANIFEST.MFPlugin-Id以及Plugin-Version是必要信息。

加载执行插件(plugin-app)

public class Main {

    public static void main(String[] args) {
        // jar插件管理器
        PluginManager pluginManager = new JarPluginManager();

        // 加载指定路径插件
        pluginManager.loadPlugin(Paths.get("plugins-0.0.1-SNAPSHOT.jar"));

        // 启动指定插件(也可以加载所有插件)
        pluginManager.startPlugin("welcome-plugin");

        // 执行插件
        List<Greeting> greetings = pluginManager.getExtensions(Greeting.class);
        for (Greeting greeting : greetings) {
            System.out.println(">>> " + greeting.getGreeting());
        }

        // 停止并卸载指定插件
        pluginManager.stopPlugin("welcome-plugin");
        pluginManager.unloadPlugin("welcome-plugin");
    }
}

框架PF4J 与 Spring 整合

引入依赖

引入整合spring框架的依赖

<dependency>
    <groupId>org.pf4j</groupId>
    <artifactId>pf4j-spring</artifactId>
    <version>${pf4j-spring.version}</version>
</dependency>    

创建核心配置Bean

@Configuration
public class SpringConfiguration {

    @Bean
    public SpringPluginManager pluginManager() {
        return new SpringPluginManager();
    }

    @Bean
    @DependsOn("pluginManager")
    public Greetings greetings() {
        return new Greetings();
    }
}

将PF4J扩展作为Spring Bean使用:

public class Greetings {

    @Autowired
    private List<Greeting> greetings;

    public void printGreetings() {
        System.out.println(String.format("Found %d extensions for extension point '%s'", greetings.size(), Greeting.class.getName()));
        for (Greeting greeting : greetings) {
            System.out.println(">>> " + greeting.getGreeting());
        }
    }
}

应用启动类

public class Boot {

    public static void main(String[] args) {
        // 注解方式启动容器上下文
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfiguration.class);

        // 从容器中获取扩展点
        Greetings greetings = applicationContext.getBean(Greetings.class);
        greetings.printGreetings();

        // 从容器中获取插件管理器
        PluginManager pluginManager = applicationContext.getBean(PluginManager.class);
        // 下线插件 
        pluginManager.stopPlugins();
    }
}

框架PF4J 与 Spring Boot整合

关于PF4JSpring Boot框架整合可以在官方提供的pf4j-spring基础上自行扩展,实现思路大体一致,这里也给出一个质量不错的三方扩展框架 springboot-plugin-framework,可以用来参考实现如何整合Spring Boot框架。

总结

实际项目开发过程中,如果碰到一些系统需要在运行时动态扩展,没法直接使用策略模式解决问题时,不妨采用插件化架构进行系统设计。此外值得注意的是,插件的版本维护要与主应用一样对待,应该要有一个合理的版本管理机制,否则生产加载错插件版本,可不是闹着玩的。

参考

《从码农到工匠》