SpringBoot实现动态加载外部Jar流程详解

 更新时间:2023年05月20日 11:48:08   作者:加班狂魔  
这篇文章主要介绍了SpringBoot动态加载外部Jar的流程,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧

背景及实现思路

想要设计一个stater,可以方便加载一个可以完整运行的springboot单体jar包,为了在已执行的服务上面快速的扩展功能而不需要重启整个服务,又或者低代码平台生成代码之后可以快速预览。

加载jar的技术栈

  • springboot 2.2.6.RELEASE
  • mybatis-plus 3.4.1

实现加载

想要完成类加载要熟悉spring中类加载机制,以及java中classloader的双亲委派机制。

加载分为两大步

第一步需要将对应的jar中的class文件加载进当前运行内存中,第二步则是将对应的bean注册到spring,交由spring管理。

load class

load class主要使用jdk中URLClassLoader工具类,但是这里要注意一点,构建classloader时,构造函数可以指定父类加载器,如果指定之后,java才会将两个classloader加载的同一个class视作类型一致,如果不指定会出现 com.demo.A can not cast to com.demo.A这样的情况。

但是我这里依旧没有指定父类加载器,原因如下:

  • 我要加载的jar都是可以独立运行的,没有必须要依赖别的工程的文件
  • 我需要可以卸载掉,如果制定了父类加载器,那么会到这这个classloader不能回收,那么该加载器就一直在内存中。

加载jar的代码

/**
     * 加载jar包
     *
     * @param jarPath     jar路径
     * @param packageName 扫面代码的路径
     * @return
     */
    public boolean loadJar(String jarPath, String packageName) {
        try {
            File file = FileUtil.file(jarPath);
            URLClassLoader classloader = new URLClassLoader(new URL[]{file.toURI().toURL()}, this.applicationContext.getClassLoader());
            JarFile jarFile = new JarFile(file);
            // 获取jar包下所有的classes
            String pkgPath = packageName.replace(".", "/");
            Enumeration<JarEntry> entries = jarFile.entries();
            Class<?> clazz = null;
            List<JarEntry> xmlJarEntry = new ArrayList<>();
            List<String> loadedAliasClasses = new ArrayList<>();
            List<String> otherClasses = new ArrayList<>();
            // 首先加载model
            while (entries.hasMoreElements()) {
                JarEntry jarEntry = entries.nextElement();
                String entryName = jarEntry.getName();
                if (entryName.charAt(0) == '/') {
                    entryName = entryName.substring(1);
                }
                if (entryName.endsWith("Mapper.xml")) {
                    xmlJarEntry.add(jarEntry);
                } else {
                    if (jarEntry.isDirectory() || !entryName.contains(pkgPath) || !entryName.endsWith(".class")) {
                        continue;
                    }
                    String className = entryName.substring(0, entryName.length() - 6);
                    otherClasses.add(className.replace("/", "."));
                    log.info("load class : " + className.replace("/", "."));
                    // 将变量首字母置小写
                    String beanName = StringUtils.uncapitalize(className);
                    if (beanName.contains(LoaderConstant.MODEL)) {
                        // 加载所有的class
                        clazz = classloader.loadClass(className.replace("/", "."));
                        SqlSessionFactory sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
                        sqlSessionFactory.getConfiguration().getTypeAliasRegistry().registerAlias(beanName.replace("/", "."), clazz);
                        loadedAliasClasses.add(beanName.replace("/", ".").toLowerCase());
                        doMap.put(className.replace("/", "."), clazz);
                    }
                }
            }
            // 再加载其他class
            for (String otherClass : otherClasses) {
                // 加载所有的class
                clazz = classloader.loadClass(otherClass.replace("/", "."));
                log.info("load class : " + otherClass.replace("/", "."));
                // 将变量首字母置小写
                String beanName = StringUtils.uncapitalize(otherClass);
                if (beanName.endsWith(LoaderConstant.MAPPER)) {
                    mapperMap.put(beanName, clazz);
                } else if (beanName.endsWith(LoaderConstant.CONTROLLER)) {
                    controllerMap.put(beanName, clazz);
                } else if (beanName.endsWith(LoaderConstant.SERVICE_IMPL)) {
                    serviceImplMap.put(beanName, clazz);
                } else if (beanName.endsWith(LoaderConstant.SERVICE)) {
                    serviceMap.put(beanName, clazz);
                }
            }
            // 加载所有XML
            for (JarEntry jarEntry : xmlJarEntry) {
                SqlSessionFactory sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
                mybatisXMLLoader.xmlReload(sqlSessionFactory, jarFile, jarEntry, jarEntry.getName());
            }
            Jar jar = new Jar();
            jar.setName(jarPath);
            jar.setJarFile(jarFile);
            jar.setLoader(classloader);
            jar.setLoadedAliasClasses(loadedAliasClasses);
            // 开始加载bean
            registerBean(jar);
            registry.registerJar(jarPath, jar);
        } catch (Exception e) {
            log.error(e.getLocalizedMessage());
            return false;
        }
        return true;
    }

通常bean注册过程

想要实现热加载,一定得了解在spring中类的加载机制,大体上spring在扫描到@Component注解的类时,会根据其class生成对应的BeanDefinition,然后在将其注册在BeanDefinitionRegistry(这是个接口,最终由DefaultListableBeanFactory实现)。当其备引用注入实例时即getBean时被实例化并被注册到DefaultSingletonBeanRegistry中。后续单例都将由DefaultSingletonBeanRegistry所管理。

controller加载

controller的加载机制

controller所特殊的是,spring会将其注册到RequestMappingHandlerMapping中。所以想要热加载controller 就需要三步。

  • 生成并注册BeanDefinition
  • 生成并注册实例注册
  • RequestMappingHandlerMapping

代码如下

// 获取bean工厂并转换为DefaultListableBeanFactory
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) ((ConfigurableApplicationContext)
                applicationContext).getBeanFactory();
        // 定义BeanDefinition
        BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
        GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionBuilder.getRawBeanDefinition();
        //设置当前bean定义对象是单利的
        beanDefinition.setScope("singleton");
        // 将变量首字母置小写
        beanName = StringUtils.uncapitalize(beanName);
        // 将构建的BeanDefinition交由Spring管理
        beanFactory.registerBeanDefinition(beanName, beanDefinition);
        // 手动构建实例,并注入base service 防止卸载之后不再生成
        Object obj = clazz.newInstance();
        beanFactory.registerSingleton(beanName, obj);
        log.info("register Singleton :" + beanName);
        final RequestMappingHandlerMapping requestMappingHandlerMapping =
                    applicationContext.getBean(RequestMappingHandlerMapping.class);
        if (requestMappingHandlerMapping != null) {
                String handler = beanName;
                Object controller = null;
                try {
                    controller = applicationContext.getBean(handler);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                if (controller == null) {
                    return beanName;
                }
                // 注册Controller
                Method method = requestMappingHandlerMapping.getClass().getSuperclass().getSuperclass().
                        getDeclaredMethod("detectHandlerMethods", Object.class);
                // 将private改为可使用
                method.setAccessible(true);
                method.invoke(requestMappingHandlerMapping, handler);
        }

关于IOC

其实只要注册BeanDefinition之后,你getBean的时候spring会自动帮你完成@Autowired @Resouce 以及构造方法的注入,这里我自己完成实例化是想完成一些业务上的处理,如自定义注入一些代理类。

关于AOP

这样写有一个弊端就是无法使用AOP,因为AOP是在getBean的时候三层缓存中完成代理的生成的,这里如果你要用这种方式注入可以参考spring源码,构建出来代理类再注入

service加载

service加载我这里直接将service对应的实现类实例化再加载进去就可以了,不需要什么特殊的处理,所以这里就不贴代码了,加载同controller的第一步

mapper加载

mapper的加载时最复杂的一部分,首先针mapper有两种,一种是纯Mapper接口文件的加载,一种是xml文件的加载。并且你需要分析本身Mybatis是如何加载的,这样才能完整的降mapper加载到内存中。这里我将步骤分解为以下几步

  • 注册别名(主要是为了XML使用)
  • 解析XML文件
  • 解析Mapper接口,注册mapper并注册

注册别名

mybatis对于别名的管理是存在SqlSessionFactory的Configuration(这个对象很重要,mybatis加载的资源之类的都在这个对象中管理)对象的TypeAliasRegistry中。TypeAliasRegistry是使用HashMap来维护别名的,这里我们直接调用registerAliases方法就好

SqlSessionFactory sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
sqlSessionFactory.getConfiguration().getTypeAliasRegistry().registerAlias(beanName.replace("/", "."), clazz);

解析XML文件

解析XML文件其实比较简单只要调用XMLMapperBuilder来解析就好了,XMLMapperBuilder.parse方法会解析XML文件并注册resultMaps、sqlFragments、mappedStatements。但是这里需要注意一点,那就是你解析的时候需要判断一下把之前加载的数据需要删除掉,同理resultMaps、sqlFragments、mappedStatements这些数据都是在SqlSessionFactory的Configuration中维护的,我们只要通过反射取得这些对象然后修改就可以了,代码如下

/**
     * 解析加载XML
     *
     * @param sqlSessionFactory
     * @param jarFile jar对象
     * @param jarEntry jar包中的XML对象
     * @param name XML名称
     * @throws IOException
     * @throws NoSuchFieldException
     * @throws IllegalAccessException
     */
    public void xmlReload(SqlSessionFactory sqlSessionFactory, JarFile jarFile, JarEntry jarEntry, String name) throws IOException, NoSuchFieldException, IllegalAccessException {
        // 2. 取得Configuration
        Configuration targetConfiguration = sqlSessionFactory.getConfiguration();
        Class<?> aClass = targetConfiguration.getClass();
        if (targetConfiguration.getClass().getSimpleName().equals("MybatisConfiguration")) {
            aClass = Configuration.class;
        }
        Set<String> loadedResources = (Set<String>) ObjectUtil.getFieldValue(targetConfiguration, aClass, "loadedResources");
        loadedResources.remove(name);
        // 3. 去掉之前加载的数据
        Map<String, ResultMap> resultMaps = (Map<String, ResultMap>) ObjectUtil.getFieldValue(targetConfiguration, aClass, "resultMaps");
        Map<String, XNode> sqlFragmentsMaps = (Map<String, XNode>) ObjectUtil.getFieldValue(targetConfiguration, aClass, "sqlFragments");
        Map<String, MappedStatement> mappedStatementMaps = (Map<String, MappedStatement>) ObjectUtil.getFieldValue(targetConfiguration, aClass, "mappedStatements");
        XPathParser parser = new XPathParser(jarFile.getInputStream(jarEntry), true, targetConfiguration.getVariables(), new XMLMapperEntityResolver());
        XNode mapperXNode = parser.evalNode("/mapper");
        List<XNode> resultMapNodes = mapperXNode.evalNodes("/mapper/resultMap");
        String namespace = mapperXNode.getStringAttribute("namespace");
        for (XNode xNode : resultMapNodes) {
            String id = xNode.getStringAttribute("id", xNode.getValueBasedIdentifier());
            resultMaps.remove(namespace + "." + id);
        }
        List<XNode> sqlNodes = mapperXNode.evalNodes("/mapper/sql");
        for (XNode sqlNode : sqlNodes) {
            String id = sqlNode.getStringAttribute("id", sqlNode.getValueBasedIdentifier());
            sqlFragmentsMaps.remove(namespace + "." + id);
        }
        List<XNode> msNodes = mapperXNode.evalNodes("select|insert|update|delete");
        for (XNode msNode : msNodes) {
            String id = msNode.getStringAttribute("id", msNode.getValueBasedIdentifier());
            mappedStatementMaps.remove(namespace + "." + id);
        }
        try {
            // 4. 重新加载和解析被修改的 xml 文件
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(jarFile.getInputStream(jarEntry),
                    targetConfiguration, name, targetConfiguration.getSqlFragments());
            xmlMapperBuilder.parse();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        log.info("Parsed mapper file: '" + name + "'");
    }

其他类记载

其他类加载就比较简单了,直接使用classloader将这些类load进去就好,如果是单例需要被spring管理的则registerBeanDefinition就可以了

到此这篇关于SpringBoot实现动态加载外部Jar流程详解的文章就介绍到这了,更多相关SpringBoot动态加载外部Jar内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java实现产生随机字符串主键的UUID工具类

    Java实现产生随机字符串主键的UUID工具类

    这篇文章主要介绍了Java实现产生随机字符串主键的UUID工具类,涉及java随机数与字符串遍历、转换等相关操作技巧,需要的朋友可以参考下
    2017-10-10
  • MyBatis Generator配置入门

    MyBatis Generator配置入门

    本文主要介绍了MyBatis Generator配置入门,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-07-07
  • Java的布隆过滤器你了解吗

    Java的布隆过滤器你了解吗

    这篇文章主要为大家详细介绍了Java的布隆过滤器,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下,希望能够给你带来帮助
    2022-03-03
  • java使用EasyExcel实现合并单元格

    java使用EasyExcel实现合并单元格

    这篇文章主要为大家详细介绍了java使用EasyExcel实现合并单元格的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2023-12-12
  • 浅谈java是如何做资源回收补救的

    浅谈java是如何做资源回收补救的

    这篇文章主要介绍了浅谈java是如何做资源回收补救的,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-06-06
  • Java输出多位小数的三种方法(附代码)

    Java输出多位小数的三种方法(附代码)

    这篇文章主要给大家介绍了关于Java输出多位小数的三种方法的相关资料,在实际工作中常常需要设定数字的输出格式,如以百分比的形式输出,或者设定小数位数等,需要的朋友可以参考下
    2023-07-07
  • java识别一篇文章中某单词出现个数的方法

    java识别一篇文章中某单词出现个数的方法

    这篇文章主要介绍了java识别一篇文章中某单词出现个数的方法,涉及java字符解析操作的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-10-10
  • Java利用InputStream类实现文件读取与处理

    Java利用InputStream类实现文件读取与处理

    在Java开发中,输入流(InputStream)是一个非常重要的概念,它涉及到文件读写、网络传输等多个方面,InputStream类是Java中输入流的抽象基类,定义了读取输入流数据的方法,本文将以InputStream类为切入点,介绍Java中的输入流概念及其应用,需要的朋友可以参考下
    2023-11-11
  • springboot结合ehcache防止恶意刷新请求的实现

    springboot结合ehcache防止恶意刷新请求的实现

    这篇文章主要介绍了springboot结合ehcache防止恶意刷新请求的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-12-12
  • java BigDecimal精度丢失及常见问分析

    java BigDecimal精度丢失及常见问分析

    这篇文章主要为大家介绍了java BigDecimal精度丢失及常见问分析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-02-02

最新评论