Skip to content

How to dynamically load and unload plugins using pf4j-spring? #84

Description

@yuhangc525

Hello!
I now want to dynamically load and unload plugins so that the main project can access the beans and APIs in the plugins.
My loading code is as follows:

public void registerPlugin(Path pluginPath) {
        SpringPlugin plugin = null;
        String pluginId = null;
        try {
            pluginId = pluginManager.loadPlugin(pluginPath);
            pluginManager.startPlugin(pluginId);
            plugin = (SpringPlugin) pluginManager.getPlugin(pluginId).getPlugin();
            ApplicationContext pluginApplicationContext = plugin.getApplicationContext();
            String[] beanNames = pluginApplicationContext.getBeanDefinitionNames();
            for (String beanName : beanNames) {
                Object bean = pluginApplicationContext.getBean(beanName);
                if (bean.getClass().isAnnotationPresent(Controller.class) || bean.getClass().isAnnotationPresent(RestController.class)) {
                    registerController(bean);
                }
            }
        } catch (Exception e) {
            //加载插件失败要将所有的bea和controller注销
            unloadPlugin(pluginId,false);
            log.error("插件动态加载失败:", e);
        }
    }
    public void registerController(Object bean) {
        RequestMappingHandlerMapping mappingHandlerMapping = mainApplicationContext.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
        RequestMappingInfo classMappingInfo = getClassMappingInfo(bean);
        for (Method method : bean.getClass().getMethods()) {
            if (!isHandlerMethod(method)) {
                continue;
            }
            RequestMappingInfo mappingInfo = getMethodMappingInfo(method);
            // 合并类级别的路径前缀与方法级别的 RequestMappingInfo
            RequestMappingInfo finalMappingInfo = (classMappingInfo != null && mappingInfo != null)
                    ? classMappingInfo.combine(mappingInfo)
                    : mappingInfo;
            if (finalMappingInfo != null) {
                mappingHandlerMapping.registerMapping(finalMappingInfo, bean, method);

                log.info("方法 {} 成功注册, 注册路径:{}", method.getName(), finalMappingInfo.getPatternsCondition());
            }
        }
    }
private RequestMappingInfo getMethodMappingInfo(Method method) {
        RequestMappingInfo mappingInfo = null;
        if (AnnotationUtils.findAnnotation(method, GetMapping.class) != null) {
            GetMapping getMapping = AnnotationUtils.findAnnotation(method, GetMapping.class);
            mappingInfo = RequestMappingInfo.paths(getMapping.path())
                    .methods(RequestMethod.GET)
                    .produces(getMapping.produces())
                    .consumes(getMapping.consumes())
                    .headers(getMapping.headers())
                    .params(getMapping.params())
                    .build();
        } else if (AnnotationUtils.findAnnotation(method, PostMapping.class) != null) {
            PostMapping postMapping = AnnotationUtils.findAnnotation(method, PostMapping.class);
            mappingInfo = RequestMappingInfo.paths(postMapping.path())
                    .methods(RequestMethod.POST)
                    .produces(postMapping.produces())
                    .consumes(postMapping.consumes())
                    .headers(postMapping.headers())
                    .params(postMapping.params())
                    .build();
        } else if (AnnotationUtils.findAnnotation(method, PutMapping.class) != null) {
            PutMapping putMapping = AnnotationUtils.findAnnotation(method, PutMapping.class);
            mappingInfo = RequestMappingInfo.paths(putMapping.path())
                    .methods(RequestMethod.PUT)
                    .produces(putMapping.produces())
                    .consumes(putMapping.consumes())
                    .headers(putMapping.headers())
                    .params(putMapping.params())
                    .build();
        } else if (AnnotationUtils.findAnnotation(method, DeleteMapping.class) != null) {
            DeleteMapping deleteMapping = AnnotationUtils.findAnnotation(method, DeleteMapping.class);
            mappingInfo = RequestMappingInfo.paths(deleteMapping.path())
                    .methods(RequestMethod.DELETE)
                    .produces(deleteMapping.produces())
                    .consumes(deleteMapping.consumes())
                    .headers(deleteMapping.headers())
                    .params(deleteMapping.params())
                    .build();
        }
        return mappingInfo;
    }

    /**
     * 判断一个方法是否为处理器方法
     *
     * @param method
     * @return
     */
    private boolean isHandlerMethod(Method method) {
        return AnnotationUtils.findAnnotation(method, RequestMapping.class) != null ||
                AnnotationUtils.findAnnotation(method, GetMapping.class) != null ||
                AnnotationUtils.findAnnotation(method, PostMapping.class) != null ||
                AnnotationUtils.findAnnotation(method, PutMapping.class) != null ||
                AnnotationUtils.findAnnotation(method, DeleteMapping.class) != null;
    }

My unloading code is as follows:

    public void unloadPlugin(String pluginId, boolean index) {
        SpringPlugin plugin = (SpringPlugin) pluginManager.getPlugin(pluginId).getPlugin();
        ApplicationContext pluginApplicationContext = plugin.getApplicationContext();
        String[] beanDefinitionNames = pluginApplicationContext.getBeanDefinitionNames();
        RequestMappingHandlerMapping mappingHandlerMapping = mainApplicationContext.getBean("requestMappingHandlerMapping", RequestMappingHandlerMapping.class);
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) mainApplicationContext.getAutowireCapableBeanFactory();
        for (String beanName : beanDefinitionNames) {
            Object bean = pluginApplicationContext.getBean(beanName);
            if (mainApplicationContext.containsBean(beanName)){
                beanFactory.destroyBean(bean);
                    beanFactory.destroySingleton(beanName);// 这种方式能清除掉单例缓存
                    beanFactory.removeBeanDefinition(beanName);
            }
            if (bean.getClass().isAnnotationPresent(Controller.class) || bean.getClass().isAnnotationPresent(RestController.class)) {
                unRegisterMethods(bean, mappingHandlerMapping);
                log.info("插件【{}】中的 RestController {} 已从 RequestMappingHandlerMapping 中移除", pluginId, beanName);
            }
        }
        plugin.stop();
        closeClassLoader(plugin, pluginId);
        pluginManager.deletePlugin(pluginId);
        applicationEventPublisher.publishEvent(new PluginLoadedEvent(pluginId));
    }
    private void closeClassLoader(SpringPlugin plugin, String pluginId) {
        Thread.currentThread().setContextClassLoader(pluginManager.getClass().getClassLoader());
        ClassLoader pluginClassLoader = plugin.getWrapper().getPluginClassLoader();
        if (pluginClassLoader instanceof URLClassLoader) {
            try {
                ((URLClassLoader) pluginClassLoader).close();
            } catch (IOException e) {
                log.error("插件【{}】的类加载器关闭失败!", pluginId, e);
            }
        }
    }

After using the above code, I can access the interface defined in the bean and controller in the plugin, but when uninstalling, I found that the class loader of the plugin could not be closed correctly, and no error message was output.
I want to know what is the correct way to dynamically load and unload, is there something I have not considered, and I have not found a solution in the official documentation. I really hope you can help me. Thank you very much.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions