Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
yuhangc525 opened this issue Sep 18, 2024 · 3 comments
Open

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

yuhangc525 opened this issue Sep 18, 2024 · 3 comments

Comments

@yuhangc525
Copy link

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.

@yuhangc525
Copy link
Author

image
This is the reference situation of the class loader in the mat analysis.

@decebals
Copy link
Member

I found that the class loader of the plugin could not be closed correctly, and no error message was output.

According the code source the plugin class loader is closed automatically, no need to do it explicitly in your code.

It's somehow normal that the plugin class loader exists in memory, it's not collected by garbage collector. The idea is the the unloaded plugin is invalidated, it cannot loads class in the memory of app and the plugin (and the associated class loader) is not references anymore in pf4j.

What is your problem after all, what is not working?
I feel that the problem is somehow related to spring, because after unload (stop) the extension bean added by plugin continues to be available in spring. Try to investigate and add a very simple simple quickstart app that simulate the problem. Try to use the latest pf4j version (the latest pf4j-spring doesn't use the latest pf4j version).

@yuhangc525
Copy link
Author

First of all, thank you for your reply.My problem is this: when I uninstall a plugin, I want to clean up all the resources of the plugin. I can now clean up beans, etc. The only thing I can't do is to close the class loader of the plugin. I am worried that when I frequently load and uninstall plugins, the class loader cannot be recycled, causing memory leaks. As shown in the picture below, when I uninstall a plugin, I want to clean up all the resources related to the plugin. Unfortunately, a lot of space is not recycled.
image
As for what you said about using the latest version of pf4j, I'll try and see if that fixes the issue.
Thanks again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants