控制复杂性是计算机编程的本质。 ——Brian Kernighan
在我们日常使用的一些软件的过程当中,往往会装上一些插件来满足一些个性化的需求,比如说我们会在IDEA装上"Maven Helper"插件来分析依赖冲突;在Chrome浏览器上面装上"vue-devtool"来调试程序;可见基本上所有成熟的软件都做了很好的插件支持。本文将主要介绍“插件模式”的基本实现原理,以及相关的实战操作。
插件模式
熟悉GoF的23种设计模式的同学应该清楚,插件模式其实并不在其中,但是运用非常广泛。其主要核心目的也是为了增强系统扩展性,但是与普通的面向对象扩展方式不同的地方在于:普通的扩展往往发生在系统软件内部,而插件式的扩展往往体现在软件外部。比如说我们在某个项目中使用了策略模式,当需要添加新的策略时,我们不得不重新编译代码、打包和发布,新的策略才能生效。而插件式的扩展大多以一个新的组件(比如jar包)的方式加入到软件当中,系统软件本身并不需要重新打包部署,有些插件模式甚至可以做到热部署,即在程序运行过程中实现插件的动态加载,做到真正的即插即用。
插件架构
插件架构基本功能
- 插件的定义
- 插件的加载
- 插件生命周期管理(安装、卸载、启动、停止、更新)
- 插件间的交互机制
- 插件的扩展
两种典型的插件架构产品
如上图所示,“平扩展”架构产品中插件只是用来做局部功能的扩展,而“微核+扩展”架构其核心本身只是一个插件加载器,所有功能统一靠插件实现。
插件模式基本实现原理
在一个插件框架当中,通常会涉及到以下概念:
- ExtensionPoint: 扩展点,用来表示可以扩展的功能点。
- Extension: 扩展,是对ExtensionPoint的扩展实现。
- PluginDescriptor: 插件描述,即描述插件的元数据,定义了插件的基本描述、版本、以及运行插件所需要的的依赖等信息。通常一个PluginDescriptor可能对应一个Plugin的配置文件。
- PluginRegistry: 插件注册器,主要用来对插件的注册和存储。
- PluginManager: 插件管理,用来安装、激活插件实例。
- Plugin: 插件实例,PluginManager激活插件之后,就会产生一个Plugin实例。
插件框架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.MF
中Plugin-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整合
关于PF4J
与Spring Boot
框架整合可以在官方提供的pf4j-spring
基础上自行扩展,实现思路大体一致,这里也给出一个质量不错的三方扩展框架 springboot-plugin-framework,可以用来参考实现如何整合Spring Boot
框架。
总结
实际项目开发过程中,如果碰到一些系统需要在运行时动态扩展,没法直接使用策略模式解决问题时,不妨采用插件化架构进行系统设计。此外值得注意的是,插件的版本维护要与主应用一样对待,应该要有一个合理的版本管理机制,否则生产加载错插件版本,可不是闹着玩的。
参考
《从码农到工匠》