Plugin - 插件开发03_Spring Boot动态插件化与热加载

文章目录


Pre

插件 - 通过SPI方式实现插件管理

插件 - 一份配置,离插件机制只有一步之遥

插件 - 插件机制触手可及

Plugin - 插件开发01_SPI的基本使用

Plugin - 插件开发02_使用反射机制和自定义配置实现插件化开发

Plugin - 插件开发03_Spring Boot动态插件化与热加载

Plugin - 插件开发04_Spring Boot中的SPI机制与Spring Factories实现


方案概览

常用的插件化实现思路:

  1. spi机制
  2. spring内置扩展点
  3. spring aop技术
  4. springboot中的Factories机制
  5. 第三方插件包:spring-plugin
  6. 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应用中实现灵活的插件机制。这样的插件化架构不仅能够提升系统的可扩展性,还能有效地支持插件的动态加载和卸载,为系统提供更好的功能扩展能力

相关推荐
悟空码字1 天前
Spring Boot 整合 MongoDB 最佳实践:CRUD、分页、事务、索引全覆盖
java·spring boot·后端
皮皮林5513 天前
拒绝写重复代码,试试这套开源的 SpringBoot 组件,效率翻倍~
java·spring boot
用户908324602735 天前
Spring AI 1.1.2 + Neo4j:用知识图谱增强 RAG 检索(上篇:图谱构建)
java·spring boot
用户8307196840826 天前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解6 天前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解6 天前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记6 天前
Spring Boot Web MVC配置详解
spring boot·后端
初次攀爬者7 天前
Kafka 基础介绍
spring boot·kafka·消息队列
用户8307196840827 天前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
Java水解7 天前
SpringBoot3全栈开发实战:从入门到精通的完整指南
spring boot·后端