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内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • IDEA 配合 Dockerfile 部署 SpringBoot 工程的注意事项

    IDEA 配合 Dockerfile 部署 SpringBoot 工程的注意事项

    这篇文章主要介绍了IDEA 配合 Dockerfile 部署 SpringBoot 工程,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-09-09
  • maven快速生成SpringBoot打包文件的方法步骤

    maven快速生成SpringBoot打包文件的方法步骤

    本文主要介绍了使用Maven快速生成SpringBoot项目打包文件的方法,包括如何生成可执行的JAR文件,如何将配置文件、运行脚本、调试脚本、证书文件等拷贝到指定目录,及如何编译出部署包,这种方法能大大方便微服务的部署,提高部署效率
    2024-10-10
  • 详解Java单元测试之JUnit篇

    详解Java单元测试之JUnit篇

    这篇文章主要介绍了详解Java单元测试之JUnit篇,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • MyBatis存储过程、MyBatis分页、MyBatis一对多增删改查操作

    MyBatis存储过程、MyBatis分页、MyBatis一对多增删改查操作

    本文通过一段代码给大家介绍了MyBatis存储过程、MyBatis分页、MyBatis一对多增删改查操作,非常不错,具有参考借鉴价值,感兴趣的朋友一起看看吧
    2016-11-11
  • SpringBoot整合FTP实现文件传输的步骤

    SpringBoot整合FTP实现文件传输的步骤

    这篇文章主要给大家介绍了SpringBoot整合FTP实现文件传输的步骤,文中的流程步骤和代码示例介绍的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
    2023-11-11
  • Java使用utf8格式保存文本文件的方法

    Java使用utf8格式保存文本文件的方法

    这篇文章主要介绍了Java使用utf8格式保存文本文件的方法,涉及Java针对字符流编码操作的相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下
    2015-11-11
  • java制作简单的坦克大战

    java制作简单的坦克大战

    坦克大战是我们小时候玩红白机时代的经典游戏,看到有不少小伙伴都使用各种语言实现了一下,手痒痒,也使用java做的一个比较简单的坦克大战,主要面向于学过Java的人群,与学了一段时间的人,有利于面向对象思想的提高,推荐给大家。
    2015-03-03
  • 说说@ModelAttribute在父类和子类中的执行顺序

    说说@ModelAttribute在父类和子类中的执行顺序

    这篇文章主要介绍了@ModelAttribute在父类和子类中的执行顺序,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06
  • Java优雅的处理金钱问题(BigDecimal)

    Java优雅的处理金钱问题(BigDecimal)

    本文主要介绍了Java优雅的处理金钱问题(BigDecimal),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-06-06
  • 如何给HttpServletRequest增加消息头

    如何给HttpServletRequest增加消息头

    这篇文章主要介绍了如何给HttpServletRequest增加消息头的实现方式,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06

最新评论