A recent requirement has been to enable existing SpringBoot projects to load external JAR packages to add and update interface logic. In line with the thinking of reusability, I haven’t found anything similar on the Internet for a long time. The only thing somewhat similar is spring-loaded. However, according to my online knowledge, this thing has the following disadvantages:

1. Using Java Agent to start, I prefer to use POM-dependent mode directly

2, does not support new fields, new methods, estimated also does not support mybatis XML loading those bar, did not understand

3, only suitable for use in development environment IDE, can not be used in production

I have no choice but to implement one by myself. The functions I need to implement are as follows

1, load the external extension JAR package in the new interface, multiple loads need to be able to fully update

2, should be able to load mybatis, Mybatis -plus put SQL XML file

3, should be able to load @mapper modified mybatis interface resources

4. Need to be able to load other Spring-managed Bean resources

5. Need to be able to update swagger document after loading

The main idea is to implement a container that extends the entire interface, which is similar to and different from hot loading, where hot deployment monitors local class file changes and then uses automatic restart or reloading. The hot deployment areas are DevTools and JRebel, which use automatic restart. Check that your classes have changed, and then use reflection to call your main method to restart the class. The main method uses the reload method. This article is to load the external JAR package, the JAR package as long as it is an accessible URL resources can be. Although not the same as hot deployment, the solution is to use the overloading method, that is, only the resources in the extension pack will be updated.

Start with a custom module classloader

package com.rdpaas.dynamic.core;


import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.PrivilegedExceptionAction;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;


/**
 * 动态加载外部jar包的自定义类加载器
 * @author rongdi
 * @date 2021-03-06
 * @blog https://www.cnblogs.com/rongdi
 */
public class ModuleClassLoader extends URLClassLoader {

    private Logger logger = LoggerFactory.getLogger(ModuleClassLoader.class);

    private final static String CLASS_SUFFIX = ".class";

    private final static String XML_SUFFIX = ".xml";

    private final static String MAPPER_SUFFIX = "mapper/";

    //属于本类加载器加载的jar包
    private JarFile jarFile;

    private Map<String, byte[]> classBytesMap = new HashMap<>();

    private Map<String, Class<?>> classesMap = new HashMap<>();

    private Map<String, byte[]> xmlBytesMap = new HashMap<>();

    public ModuleClassLoader(ClassLoader classLoader, URL... urls) {
        super(urls, classLoader);
        URL url = urls[0];
        String path = url.getPath();
        try {
            jarFile = new JarFile(path);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] buf = classBytesMap.get(name);
        if (buf == null) {
            return super.findClass(name);
        }
        if(classesMap.containsKey(name)) {
            return classesMap.get(name);
        }
        /**
         * 这里应该算是骚操作了,我不知道市面上有没有人这么做过,反正我是想了好久,遇到各种因为spring要生成代理对象
         * 在他自己的AppClassLoader找不到原对象导致的报错,注意如果你限制你的扩展包你不会有AOP触碰到的类或者@Transactional这种
         * 会产生代理的类,那么其实你不用这么骚,直接在这里调用defineClass把字节码装载进去就行了,不会有什么问题,最多也就是
         * 在加载mybatis的xml那里前后加三句话,
         * 1、获取并使用一个变量保存当前线程类加载器
         * 2、将自定义类加载器设置到当前线程类加载器
         * 3、还原当前线程类加载器为第一步保存的类加载器
         * 这样之后mybatis那些xml里resultType,resultMap之类的需要访问扩展包的Class的就不会报错了。
         * 不过直接用现在这种骚操作,更加一劳永逸,不会有mybatis的问题了
         */
        return loadClass(name,buf);
    }

    /**
     * 使用反射强行将类装载的归属给当前类加载器的父类加载器也就是AppClassLoader,如果报ClassNotFoundException
     * 则递归装载
     * @param name
     * @param bytes
     * @return
     */
    private Class<?> loadClass(String name, byte[] bytes) throws ClassNotFoundException {

        Object[] args = new Object[]{name, bytes, 0, bytes.length};
        try {
            /**
             * 拿到当前类加载器的parent加载器AppClassLoader
             */
            ClassLoader parent = this.getParent();
            /**
             * 首先要明确反射是万能的,仿造org.springframework.cglib.core.ReflectUtils的写法,强行获取被保护
             * 的方法defineClass的对象,然后调用指定类加载器的加载字节码方法,强行将加载归属塞给它,避免被spring的AOP或者@Transactional
             * 触碰到的类需要生成代理对象,而在AppClassLoader下加载不到外部的扩展类而报错,所以这里强行将加载外部扩展包的类的归属给
             * AppClassLoader,让spring的cglib生成代理对象时可以加载到原对象
             */
            Method classLoaderDefineClass = (Method) AccessController.doPrivileged(new PrivilegedExceptionAction() {
                @Override
                public Object run() throws Exception {
                    return ClassLoader.class.getDeclaredMethod("defineClass",
                            String.class, byte[].class, Integer.TYPE, Integer.TYPE);
                }
            });
            if(!classLoaderDefineClass.isAccessible()) {
                classLoaderDefineClass.setAccessible(true);
            }
            return (Class<?>)classLoaderDefineClass.invoke(parent,args);
        } catch (Exception e) {
            if(e instanceof InvocationTargetException) {
                String message = ((InvocationTargetException) e).getTargetException().getCause().toString();
                /**
                 * 无奈,明明ClassNotFoundException是个异常,非要抛个InvocationTargetException,导致
                 * 我这里一个不太优雅的判断
                 */
                if(message.startsWith("java.lang.ClassNotFoundException")) {
                    String notClassName = message.split(":")[1];
                    if(StringUtils.isEmpty(notClassName)) {
                        throw new ClassNotFoundException(message);
                    }
                    notClassName = notClassName.trim();
                    byte[] bytes1 = classBytesMap.get(notClassName);
                    if(bytes1 == null) {
                        throw new ClassNotFoundException(message);
                    }
                    /**
                     * 递归装载未找到的类
                     */
                    Class<?> notClass = loadClass(notClassName, bytes1);
                    if(notClass == null) {
                        throw new ClassNotFoundException(message);
                    }
                    classesMap.put(notClassName,notClass);
                    return loadClass(name,bytes);
                }
            } else {
                logger.error("",e);
            }
        }
        return null;
    }

    public Map<String,byte[]> getXmlBytesMap() {
        return xmlBytesMap;
    }


    /**
     * 方法描述 初始化类加载器,保存字节码
     */
    public Map<String, Class> load() {

        Map<String, Class> cacheClassMap = new HashMap<>();

        //解析jar包每一项
        Enumeration<JarEntry> en = jarFile.entries();
        InputStream input = null;
        try {
            while (en.hasMoreElements()) {
                JarEntry je = en.nextElement();
                String name = je.getName();
                //这里添加了路径扫描限制
                if (name.endsWith(CLASS_SUFFIX)) {
                    String className = name.replace(CLASS_SUFFIX, "").replaceAll("/", ".");
                    input = jarFile.getInputStream(je);
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    int bufferSize = 4096;
                    byte[] buffer = new byte[bufferSize];
                    int bytesNumRead = 0;
                    while ((bytesNumRead = input.read(buffer)) != -1) {
                        baos.write(buffer, 0, bytesNumRead);
                    }
                    byte[] classBytes = baos.toByteArray();
                    classBytesMap.put(className, classBytes);
                } else if(name.endsWith(XML_SUFFIX) && name.startsWith(MAPPER_SUFFIX)) {
                    input = jarFile.getInputStream(je);
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    int bufferSize = 4096;
                    byte[] buffer = new byte[bufferSize];
                    int bytesNumRead = 0;
                    while ((bytesNumRead = input.read(buffer)) != -1) {
                        baos.write(buffer, 0, bytesNumRead);
                    }
                    byte[] xmlBytes = baos.toByteArray();
                    xmlBytesMap.put(name, xmlBytes);
                }
            }
        } catch (IOException e) {
            logger.error("",e);
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        //将jar中的每一个class字节码进行Class载入
        for (Map.Entry<String, byte[]> entry : classBytesMap.entrySet()) {
            String key = entry.getKey();
            Class<?> aClass = null;
            try {
                aClass = loadClass(key);
            } catch (ClassNotFoundException e) {
                logger.error("",e);
            }
            cacheClassMap.put(key, aClass);
        }
        return cacheClassMap;

    }

    public Map<String, byte[]> getClassBytesMap() {
        return classBytesMap;
    }
}
Copy the code

Then come a load mybatis XML resources class, this class parsing XML part is reference online information

package com.rdpaas.dynamic.core;

import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.executor.ErrorContext;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.parsing.XPathParser;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.mapper.MapperFactoryBean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayInputStream;
import java.lang.reflect.Field;
import java.util.*;

/**
 * mybatis的mapper.xml和@Mapper加载类
 * @author rongdi
 * @date 2021-03-06
 * @blog https://www.cnblogs.com/rongdi
 */
public class MapperLoader {

    private Logger logger = LoggerFactory.getLogger(MapperLoader.class);

    private Configuration configuration;

    /**
     * 刷新外部mapper,包括文件和@Mapper修饰的接口
     * @param sqlSessionFactory
     * @param xmlBytesMap
     * @return
     */
    public Map<String,Object> refresh(SqlSessionFactory sqlSessionFactory, Map<String, byte[]> xmlBytesMap) {
        Configuration configuration = sqlSessionFactory.getConfiguration();
        this.configuration = configuration;

        /**
         * 这里用来区分mybatis-plus和mybatis,mybatis-plus的Configuration是继承自mybatis的子类
         */
        boolean isSupper = configuration.getClass().getSuperclass() == Configuration.class;
        Map<String,Object> mapperMap = new HashMap<>();
        try {
            /**
             * 遍历外部传入的xml字节码map
             */
            for(Map.Entry<String,byte[]> entry:xmlBytesMap.entrySet()) {
                String resource = entry.getKey();
                byte[] bytes = entry.getValue();
                /**
                 * 使用反射强行拿出configuration中的loadedResources属性
                 */
                Field loadedResourcesField = isSupper
                        ? configuration.getClass().getSuperclass().getDeclaredField("loadedResources")
                        : configuration.getClass().getDeclaredField("loadedResources");
                loadedResourcesField.setAccessible(true);
                Set loadedResourcesSet = ((Set) loadedResourcesField.get(configuration));
                /**
                 * 加载mybatis中的xml
                 */
                XPathParser xPathParser = new XPathParser(new ByteArrayInputStream(bytes), true, configuration.getVariables(),
                        new XMLMapperEntityResolver());
                /**
                 * 解析mybatis的xml的根节点,
                 */
                XNode context = xPathParser.evalNode("/mapper");
                /**
                 * 拿到namespace,namespace就是指Mapper接口的全限定名
                 */
                String namespace = context.getStringAttribute("namespace");
                Field field = configuration.getMapperRegistry().getClass().getDeclaredField("knownMappers");
                field.setAccessible(true);

                /**
                 * 拿到存放Mapper接口和对应代理子类的映射map,
                 */
                Map mapConfig = (Map) field.get(configuration.getMapperRegistry());
                /**
                 * 拿到Mapper接口对应的class对象
                 */
                Class nsClass = Resources.classForName(namespace);

                /**
                 * 先删除各种
                 */
                mapConfig.remove(nsClass);
                loadedResourcesSet.remove(resource);
                configuration.getCacheNames().remove(namespace);

                /**
                 * 清掉namespace下各种缓存
                 */
                cleanParameterMap(context.evalNodes("/mapper/parameterMap"), namespace);
                cleanResultMap(context.evalNodes("/mapper/resultMap"), namespace);
                cleanKeyGenerators(context.evalNodes("insert|update|select|delete"), namespace);
                cleanSqlElement(context.evalNodes("/mapper/sql"), namespace);

                /**
                 * 加载并解析对应xml
                 */
                XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(new ByteArrayInputStream(bytes),
                        sqlSessionFactory.getConfiguration(), resource,
                        sqlSessionFactory.getConfiguration().getSqlFragments());
                xmlMapperBuilder.parse();

                /**
                 * 构造MapperFactoryBean,注意这里一定要传入sqlSessionFactory,
                 * 这块逻辑通过debug源码试验了很久
                 */
                MapperFactoryBean mapperFactoryBean = new MapperFactoryBean(nsClass);
                mapperFactoryBean.setSqlSessionFactory(sqlSessionFactory);
                /**
                 * 放入map,返回出去给ModuleApplication去加载
                 */
                mapperMap.put(namespace,mapperFactoryBean);
                logger.info("refresh: '" + resource + "', success!");

            }
            return mapperMap;
        } catch (Exception e) {
            logger.error("refresh error",e.getMessage());
        } finally {
            ErrorContext.instance().reset();
        }
        return null;
    }

    /**
     * 清理parameterMap
     *
     * @param list
     * @param namespace
     */
    private void cleanParameterMap(List<XNode> list, String namespace) {
        for (XNode parameterMapNode : list) {
            String id = parameterMapNode.getStringAttribute("id");
            configuration.getParameterMaps().remove(namespace + "." + id);
        }
    }

    /**
     * 清理resultMap
     *
     * @param list
     * @param namespace
     */
    private void cleanResultMap(List<XNode> list, String namespace) {
        for (XNode resultMapNode : list) {
            String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier());
            configuration.getResultMapNames().remove(id);
            configuration.getResultMapNames().remove(namespace + "." + id);
            clearResultMap(resultMapNode, namespace);
        }
    }

    private void clearResultMap(XNode xNode, String namespace) {
        for (XNode resultChild : xNode.getChildren()) {
            if ("association".equals(resultChild.getName()) || "collection".equals(resultChild.getName())
                    || "case".equals(resultChild.getName())) {
                if (resultChild.getStringAttribute("select") == null) {
                    configuration.getResultMapNames()
                            .remove(resultChild.getStringAttribute("id", resultChild.getValueBasedIdentifier()));
                    configuration.getResultMapNames().remove(namespace + "."
                            + resultChild.getStringAttribute("id", resultChild.getValueBasedIdentifier()));
                    if (resultChild.getChildren() != null && !resultChild.getChildren().isEmpty()) {
                        clearResultMap(resultChild, namespace);
                    }
                }
            }
        }
    }

    /**
     * 清理selectKey
     *
     * @param list
     * @param namespace
     */
    private void cleanKeyGenerators(List<XNode> list, String namespace) {
        for (XNode context : list) {
            String id = context.getStringAttribute("id");
            configuration.getKeyGeneratorNames().remove(id + SelectKeyGenerator.SELECT_KEY_SUFFIX);
            configuration.getKeyGeneratorNames().remove(namespace + "." + id + SelectKeyGenerator.SELECT_KEY_SUFFIX);

            Collection<MappedStatement> mappedStatements = configuration.getMappedStatements();
            List<MappedStatement> objects = new ArrayList<>();
            Iterator<MappedStatement> it = mappedStatements.iterator();
            while (it.hasNext()) {
                Object object = it.next();
                if (object instanceof MappedStatement) {
                    MappedStatement mappedStatement = (MappedStatement) object;
                    if (mappedStatement.getId().equals(namespace + "." + id)) {
                        objects.add(mappedStatement);
                    }
                }
            }
            mappedStatements.removeAll(objects);
        }
    }

    /**
     * 清理sql节点缓存
     *
     * @param list
     * @param namespace
     */
    private void cleanSqlElement(List<XNode> list, String namespace) {
        for (XNode context : list) {
            String id = context.getStringAttribute("id");
            configuration.getSqlFragments().remove(id);
            configuration.getSqlFragments().remove(namespace + "." + id);
        }
    }

}
Copy the code

It is important to note that the XXMapper interface needs to be put into the Spring container as well, but the interface cannot be directly converted to spring’s BeanDefinition because the interface cannot be instantiated and the BeanDefinition is the template of the object. It is definitely not allowed to put interfaces directly into it. By looking at mybatis- Spring source code, we can see that these interfaces are wrapped into MapperFactoryBeans. When instantiated in the Spring container, getObject is called to generate a proxy object for Mapper. Here is the code to load the various resources into the Spring container

package com.rdpaas.dynamic.core;

import com.rdpaas.dynamic.utils.ReflectUtil;
import com.rdpaas.dynamic.utils.SpringUtil;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.builders.ResponseMessageBuilder;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.service.ResponseMessage;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.DocumentationPlugin;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.spring.web.plugins.DocumentationPluginsBootstrapper;
import springfox.documentation.spring.web.plugins.DocumentationPluginsManager;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.*;

/**
 * 基于spring的应用上下文提供一些工具方法
 * @author rongdi
 * @date 2021-03-06
 * @blog https://www.cnblogs.com/rongdi
 */
public class ModuleApplication {

    private final static String SINGLETON = "singleton";

    private final static String DYNAMIC_DOC_PACKAGE = "dynamic.swagger.doc.package";

    private Set<RequestMappingInfo> extMappingInfos = new HashSet<>();

    private ApplicationContext applicationContext;

    /**
     * 使用spring上下文拿到指定beanName的对象
     */
    public <T> T getBean(String beanName) {
        return (T) ((ConfigurableApplicationContext) applicationContext).getBeanFactory().getBean(beanName);
    }

    /**
     * 使用spring上下文拿到指定类型的对象
     */
    public <T> T getBean(Class<T> clazz) {
        return (T) ((ConfigurableApplicationContext) applicationContext).getBeanFactory().getBean(clazz);
    }

    /**
     * 加载一个外部扩展jar,包括springmvc接口资源,mybatis的@mapper和mapper.xml和spring bean等资源
     * @param url jar url
     * @param applicationContext spring context
     * @param sqlSessionFactory mybatis的session工厂
     */
    public void reloadJar(URL url, ApplicationContext applicationContext,SqlSessionFactory sqlSessionFactory) throws Exception {
        this.applicationContext = applicationContext;
        URL[] urls = new URL[]{url};
        /**
         * 这里实际上是将spring的ApplicationContext的类加载器当成parent传给了自定义类加载器,很明自定义的子类加载器自己加载
         * 的类,parent类加载器直接是获取不到的,所以在自定义类加载器做了特殊的骚操作
         */
        ModuleClassLoader moduleClassLoader = new ModuleClassLoader(applicationContext.getClassLoader(), urls);
        /**
         * 使用模块类加载器加载url资源的jar包,直接返回类的全限定名和Class对象的映射,这些Class对象是
         * jar包里所有.class结尾的文件加载后的结果,同时mybatis的xml加载后,无奈的放入了
         * moduleClassLoader.getXmlBytesMap(),不是很优雅
         */
        Map<String, Class> classMap = moduleClassLoader.load();

        MapperLoader mapperLoader = new MapperLoader();

        /**
         * 刷新mybatis的xml和Mapper接口资源,Mapper接口其实就是xml的namespace
         */
        Map<String, Object> extObjMap = mapperLoader.refresh(sqlSessionFactory, moduleClassLoader.getXmlBytesMap());
        /**
         * 将各种资源放入spring容器
         */
        registerBeans(applicationContext, classMap, extObjMap);
    }

    /**
     * 装载bean到spring中
     *
     * @param applicationContext
     * @param cacheClassMap
     */
    public void registerBeans(ApplicationContext applicationContext, Map<String, Class> cacheClassMap,Map<String,Object> extObjMap) throws Exception {
        /**
         * 将applicationContext转换为ConfigurableApplicationContext
         */
        ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
        /**
         * 获取bean工厂并转换为DefaultListableBeanFactory
         */
        DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();

        /**
         * 有一些对象想给spring管理,则放入spring中,如mybatis的@Mapper修饰的接口的代理类
         */
        if(extObjMap != null && !extObjMap.isEmpty()) {
            extObjMap.forEach((beanName,obj) ->{
                /**
                 * 如果已经存在,则销毁之后再注册
                 */
                if(defaultListableBeanFactory.containsSingleton(beanName)) {
                    defaultListableBeanFactory.destroySingleton(beanName);
                }
                defaultListableBeanFactory.registerSingleton(beanName,obj);
            });
        }

        for (Map.Entry<String, Class> entry : cacheClassMap.entrySet()) {
            String className = entry.getKey();
            Class<?> clazz = entry.getValue();
            if (SpringUtil.isSpringBeanClass(clazz)) {
                //将变量首字母置小写
                String beanName = StringUtils.uncapitalize(className);
                beanName = beanName.substring(beanName.lastIndexOf(".") + 1);
                beanName = StringUtils.uncapitalize(beanName);

               /**
                 * 已经在spring容器就删了
                 */
                if (defaultListableBeanFactory.containsBeanDefinition(beanName)) {
                    defaultListableBeanFactory.removeBeanDefinition(beanName);
                }
                /**
                 * 使用spring的BeanDefinitionBuilder将Class对象转成BeanDefinition
                 */
                BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
                BeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
                //设置当前bean定义对象是单利的
                beanDefinition.setScope(SINGLETON);
                /**
                 * 以指定beanName注册上面生成的BeanDefinition
                 */
                defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinition);
            }

        }

        /**
         * 刷新springmvc,让新增的接口生效
         */
        refreshMVC((ConfigurableApplicationContext) applicationContext);

    }

    /**
     * 刷新springMVC,这里花了大量时间调试,找不到开放的方法,只能取个巧,在更新RequestMappingHandlerMapping前先记录之前
     * 所有RequestMappingInfo,记得这里一定要copy一下,然后刷新后再记录一次,计算出差量存放在成员变量Set中,然后每次开头判断
     * 差量那里是否有内容,有就先unregiester掉
     */
    private void refreshMVC(ConfigurableApplicationContext applicationContext) throws Exception {


        Map<String, RequestMappingHandlerMapping> map = applicationContext.getBeanFactory().getBeansOfType(RequestMappingHandlerMapping.class);
        /**
         * 先拿到RequestMappingHandlerMapping对象
         */
        RequestMappingHandlerMapping mappingHandlerMapping = map.get("requestMappingHandlerMapping");

        /**
         * 重新注册mapping前先判断是否存在了,存在了就先unregister掉
         */
        if(!extMappingInfos.isEmpty()) {
            for(RequestMappingInfo requestMappingInfo:extMappingInfos) {
                mappingHandlerMapping.unregisterMapping(requestMappingInfo);
            }
        }

        /**
         * 获取刷新前的RequestMappingInfo
         */
        Map<RequestMappingInfo, HandlerMethod> preMappingInfoHandlerMethodMap = mappingHandlerMapping.getHandlerMethods();
        /**
         * 这里注意一定要拿到拷贝,不然刷新后内容就一致了,就没有差量了
         */
        Set<RequestMappingInfo> preRequestMappingInfoSet = new HashSet(preMappingInfoHandlerMethodMap.keySet());

        /**
         * 这里是刷新springmvc上下文
         */
        applicationContext.getBeanFactory().getBeansOfType(RequestMappingHandlerMapping.class)
        .forEach((key,value) ->{
            value.afterPropertiesSet();
        });

        /**
         * 获取刷新后的RequestMappingInfo
         */
        Map<RequestMappingInfo, HandlerMethod> afterMappingInfoHandlerMethodMap = mappingHandlerMapping.getHandlerMethods();
        Set<RequestMappingInfo> afterRequestMappingInfoSet = afterMappingInfoHandlerMethodMap.keySet();

        /**
         * 填充差量部分RequestMappingInfo
          */
        fillSurplusRequestMappingInfos(preRequestMappingInfoSet,afterRequestMappingInfoSet);

        /**
         * 这里真的是不讲武德了,每次调用value.afterPropertiesSet();如下urlLookup都会产生重复,暂时没找到开放方法去掉重复,这里重复会导致
         * 访问的时候报错Ambiguous handler methods mapped for
         * 目标是去掉RequestMappingHandlerMapping -> RequestMappingInfoHandlerMapping -> AbstractHandlerMethodMapping
         * -> mappingRegistry -> urlLookup重复的RequestMappingInfo,这里的.getClass().getSuperclass().getSuperclass()相信会
         * 很懵逼,如果单独通过getClass().getDeclaredMethod("getMappingRegistry",new Class[]{})是无论如何都拿不到父类的非public非
         * protected方法的,因为这个方法不属于子类,只有父类才可以访问到,只有你拿得到你才有资格不讲武德的使用method.setAccessible(true)强行
         * 访问
         */
        Method method = ReflectUtil.getMethod(mappingHandlerMapping,"getMappingRegistry",new Class[]{});
        method.setAccessible(true);
        Object mappingRegistryObj = method.invoke(mappingHandlerMapping,new Object[]{});
        Field field = mappingRegistryObj.getClass().getDeclaredField("urlLookup");
        field.setAccessible(true);
        MultiValueMap<String, RequestMappingInfo> multiValueMap = (MultiValueMap)field.get(mappingRegistryObj);
        multiValueMap.forEach((key,list) -> {
            clearMultyMapping(list);
        });

    }
 /**
     * 填充差量的RequestMappingInfo,因为已经重写过hashCode和equals方法所以可以直接用对象判断是否存在
     * @param preRequestMappingInfoSet
     * @param afterRequestMappingInfoSet
     */
    private void fillSurplusRequestMappingInfos(Set<RequestMappingInfo> preRequestMappingInfoSet,Set<RequestMappingInfo> afterRequestMappingInfoSet) {
        for(RequestMappingInfo requestMappingInfo:afterRequestMappingInfoSet) {
            if(!preRequestMappingInfoSet.contains(requestMappingInfo)) {
                extMappingInfos.add(requestMappingInfo);
            }
        }
    }

    /**
     * 简单的逻辑,删除List里重复的RequestMappingInfo,已经写了toString,直接使用mappingInfo.toString()就可以区分重复了
     * @param mappingInfos
     */
    private void clearMultyMapping(List<RequestMappingInfo> mappingInfos) {
        Set<String> containsList = new HashSet<>();
        for(Iterator<RequestMappingInfo> iter = mappingInfos.iterator();iter.hasNext();) {
            RequestMappingInfo mappingInfo = iter.next();
            String flag = mappingInfo.toString();
            if(containsList.contains(flag)) {
                iter.remove();
            } else {
                containsList.add(flag);
            }
        }
    }

}
Copy the code

There are two things that are really frustrating, and the first one is refreshing SpringMVC where they provide a way to refresh springMVC context that is not very friendly, Refresh the context after RequestMappingHandlerMapping – > RequestMappingInfoHandlerMapping – > AbstractHandlerMethodMapping – > The mappingRegistry -> urlLookup property will have duplicate paths as follows

After loading the same JAR package twice, I went to refresh SpringMVC for the second time. I could see that the interface in the extension package was not found to be duplicated because of unregister. Those duplicated paths were all the interfaces of its own service. If a duplicate interface is accessed at this time, the following error occurs

java.lang.IllegalStateException: Ambiguous handler methods mapped for ‘/error’:

RequestMappingInfo = urlLookup = urlLookup = urlLookup = urlLookup = urlLookup = urlLookup Actually don’t reject using reflection, the spring source of using reflection to forcibly call methods, such as the org, springframework. Additional. Core. ReflectUtils class copied as follows:


classLoaderDefineClass = (Method) AccessController.doPrivileged(new PrivilegedExceptionAction() {
   public Object run() throws Exception {
      return ClassLoader.class.getDeclaredMethod("defineClass",
            String.class, byte[].class, Integer.TYPE, Integer.TYPE, ProtectionDomain.class);
   }
});
classLoaderDefineClassMethod = classLoaderDefineClass;
// Classic option: protected ClassLoader.defineClass method
if (c == null && classLoaderDefineClassMethod != null) {
   if (protectionDomain == null) {
      protectionDomain = PROTECTION_DOMAIN;
   }
   Object[] args = new Object[]{className, b, 0, b.length, protectionDomain};
   try {
      if (!classLoaderDefineClassMethod.isAccessible()) {
         classLoaderDefineClassMethod.setAccessible(true);
      }
      c = (Class) classLoaderDefineClassMethod.invoke(loader, args);
   }
   catch (InvocationTargetException ex) {
      throw new CodeGenerationException(ex.getTargetException());
   }
   catch (Throwable ex) {
      // Fall through if setAccessible fails with InaccessibleObjectException on JDK 9+
      // (on the module path and/or with a JVM bootstrapped with --illegal-access=deny)
      if (!ex.getClass().getName().endsWith("InaccessibleObjectException")) {
         throw new CodeGenerationException(ex);
      }
   }
}
Copy the code

It can be seen from the above that famous artists like Spring also don’t speak martial arts. Personally, I think reflection itself is used for us to break rules. Only by breaking rules can there be innovation. Reflection is omnipotent as long as you don’t encounter final attributes, haha! So I used reflection to forcibly remove duplicate code as follows:

AfterPropertiesSet (); afterPropertiesSet(); UrlLookup (urlLookup, urlLookup, urlLookup) Repeat here will lead to * access error when Ambiguous handler methods mapped for * goal is to remove the RequestMappingHandlerMapping - > RequestMappingInfoHandlerMapping -> AbstractHandlerMethodMapping * -> mappingRegistry -> UrlLookup repeated RequestMappingInfo, where.getClass().getSuperClass ().getSuperclass().getSuperclass() If you use getClass().getDeclaredMethod("getMappingRegistry",new Class[]{}) alone, you can't get a non-public non-* protected method of the parent Class anyway, because the method is not a subclass. Only the parent class can access it, Use method.setaccessible (true) to forcibly access */ method method = ReflectUtil.getMethod(mappingHandlerMapping,"getMappingRegistry",new Class[]{}); method.setAccessible(true); Object mappingRegistryObj = method.invoke(mappingHandlerMapping,new Object[]{}); Field field = mappingRegistryObj.getClass().getDeclaredField("urlLookup"); field.setAccessible(true); MultiValueMap<String, RequestMappingInfo> multiValueMap = (MultiValueMap)field.get(mappingRegistryObj); multiValueMap.forEach((key,list) -> { clearMultyMapping(list); }); RequestMappingInfo = RequestMappingInfo = RequestMappingInfo @param mappingInfos */ private void clearMultyMapping(List<RequestMappingInfo>) mappingInfos) { Set<String> containsList = new HashSet<>(); for(Iterator<RequestMappingInfo> iter = mappingInfos.iterator(); iter.hasNext();) { RequestMappingInfo mappingInfo = iter.next(); String flag = mappingInfo.toString(); if(containsList.contains(flag)) { iter.remove(); } else { containsList.add(flag); }}}Copy the code

There is also a painful place to refresh Swagger’s document. Swagger only knows when he needs to do this, because he packages many dishes. There is no way to refresh and no entrance that can be controlled, so there is really no way. If you want to integrate Swagger2 with SpringBoot, you will need to write the following configuration code

@configuration@enablesWagger2 public class SwaggerConfig { @bean public Docket createRestApi() {List<ResponseMessage> responseMessageList = new ArrayList<>(); ResponseMessageList. Add (new ResponseMessageBuilder (). Code (200). The message (" success "). ResponseModel (new ModelRef("Payload")).build()); Docket docket = new Docket(DocumentationType.SWAGGER_2) .globalResponseMessage(RequestMethod.GET,responseMessageList) .globalResponseMessage(RequestMethod.DELETE,responseMessageList) GlobalResponseMessage (RequestMethod. POST, responseMessageList) apiInfo (apiInfo ()). The select () / / for the current path .apis(RequestHandlerSelectors.basePackage("com.xxx")).paths(PathSelectors.any()).build(); return docket; Private ApiInfo ApiInfo () {return new ApiInfoBuilder().title(" Use Swagger2 Build a RESTful API ") / / founder. Contact, new contact (" rongdi ", "https://www.cnblogs.com/rongdi", "495194630 @qq.com")) / / version number, version (" 1.0 ") / / description. The description (" API management "). The build (); }}Copy the code

To access Swagger’s documents, request interface /v2/api-docs

Can be found through debugging swagger2 interface is by implementing SmartLifecycle DocumentationPluginsBootstrapper class, when the spring container loading all the beans and complete the initialized, The class of the interface will callback implementations (DocumentationPluginsBootstrapper) corresponding method in the start () method, the following will introduce how to find here.

The DocumentationPlugin collection is then looped through to process the document

It is then put into DocumentationCache

And then we go back to our Swagger interface class, and we actually get Documention from this DocumentationCache

‘If we can’t find a way to resolve the problem, we can at least find the address (exit) of the interface above the document, and find that the json content of the document returned by the interface is retrieved from the DocumentationCache. So it’s obvious that there must be some place to store data in the DocumentationCache, and we can actually just set a breakpoint in the addDocumentation method and look at the run stack on the left side of the debug and see the call link

Go back to the configuration code we wrote when we plugged in Swagger2

// Swagger2 config file @bean public Docket createRestApi() {List<ResponseMessage> responseMessageList = new ArrayList<>(); ResponseMessageList. Add (new ResponseMessageBuilder (). Code (200). The message (" success "). ResponseModel (new ModelRef("Payload")).build()); Docket docket = new Docket(DocumentationType.SWAGGER_2) .globalResponseMessage(RequestMethod.GET,responseMessageList) .globalResponseMessage(RequestMethod.DELETE,responseMessageList) GlobalResponseMessage (RequestMethod. POST, responseMessageList) apiInfo (apiInfo ()). The select () / / for the current path .apis(RequestHandlerSelectors.basePackage("com.xxx")).paths(PathSelectors.any()).build(); return docket; }Copy the code

In fact, we only need to care about the basePackage Docket object, we extend the jar package most likely the interface of the package is not the same as the existing package, so we need to add a Docket plug-in. And join DocumentationPlugin collection, and then call DocumentationPluginsBootstrapper stop () method to clear the cache, then call the start () begin to parse again

The specific implementation code is as follows

 /**
     * 刷新springMVC,这里花了大量时间调试,找不到开放的方法,只能取个巧,在更新RequestMappingHandlerMapping前先记录之前
     * 所有RequestMappingInfo,记得这里一定要copy一下,然后刷新后再记录一次,计算出差量存放在成员变量Set中,然后每次开头判断
     * 差量那里是否有内容,有就先unregiester掉
     */
    private void refreshMVC(ConfigurableApplicationContext applicationContext) throws Exception {


        Map<String, RequestMappingHandlerMapping> map = applicationContext.getBeanFactory().getBeansOfType(RequestMappingHandlerMapping.class);
        /**
         * 先拿到RequestMappingHandlerMapping对象
         */
        RequestMappingHandlerMapping mappingHandlerMapping = map.get("requestMappingHandlerMapping");

        /**
         * 重新注册mapping前先判断是否存在了,存在了就先unregister掉
         */
        if(!extMappingInfos.isEmpty()) {
            for(RequestMappingInfo requestMappingInfo:extMappingInfos) {
                mappingHandlerMapping.unregisterMapping(requestMappingInfo);
            }
        }

        /**
         * 获取刷新前的RequestMappingInfo
         */
        Map<RequestMappingInfo, HandlerMethod> preMappingInfoHandlerMethodMap = mappingHandlerMapping.getHandlerMethods();
        /**
         * 这里注意一定要拿到拷贝,不然刷新后内容就一致了,就没有差量了
         */
        Set<RequestMappingInfo> preRequestMappingInfoSet = new HashSet(preMappingInfoHandlerMethodMap.keySet());

        /**
         * 这里是刷新springmvc上下文
         */
        applicationContext.getBeanFactory().getBeansOfType(RequestMappingHandlerMapping.class)
        .forEach((key,value) ->{
            value.afterPropertiesSet();
        });

        /**
         * 获取刷新后的RequestMappingInfo
         */
        Map<RequestMappingInfo, HandlerMethod> afterMappingInfoHandlerMethodMap = mappingHandlerMapping.getHandlerMethods();
        Set<RequestMappingInfo> afterRequestMappingInfoSet = afterMappingInfoHandlerMethodMap.keySet();

        /**
         * 填充差量部分RequestMappingInfo
          */
        fillSurplusRequestMappingInfos(preRequestMappingInfoSet,afterRequestMappingInfoSet);

        /**
         * 这里真的是不讲武德了,每次调用value.afterPropertiesSet();如下urlLookup都会产生重复,暂时没找到开放方法去掉重复,这里重复会导致
         * 访问的时候报错Ambiguous handler methods mapped for
         * 目标是去掉RequestMappingHandlerMapping -> RequestMappingInfoHandlerMapping -> AbstractHandlerMethodMapping
         * -> mappingRegistry -> urlLookup重复的RequestMappingInfo,这里的.getClass().getSuperclass().getSuperclass()相信会
         * 很懵逼,如果单独通过getClass().getDeclaredMethod("getMappingRegistry",new Class[]{})是无论如何都拿不到父类的非public非
         * protected方法的,因为这个方法不属于子类,只有父类才可以访问到,只有你拿得到你才有资格不讲武德的使用method.setAccessible(true)强行
         * 访问
         */
        Method method = ReflectUtil.getMethod(mappingHandlerMapping,"getMappingRegistry",new Class[]{});
        method.setAccessible(true);
        Object mappingRegistryObj = method.invoke(mappingHandlerMapping,new Object[]{});
        Field field = mappingRegistryObj.getClass().getDeclaredField("urlLookup");
        field.setAccessible(true);
        MultiValueMap<String, RequestMappingInfo> multiValueMap = (MultiValueMap)field.get(mappingRegistryObj);
        multiValueMap.forEach((key,list) -> {
            clearMultyMapping(list);
        });

        /**
         * 刷新swagger文档
         */
        refreshSwagger(applicationContext);
    }


    /**
     * 刷新swagger文档
     * @param applicationContext
     * @throws Exception
     */
    private void refreshSwagger(ConfigurableApplicationContext applicationContext) throws Exception {
        /**
         * 获取扩展包swagger的地址接口扫描包,如果有配置则执行文档刷新操作
         */
        String extSwaggerDocPackage = applicationContext.getEnvironment().getProperty(DYNAMIC_DOC_PACKAGE);
        if (!StringUtils.isEmpty(extSwaggerDocPackage)) {
            /**
             * 拿到swagger解析文档的入口类,真的不想这样,主要是根本不提供刷新和重新加载文档的方法,只能不讲武德了
             */
            DocumentationPluginsBootstrapper bootstrapper = applicationContext.getBeanFactory().getBean(DocumentationPluginsBootstrapper.class);
            /**
             * 不管愿不愿意,强行拿到属性得到documentationPluginsManager对象
             */
            Field field1 = bootstrapper.getClass().getDeclaredField("documentationPluginsManager");
            field1.setAccessible(true);
            DocumentationPluginsManager documentationPluginsManager = (DocumentationPluginsManager) field1.get(bootstrapper);

            /**
             * 继续往下层拿documentationPlugins属性
             */
            Field field2 = documentationPluginsManager.getClass().getDeclaredField("documentationPlugins");
            field2.setAccessible(true);
            PluginRegistry<DocumentationPlugin, DocumentationType> pluginRegistrys = (PluginRegistry<DocumentationPlugin, DocumentationType>) field2.get(documentationPluginsManager);
            /**
             * 拿到最关键的文档插件集合,所有逻辑文档解析逻辑都在插件中
             */
            List<DocumentationPlugin> dockets = pluginRegistrys.getPlugins();
            /**
             * 真的不能怪我,好端端,你还搞个不能修改的集合,强行往父类递归拿到unmodifiableList的list属性
             */
            Field unModList = ReflectUtil.getField(dockets,"list");
            unModList.setAccessible(true);
            List<DocumentationPlugin> modifyerList = (List<DocumentationPlugin>) unModList.get(dockets);
            /**
             * 这下老实了吧,把自己的Docket加入进去,这里的groupName为dynamic
             */
            modifyerList.add(createRestApi(extSwaggerDocPackage));
            /**
             * 清空罪魁祸首DocumentationCache缓存,不然就算再加载一次,获取文档还是从这个缓存中拿,不会完成更新
             */
            bootstrapper.stop();
            /**
             * 手动执行重新解析swagger文档
             */
            bootstrapper.start();
        }
    }

    public Docket createRestApi(String basePackage) {
        List<ResponseMessage> responseMessageList = new ArrayList<>();
        responseMessageList.add(new ResponseMessageBuilder().code(200).message("成功").responseModel(new ModelRef("Payload")).build());
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("dynamic")
                .globalResponseMessage(RequestMethod.GET,responseMessageList)
                .globalResponseMessage(RequestMethod.DELETE,responseMessageList)
                .globalResponseMessage(RequestMethod.POST,responseMessageList)
                .apiInfo(apiInfo()).select()
                //为当前包路径
                .apis(RequestHandlerSelectors.basePackage(basePackage)).paths(PathSelectors.any()).build();
        return docket;
    }

    /**
     * 构建api文档的详细信息函数
     */
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                //页面标题
                .title("SpringBoot动态扩展")
                //创建人
                .contact(new Contact("rongdi", "https://www.cnblogs.com/rongdi", "[email protected]"))
                //版本号
                .version("1.0")
                //描述
                .description("api管理").build();
    }
Copy the code

Okay, let’s give you an overview of the extension

package com.rdpaas.dynamic.config; import com.rdpaas.dynamic.core.ModuleApplication; import org.apache.ibatis.session.SqlSessionFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.net.URL; / * * * all Configuration entry * @ author rongdi * @ the date 2021-03-06 * * / @ @ blog https://www.cnblogs.com/rongdi Configuration public class DynamicConfig implements ApplicationContextAware { private static final Logger logger = LoggerFactory.getLogger(DynamicConfig.class); @Autowired private SqlSessionFactory sqlSessionFactory; private ApplicationContext applicationContext; @Value("${dynamic.jar:/}") private String dynamicJar; @Bean public ModuleApplication moduleApplication() throws Exception { return new ModuleApplication(); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } /** * Reload an external JAR using an event called ApplicationStartedEvent. This can also be done directly in the moduleApplication() method, but in order to verify that the container is initialized and the extension package is loaded, So I put it there on purpose. * @return */ @Bean @ConditionalOnProperty(prefix = "dynamic",name = "jar") public ApplicationListener ApplicationListener1 () {return (ApplicationListener<ApplicationStartedEvent>) event -> {try {/** * loads the external extension JAR */ moduleApplication().reloadJar(new URL(dynamicJar),applicationContext,sqlSessionFactory); } catch (Exception e) { logger.error("",e); }}; }}Copy the code

And a switch note

package com.rdpaas.dynamic.anno; import com.rdpaas.dynamic.config.DynamicConfig; import org.springframework.context.annotation.Import; import java.lang.annotation.*; / open the dynamic extension of annotation * * * * @ author rongdi * @ date * @ 2021-03-06 blog https://www.cnblogs.com/rongdi * / @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE}) @Documented @Import({DynamicConfig.class}) public @interface EnableDynamic { }Copy the code

Ok, so far the core code and functions are shared, detailed source code and use instructions see github: github.com/rongdi/spri…