插件管理-热加载

背景

    最近我司在做一个低代码平台的项目,大概就是拖拖拽拽会生成一个表单,表单可能映射到一个数据表,对表单的数据的维护数据会入到这张表里面,但是一般简单的CURD操作可能这种需求就可以满足,但是复杂的,比如这张表的数据入表了我还需要操作其他的表,或者我可能要发一个通知消息或者之类的更复杂的业务,这种后置的业务一般都是复杂且不通用的,我们就希望能够对每个不同的业务表进行这种增强处理,而且线上的话可能存在几百台服务器,我们也不想进行服务的重启就想实现这种功能。然后插件的jar包要能使用业务系统里面的ioc容器内的对象,比如feignclient,resttemplate,jdbctemplate等等。如果业务系统内有插件内要使用的类加载业务系统的,否则加载插件内的(不要打破双亲委派)。

实现

    我们设计了一个插件管理的模块,然后针对表单的curd的操作每个操作又大致分为before,after,afercommit等一些操作位置,在这些位置动态调用插件里面的class类。整个的系统交互流程


执行流程.png

        但是我们知道外部的插件jar被装载之后的话,我们是没发卸载的,比如这个jar更新了,ClassLoader的loadClass方法会先去查询缓存。所以我们会为每个jar自定义一个类加载器,这样每个类之间是隔离的。即使你更新了插件,我们会认为是两个插件,之前的也保留,现在的会用一个新的classloader加载,然后覆盖掉ioc容器内的旧的内容保留最新的对象。
类加载过程.png
       

主要实现代码

  • 加载外部jar
    public static PluginClassLoader loadJar(String jarPath, String[] classNames, ConfigurableApplicationContext applicationContext) {
        try {
            PluginClassLoader pluginClassLoader = new PluginClassLoader(jarPath);
            DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) applicationContext.getBeanFactory();

            for (String className : classNames) {
                // 加载类
                Class obj = pluginClassLoader.loadClass(className);
                String beanName = getBeanNameByClassName(className);
                if (defaultListableBeanFactory.containsBean(beanName)) {
                    log.info("removeBeanDefinition: = {}  ", beanName);
                    defaultListableBeanFactory.removeBeanDefinition(beanName);
                }
                // 注册进入ioc
                BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(obj);
                BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
                beanDefinition.setScope("singleton");
                defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinition);
            }
            return pluginClassLoader;
        } catch (Exception e) {
            log.error("loadJar fail", e);
        }
        return null;
    }
  • 调用执行外部jar
 /**
     * 从 ioc获取对象   执行增强的逻辑
     *
     * @throws Exception
     */
    @Override
    public Object invokeJar(String fileName, String classPath, String method, Class c, Object args) throws Exception {
        InputStream in = null;
        OutputStream out = null;
        try {
            String localName = fileProperties.getUploadDir() + File.separator + fileName;
            File files = new File(localName);
            if (!files.exists()) {
                // 判断本地是否有   下载拉取包
                files.createNewFile();
                out = new FileOutputStream(files);
                // 保存到本地
                in = myMinioFileOperator.getFileBytes(fileName);
                IoUtil.copy(in, out);
                in.close();
                out.close();
                files = new File(localName);
                // 针对启动时更新上传的包
                PluginUtils.loadJar(localName, new String[]{classPath}, applicationContext);
            }
            // 执行
            String name = PluginUtils.getBeanNameByClassName(classPath);

            // 针对刚启动装载
            DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) applicationContext.getBeanFactory();
            if (!defaultListableBeanFactory.containsBean(name)) {
                PluginUtils.loadJar(localName, new String[]{classPath}, applicationContext);
            }


            Object config = applicationContext.getBean(name);
            if (EmptyUtils.isEmpty(args)) {
                return config.getClass().getDeclaredMethod(method).invoke(config);
            } else {
                return config.getClass().getDeclaredMethod(method, c).invoke(config, args);
            }
        } finally {
            IoUtil.close(in);
            IoUtil.close(out);
        }
    }
  • 类加载器定义
@Slf4j
public class PluginClassLoader extends SecureClassLoader {

    private String jarFilePath;

    public PluginClassLoader(String jarFilePath) {
        this.jarFilePath = jarFilePath;
    }

    @Override
    public Class findClass(String name) throws ClassNotFoundException {
        // 从jar 包里面查找类
        //  log.info("findClass = {}    classLoader = {}", name, name.getClass().getClassLoader());
        ByteArrayOutputStream byteArrayOutputStream = null;
        InputStream inputStream = null;
        JarFile jarFile = null;
        try {
            String path = name.replace('.', '/').concat(".class");
            String pluginUrl = "jar:file:" + jarFilePath + "!/" + path;  //  be careful
            URL url = new URL(pluginUrl);
            // InputStream inputStream = url.openStream();
            JarURLConnection jarURLConnection = (JarURLConnection) url.openConnection();
            jarFile = jarURLConnection.getJarFile();
            JarEntry jarEntry = jarURLConnection.getJarEntry();
            inputStream = jarFile.getInputStream(jarEntry);


            byteArrayOutputStream = new ByteArrayOutputStream();
            int b;
            while ((b = inputStream.read()) != -1) {
                byteArrayOutputStream.write(b);
            }
            byte[] data = byteArrayOutputStream.toByteArray();
            Class cs = this.defineClass(name, data, 0, data.length);
            log.info("findClass = {}   classLoader = {}", name, cs.getClassLoader());
            return cs;
        } catch (Exception e) {
            log.error("findClass fail", e);
        } finally {
            IoUtil.close(byteArrayOutputStream);
            IoUtil.close(inputStream);
            IoUtil.close(jarFile);
        }
        return null;
    }

    // 如果本地和包中有相同的类,以jar中为准的话需要重写loadClass方法,指定加载顺序不走双亲委派机制
    @Override
    public Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
        //  log.info("loadClass = {}", name);
        return super.loadClass(name, resolve);
    }

}

你可能感兴趣的:(插件管理-热加载)