springboot 动态加载

  • 背景及实现思路
  • 加载jar的技术栈
  • 实现加载
  • load class
  • 通常bean注册过程
  • controller加载
  • controller的加载机制
  • 关于IOC
  • 关于AOP
  • service加载
  • mapper加载
  • 注册别名
  • 解析XML文件
  • 其他类加载


背景及实现思路

想要设计一个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这样的情况。
但是我这里依旧没有指定父类加载器,原因如下:

  1. 我要加载的jar都是可以独立运行的,没有必须要依赖别的工程的文件
  2. 我需要可以卸载掉,如果制定了父类加载器,那么会到这这个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 就需要三步。

  1. 生成并注册BeanDefinition
  2. 生成并注册实例
  3. 注册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加载到内存中。这里我将步骤分解为以下几步

  1. 注册别名(主要是为了XML使用)
  2. 解析XML文件
  3. 解析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就可以了