spring-boot 插件化部署(热加载)

1,353 阅读3分钟

我正在参加「掘金·启航计划」

0. 插件化部署

插件化部署:spring-boot 服务启动后,不停止服务,对服务的功能进行模块化的新增、更新;热插拔的形式提供功能集成;

本文内容:插件化部署原理简介和项目demo;

1. 运行原理

  • 使用自定义类加载器加载我们需要的类
  • 使用spring管理自定义类加载器生命周期

核心代码

类加载器:

@Slf4j
public class HotClassLoader extends URLClassLoader {

    /**
     * <pre>
     *     jar 包更新时间记录,避免重复下载
     *     key      : jar 绝对路径
     *     value    : jar 更新时间,0-正在加载,>0 上一次更新时间
     * </pre>
     *
     */
    private static final Map<String,Long> jarUpdateTimeMap;

    private static final Map<String,List<String>> jarClassNameMap;

    static {
        jarUpdateTimeMap = new HashMap<>();
        jarClassNameMap = new HashMap<>();
    }

    private SpringInjectService springInjectService;

    public HotClassLoader(ClassLoader parent) {
        super(new URL[0], parent);
    }

    public HotClassLoader(SpringInjectService springInjectService, ClassLoader parent) {
        super(new URL[0], parent);
        this.springInjectService = springInjectService;
    }

    public void loadJar(String jarPath) {
        Long lastModifyTime = jarUpdateTimeMap.get(jarPath);
        if(Objects.equals(lastModifyTime,0L)){
            log.warn("HotClassLoader.loadJar loading ,please not repeat the operation, jarPath = {}", jarPath);
            return;
        }

        // 1. 将jar 包加载到JVM
        File file = new File(jarPath);
        if (!file.exists()) {
            log.warn("HotClassLoader.loadJar fail file not exist, jarPath = {}", jarPath);
            return;
        }

        long currentJarModifyTime = file.getAbsoluteFile().lastModified();
        if(Objects.equals(lastModifyTime,currentJarModifyTime)){
            log.warn("HotClassLoader.loadJar current version has bean loaded , jarPath = {}", jarPath);
            return;
        }

        try {
            super.addURL(file.toURI().toURL());
        } catch (MalformedURLException e) {
            throw new PluginException("通过url 添加 jar 是失败");
        }

        // 记录jar 加载时间
        jarUpdateTimeMap.put(jarPath, 0L);

        List<String> classNameList = new ArrayList<>();
        // 2. 遍历jar 包中的类
        try (JarFile jarFile = new JarFile(jarPath)) {
            List<JarEntry> jarEntryList = jarFile.stream().sequential().collect(Collectors.toList());

            for (JarEntry loopJar : jarEntryList) {
                // 2.1 将class 类加载到JVM
                String jarName = loopJar.getName();
                if (!jarName.endsWith(".class")) {
                    continue;
                }
                String className = jarName.replace(".class", "").replace("/", ".");

                // 2.2 将class 注册到spring容器
                boolean beanExist = springInjectService.containsBean(className);
                if(beanExist){
                    springInjectService.removeBean(className);
                }

                Class<?> clazz = loadClass(className, false);

                springInjectService.registerBean(className, clazz);
                classNameList.add(className);
            }
        } catch (IOException | ClassNotFoundException e) {
            throw new PluginException("jar包解析失败");
        }

        // 记录jar包中的 class 文件相对路径
        jarClassNameMap.put(jarPath, classNameList);

        // 记录jar 文件的更新时间
        jarUpdateTimeMap.put(jarPath, currentJarModifyTime);
    }

    public void unloadJar(String jarPath) {
        // 1. 校验文件是否存在
        File file = new File(jarPath);
        if (!file.exists()) {
            log.warn("HotClassLoader.loadJar fail file not exist, jarPath = {}", jarPath);
            return;
        }
        List<String> classNameList = jarClassNameMap.get(jarPath);
        if(CollectionUtils.isEmpty(classNameList)){
            log.warn("HotClassLoader.loadJar fail,the jar no class, jarPath = {}", jarPath);
            return;
        }
        // 2.1 遍历移除spring中对应的bean, 关闭类加载器,移除引用
        for (String loopClassName : classNameList) {
            boolean beanExist = springInjectService.containsBean(loopClassName);
            if(beanExist){
                springInjectService.removeBean(loopClassName);
            }
        }
        // 2.2 关闭 classloader
        try {
            close();
        } catch (IOException e) {
            throw new PluginException("HotClassLoader 加载失败");
        }
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        if(name.startsWith("java.")){
            return ClassLoader.getSystemClassLoader().loadClass(name);
        }

        Class<?> clazz = findLoadedClass(name);
        if (clazz != null) {
            if (resolve) {
                return loadClass(name);
            }
            return clazz;
        }
        return super.loadClass(name, resolve);
    }

}

spring管理帮助服务

@Slf4j
@Component
public class SpringInjectService implements ApplicationContextAware {
    private DefaultListableBeanFactory defaultListableBeanFactory;

    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
        this.defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();
    }

    /**
     * 注册bean到spring容器中
     *
     * @param beanName 名称
     * @param clazz    class
     */
    public void registerBean(String beanName, Class<?> clazz) {
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
        AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
        beanDefinitionBuilder.setScope(ConfigurableBeanFactory.SCOPE_SINGLETON);

        // 注册bean
        defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinition);
    }

    public <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

    public  <T> T getBean(String name, Class<T> clazz) {
        return applicationContext.getBean(name, clazz);
    }

    public boolean containsBean(String name) {
        return applicationContext.containsBean(name);
    }

    public void removeBean(String name){
        defaultListableBeanFactory.removeBeanDefinition(name);
    }

}

spring配置类

@Configuration
public class HotClassLoaderAutoConfigure {

    @Bean
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public HotClassLoader hotClassLoader(SpringInjectService springInjectService) {
       return new HotClassLoader(springInjectService,this.getClass().getClassLoader());
    }

}

2.项目介绍

简单来讲,如下:

  • 1)spring-boot 项目发布成功后;若有新的功能需要添加,直接将新功能打包成jar,放到指定目录,
  • 2)然后加载,即可完成新功能的添加;

demo如何运行

  • 1)查看:husky-use 项目查看,只有接口没有实现类;
  • 2)我们先启动项目,访问:http://localhost:8080/eat ,发现如下异常;
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'com.xiaoyuxx.intf.impl.EatPluginImpl' available
   at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:874) ~[spring-beans-5.3.22.jar:5.3.22]
   at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1344) ~[spring-beans-5.3.22.jar:5.3.22]
  • 3)将 husky-eat-plugin打包成jar
# 运行,得到:husky-eat-plugin-1.0-SNAPSHOT.jar
mvn clean package,
load success
you eat wtf morning tea  
  • 7)测试插件卸载功能
修改husky-eat-plugin eat 方法返回值为:morning you eat wtf tea
morning you eat wtf tea