文章目录
Pre
Plugin - 插件开发02_使用反射机制和自定义配置实现插件化开发
Plugin - 插件开发03_Spring Boot动态插件化与热加载
Plugin - 插件开发04_Spring Boot中的SPI机制与Spring Factories实现
方案概览
常用的插件化实现思路:
- spi机制
- spring内置扩展点
- spring aop技术
- springboot中的Factories机制
- 第三方插件包:spring-plugin
- java agent 技术
使用插件的好处
- 模块解耦:插件机制能帮助系统模块间解耦,使得服务间的交互变得更加灵活。
- 提升扩展性和开放性:通过插件机制,系统能够轻松扩展,支持新的功能或服务,无需大规模修改核心代码。
- 方便第三方接入:第三方可以按照预定义的插件接口进行集成,不会对主系统造成过多侵入。
流程
定义接口 定义接口 定义接口 打包SDK 打包SDK 打包SDK 读取配置 指定实现类 Bean实例化 调用接口方法 返回结果 应用 A: 定义接口 应用 B: 实现接口并打包成SDK 应用 C: 实现接口并打包成SDK 应用 D: 实现接口并打包成SDK 应用 E: 引用SDK并动态加载实现类 配置文件: 配置项 启动时注册Bean/运行时注册Bean 实例化实现类 调用 接口方法
Code
要实现一个插件化系统,以便动态地引入外部插件。这些插件可能包括功能增强、第三方集成或业务逻辑的拓展。
定义插件接口。
实现插件机制的关键步骤
- 插件实现类。
- 通过Spring的 ImportBeanDefinitionRegistrar 动态注册插件。
使用自定义类加载器加载外部JAR包中的类。
在Spring应用中动态加载、更新和删除插件。
Plugin 定义
首先,我们需要定义一个插件接口,该接口为插件提供统一的方法。插件实现类将根据此接口进行开发。
java
package com.plugin;
public interface IPlugin {
String customPluginMethod(String name);
}
接口 IPlugin 包含一个方法 customPluginMethod,插件类可以通过实现该接口来定义具体的插件行为。
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SpringBootPluginTest</artifactId>
<groupId>com.plugin</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>plugin-api</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
</project>
Plugin 实现
插件实现类实现了 IPlugin 接口,提供了插件的具体功能。
java
package com.plugin.impl;
import com.plugin.IPlugin;
public class MyPluginImpl implements IPlugin {
@Override
public String customPluginMethod(String name) {
return "MyPluginImpl-customPluginMethod executed - " + name;
}
}
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.plugin</groupId>
<artifactId>SpringBootPluginTest</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<groupId>com.plugin</groupId>
<artifactId>plugin-impl</artifactId>
<name>plugin-impl</name>
<description>插件实现类</description>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>com.plugin</groupId>
<artifactId>plugin-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<finalName>${project.artifactId}-${project.version}-jar-with-dependencies</finalName>
<appendAssemblyId>false</appendAssemblyId>
<attach>false</attach>
<archive>
<manifest>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
我们将接口实现打包为jar包, 放到业务使用方配置的目录下。
Plugin 使用方
动态加载插件
在Spring Boot中,我们可以通过自定义 ImportBeanDefinitionRegistrar
来实现插件的动态加载。在插件模块启动时,Spring Boot会自动加载并注册插件类
java
package com.plugin.config;
import com.plugin.utils.ClassLoaderHelper;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotationMetadata;
/**
* 启动时注册bean
*/
@Slf4j
public class PluginImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
/**
* jar的存放路径
*/
private String targetUrl;
/**
* 插件类全路径
*/
private String pluginClass;
/**
* 注册Bean定义到Spring容器中
*
* @param importingClassMetadata 导入类的元数据,通常用于获取注解信息等
* @param registry Bean定义的注册表,用于注册Bean定义
*/
@SneakyThrows
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 获取自定义类加载器,用于加载插件类
ClassLoader classLoader = ClassLoaderHelper.getClassLoader(targetUrl);
// 加载插件类
Class<?> clazz = classLoader.loadClass(pluginClass);
// 创建Bean定义构建器
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
// 获取Bean定义
BeanDefinition beanDefinition = builder.getBeanDefinition();
// 在注册表中注册Bean定义
registry.registerBeanDefinition(clazz.getName(), beanDefinition);
// 日志记录注册成功信息
log.info("plugin register bean [{}],Class [{}] success.", clazz.getName(), clazz);
}
/**
* 设置环境属性
*
* @param environment 环境对象,用于获取环境属性
*/
@Override
public void setEnvironment(Environment environment) {
// 从环境对象中获取目标URL属性
this.targetUrl = environment.getProperty("targetUrl");
// 从环境对象中获取插件类属性
this.pluginClass = environment.getProperty("pluginClass");
}
}
在 registerBeanDefinitions 方法中,使用自定义的类加载器 ClassLoaderHelper 动态加载插件类,并将其注册为Spring容器中的Bean。
类加载器
java
package com.plugin.utils;
import lombok.extern.slf4j.Slf4j;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
/**
* 类加载器工具类
*/
@Slf4j
public class ClassLoaderHelper {
/**
* 根据给定的URL获取一个ClassLoader实例
* 此方法旨在动态加载指定位置的类资源,通过反射手段确保URLClassLoader的addURL方法可访问
*
* @param url 类资源的URL地址,指示ClassLoader要加载的类的位置
* @return URLClassLoader的实例,用于加载指定URL路径下的类文件如果无法创建或访问ClassLoader,则返回null
*/
public static ClassLoader getClassLoader(String url) {
try {
// 获取URLClassLoader的addURL方法,该方法允许向URLClassLoader添加新的URL
Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
// 如果方法不可访问,则设置其为可访问,因为addURL方法是受保护的,需要这样做才能调用
if (!method.isAccessible()) {
method.setAccessible(true);
}
// 创建一个新的URLClassLoader实例,初始URL为空数组,使用ClassLoaderUtil类的ClassLoader作为父ClassLoader
URLClassLoader classLoader = new URLClassLoader(new URL[]{}, ClassLoaderHelper.class.getClassLoader());
// 在创建 URLClassLoader 时,指定当前系统的 ClassLoader 为父类加载器 ClassLoader.getSystemClassLoader() 这步比较关键,用于打通主程序与插件之间的 ClassLoader ,解决把插件注册进 IOC 时的各种 ClassNotFoundException 问题
// URLClassLoader classLoader = new URLClassLoader(new URL[]{}, ClassLoader.getSystemClassLoader());
// 调用addURL方法,将指定的URL添加到classLoader中,以便它可以加载该URL路径下的类
method.invoke(classLoader, new URL(url));
// 返回配置好的ClassLoader实例
return classLoader;
} catch (Exception e) {
// 记录错误信息和异常堆栈,当无法通过反射访问或调用addURL方法时,会进入此块
log.error("getClassLoader-error", e);
// 返回null,表示未能成功创建和配置ClassLoader实例
return null;
}
}
}
为了加载外部JAR包中的插件类,需要一个自定义的类加载器。ClassLoaderHelper
类负责通过反射机制调用 addURL 方法,动态加载指定URL路径下的JAR包。
注册与卸载插件
java
package com.plugin.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;
/**
* Spring 工具类
*
*/
@Slf4j
@Component
public class SpringHelper implements ApplicationContextAware {
private DefaultListableBeanFactory defaultListableBeanFactory;
private ApplicationContext applicationContext;
/**
* 设置ApplicationContext环境
*
* 当该类被Spring管理时,Spring会调用此方法将ApplicationContext注入
* 通过重写此方法,我们可以自定义处理ApplicationContext的方式
*
* @param applicationContext Spring的上下文对象,包含所有的Bean定义和配置信息
* @throws BeansException 如果在处理Bean时发生错误
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
// 将传入的ApplicationContext赋值给类的成员变量,以便后续使用
this.applicationContext = applicationContext;
// 将applicationContext转换为ConfigurableApplicationContext,以便获取BeanFactory
ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
// 获取bean工厂并转换为DefaultListableBeanFactory,以便进行更深层次的自定义配置
this.defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();
}
/**
* 注册bean到spring容器中
*
* @param beanName 名称
* @param clazz class
*/
public void registerBean(String beanName, Class<?> clazz) {
// 通过BeanDefinitionBuilder创建bean定义
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
// 注册bean
defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinitionBuilder.getRawBeanDefinition());
log.info("register bean [{}],Class [{}] success.", beanName, clazz);
}
/**
* 根据bean名称移除bean定义
* 此方法检查指定的bean名称是否在BeanFactory中定义如果定义存在,则将其移除
*
* @param beanName 要移除的bean的名称
*/
public void removeBean(String beanName) {
// 检查BeanFactory中是否定义了指定名称的bean
if(defaultListableBeanFactory.containsBeanDefinition(beanName)) {
// 如果bean定义存在,则从BeanFactory中移除该定义
defaultListableBeanFactory.removeBeanDefinition(beanName);
}
// 记录移除bean操作的日志信息
log.info("remove bean [{}] success.", beanName);
}
/**
* 根据bean的名称获取对应的bean实例
* 此方法用于从Spring应用上下文中获取指定名称的bean,便于在需要的地方直接获取bean实例,避免了硬编码
*
* @param name bean的名称,用于唯一标识一个bean
* @return Object 返回指定名称的bean实例,类型为Object,可以根据需要转换为具体的类型
*/
public Object getBean(String name) {
return applicationContext.getBean(name);
}
}
插件一旦加载并注册到Spring IoC容器后,我们可以通过API来操作插件,比如执行插件中的方法,或者动态更新和卸载插件。
java
package com.plugin.controller;
import com.plugin.IPlugin;
import com.plugin.utils.ClassLoaderHelper;
import com.plugin.utils.SpringHelper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@Slf4j
@RestController
public class PluginTestController {
@Autowired(required = false)
private IPlugin IPlugin;
@Resource
private SpringHelper springHelper;
/**
* jar的地址
*/
@Value("${targetUrl}")
private String targetUrl;
/**
* 插件类全路径
*/
@Value("${pluginClass}")
private String pluginClass;
@GetMapping("/test")
public String test() {
return IPlugin.customPluginMethod("test plugin");
}
/**
* 运行时注册bean
* 此方法用于在应用程序运行时动态加载并注册一个bean,
* 然后根据这个bean执行相应操作或返回其信息
*/
@GetMapping("/reload")
public Object reload() throws ClassNotFoundException {
// 使用自定义类加载器获取指定URL的类加载器
ClassLoader classLoader = ClassLoaderHelper.getClassLoader(targetUrl);
// 通过类加载器加载指定名称的类
Class<?> clazz = classLoader.loadClass(pluginClass);
// 在Spring上下文中注册这个类作为一个bean
springHelper.registerBean(clazz.getName(), clazz);
// 从Spring上下文中获取新注册的bean
Object bean = springHelper.getBean(clazz.getName());
// 检查bean是否实现了PluginInterface接口
if (bean instanceof IPlugin) {
// 如果实现了,将此接口赋值给当前的pluginInterface,并调用其sayHello方法
IPlugin plugin = (IPlugin) bean;
this.IPlugin = plugin;
return plugin.customPluginMethod("test reload");
} else {
// 如果没有实现,获取并记录该bean实现的第一个接口的名称,并返回bean的字符串表示
log.info(bean.getClass().getInterfaces()[0].getName());
return bean.toString();
}
}
/**
* 移除bean
* 该方法用于从Spring应用上下文中移除指定的bean
* 它首先通过ClassLoader加载指定的类,然后使用springUtil工具类移除对应的bean
* 最后,它尝试获取并打印被移除的bean的信息,如果bean仍然存在的话
*
* @return 返回被移除bean的类名
* @throws ClassNotFoundException 如果指定的类不存在,则抛出此异常
*/
@GetMapping("/remove")
public Object remove() throws ClassNotFoundException {
// 获取目标URL对应的ClassLoader
ClassLoader classLoader = ClassLoaderHelper.getClassLoader(targetUrl);
// 通过ClassLoader加载插件类
Class<?> clazz = classLoader.loadClass(pluginClass);
// 使用springUtil工具类移除加载的插件类bean
springHelper.removeBean(clazz.getName());
// 清空pluginInterface引用,表示插件接口已被移除
this.IPlugin = null;
// 尝试获取已被移除的插件类bean
// Object bean = springHelper.getBean(clazz.getName());
// 如果bean不为空,打印bean的信息
//if (bean != null) {
// log.info(bean.toString());
// }
// 返回被移除bean的类名
return clazz.getName() + " removed";
}
}
配置文件
在 application.properties 中配置插件路径和插件类的全路径
java
spring.main.allow-bean-definition-overriding=true
targetUrl=file:/D:/plugin-extends/plugin-impl-0.0.1-SNAPSHOT-jar-with-dependencies.jar
pluginClass=com.plugin.impl.MyPluginImpl
启动类
我们在Spring Boot启动类中使用 @Import 注解加载插件配置
java
package com.plugin;
import com.plugin.config.PluginImportBeanDefinitionRegistrar;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
@SpringBootApplication
@Import(PluginImportBeanDefinitionRegistrar.class)
public class SpringBootPluginTestApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootPluginTestApplication.class, args);
}
}
测试验证
启动时动态加载jar:http://127.0.0.1:8080/test
运行时动态加载jar:http://127.0.0.1:8080/reload
运行时动态卸载jar: http://127.0.0.1:8080/remove
小结
通过自定义类加载器、ImportBeanDefinitionRegistrar
和动态Bean管理,我们能够在Spring Boot应用中实现灵活的插件机制。这样的插件化架构不仅能够提升系统的可扩展性,还能有效地支持插件的动态加载和卸载,为系统提供更好的功能扩展能力。