1. 程式人生 > >自己動手實現springboot執行時新增/更新外部介面

自己動手實現springboot執行時新增/更新外部介面

  最近有個需求:需要讓現有springboot專案可以載入外部的jar包實現新增、更新介面邏輯。本著拿來主義的思維網上找了半天沒有找到類似的東西,唯一有點相似的還是spring-loaded但是這個東西據我網上了解有如下缺點:

  1、使用java agent啟動,個人傾向於直接使用pom依賴的方式

  2、不支援新增欄位,新增方法,估計也不支援mybatis的xml載入那些吧,沒了解過

  3、只適合在開發環境IDE中使用,沒法生產使用

  無奈之下,我只能自己實現一個了,我需要實現的功能如下

  1、載入外部擴充套件jar包中的新介面,多次載入需要能完全更新

  2、應該能載入mybatis、mybatis-plus中放sql的xml檔案

  3、應該能載入@Mapper修飾的mybatis的介面資源

  4、需要能載入其它被spring管理的Bean資源

  5、需要能在載入完成後更新swagger文件

  總而言之就是要實現一個能夠擴充套件完整介面的容器,其實類似於熱載入也不同於熱載入,熱部署是監控本地的class檔案的改變,然後使用自動重啟或者過載,熱部署領域比較火的就是devtools和jrebel,前者使用自動重啟的方式,監控你的classes改變了,然後使用反射呼叫你的main方法重啟一下,後者使用過載的方式,因為收費,具體原理也沒了解過,估計就是不重啟,只加載變過的class吧。而本文實現的是載入外部的jar包,這個jar包只要是個可訪問的URL資源就可以了。雖然和熱部署不一樣,但是從方案上可以借鑑,本文就是使用過載的方式,也就是隻會更新擴充套件包裡的資源。

  先來一個自定義的模組類載入器

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;
    }
}

 然後再來個載入mybatis的xml資源的類,本類解析xml部分是參考網上資料

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);
        }
    }

}

  上面需要注意的是,處理好xml還需要將XXMapper介面也放入spring容器中,但是介面是沒辦法直接轉成spring的BeanDefinition的,因為介面沒辦法例項化,而BeanDefinition作為物件的模板,肯定不允許介面直接放進去,通過看mybatis-spring原始碼,可以看出這些介面都會被封裝成MapperFactoryBean放入spring容器中例項化時就呼叫getObject方法生成Mapper的代理物件。下面就是將各種資源裝載spring容器的程式碼了

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);
            }
        }
    }

}

  上述有兩個地方很虐心,第一個就是重新整理springmvc那裡,提供的重新整理springmvc上下文的方式不友好不說,重新整理上下文後RequestMappingHandlerMapping -> RequestMappingInfoHandlerMapping -> AbstractHandlerMethodMapping -> mappingRegistry -> urlLookup屬性中會存在重複的路徑如下

   上述是我故意兩次載入同一個jar包後第二次走到重新整理springmvc之後,可以看到擴充套件包裡的介面,由於unregister所以沒有發現重複,那些重複的路徑都是本身服務的介面,由於沒有unregister所以出現了大把重複,如果這個時候訪問重複的介面,會出現如下錯誤

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

   意思就是匹配到了多個相同的路徑解決方法有兩種,第一種就是所有RequestMappingInfo都先unregister再重新整理,第二種就是我除錯很久確認就只有urlLookup會發生衝重複,所以如下使用萬能的反射強行修改值,其實不要排斥使用反射,spring原始碼中大量使用反射去強行呼叫方法,比如org.springframework.cglib.core.ReflectUtils類摘抄如下:

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);
      }
   }
}

  如上可以看出來像spring這樣的名家也一樣也很不講武德,個人認為反射本身就是用來給我們打破規則用的,只有打破規則才會有創新,所以大膽使用反射吧。只要不遇到final的屬性,反射是萬能的,哈哈!所以我使用反射強行刪除重複的程式碼如下:

     /**
         * 這裡真的是不講武德了,每次呼叫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);
        });

   /**
     * 簡單的邏輯,刪除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);
            }
        }
    }

  還有個虐心的地方是重新整理swagger文件的地方,這個swagger只有需要做這個需求時才知道,他封裝的有多菜,根本沒有重新整理相關的方法,也沒有可以控制的入口,真的是沒辦法。下面貼出我解決重新整理swagger文件的除錯過程,使用過swagger2的朋友們都知道,要想在springboot整合swagger2主要需要編寫的配置程式碼如下

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    //swagger2的配置檔案,這裡可以配置swagger2的一些基本的內容,比如掃描的包等等
    @Bean
    public Docket createRestApi() {
        List<ResponseMessage> responseMessageList = new ArrayList<>();
        responseMessageList.add(new ResponseMessageBuilder().code(200).message("成功").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()).select()
                //為當前包路徑
                .apis(RequestHandlerSelectors.basePackage("com.xxx")).paths(PathSelectors.any()).build();
        return docket;
    }

    //構建 api文件的詳細資訊函式,注意這裡的註解引用的是哪個
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                //頁面標題
                .title("使用 Swagger2 構建RESTful API")
                //建立人
                .contact(new Contact("rongdi", "https://www.cnblogs.com/rongdi", "[email protected]"))
                //版本號
                .version("1.0")
                //描述
                .description("api管理").build();
    }

}

而訪問swagger的文件請求的是如下介面/v2/api-docs

   通過除錯可以找到swagger2就是通過實現了SmartLifecycle介面的DocumentationPluginsBootstrapper類,當spring容器載入所有bean並完成初始化之後,會回撥實現該介面的類(DocumentationPluginsBootstrapper)中對應的方法start()方法,下面會介紹怎麼找到這裡的。

 接著迴圈DocumentationPlugin集合去處理文件

 接著放入DocumentationCache中

 然後再回到swagger介面的類那裡,實際上就是從這個DocumentationCache裡獲取到Documention

 ‘如果找不到解決問題的入口,我們至少可以找到訪問文件的上面這個介面地址(出口),發現介面返回的文件json內容是從DocumentationCache裡獲取,那麼我們很明顯可以想到肯定有地方存放資料到這個DocumentationCache裡,然後其實我們可以直接在addDocumentation方法裡打個斷點,然後看除錯左側的執行方法棧資訊,就可以很明確的看到呼叫鏈路了

 再回看我們接入swagger2的時候寫的配置程式碼

//swagger2的配置檔案,這裡可以配置swagger2的一些基本的內容,比如掃描的包等等
    @Bean
    public Docket createRestApi() {
        List<ResponseMessage> responseMessageList = new ArrayList<>();
        responseMessageList.add(new ResponseMessageBuilder().code(200).message("成功").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()).select()
                //為當前包路徑
                .apis(RequestHandlerSelectors.basePackage("com.xxx")).paths(PathSelectors.any()).build();
        return docket;
    }

然後再看看下圖,應該終於知道咋回事了吧,其實Docket物件我們僅僅需要關心的是basePackage,我們擴充套件jar包大概率介面所在的包和現有包不一樣,所以我們需要新增一個Docket外掛,並加入DocumentationPlugin集合,然後呼叫DocumentationPluginsBootstrapper的stop()方法清掉快取,再呼叫start()再次開始解析

 具體實現程式碼如下

 /**
     * 重新整理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();
    }

好了,下面給一下整個擴充套件功能的入口吧

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;

/**
 * 一切配置的入口
 * @author rongdi
 * @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;
    }

    /**
     * 隨便找個事件ApplicationStartedEvent,用來reload外部的jar,其實直接在moduleApplication()方法也可以做
     * 這件事,但是為了驗證容器初始化後再載入擴充套件包還可以生效,所以故意放在了這裡。
     * @return
     */
    @Bean
    @ConditionalOnProperty(prefix = "dynamic",name = "jar")
    public ApplicationListener applicationListener1() {
        return (ApplicationListener<ApplicationStartedEvent>) event -> {
            try {
                /**
                 * 載入外部擴充套件jar
                 */
                moduleApplication().reloadJar(new URL(dynamicJar),applicationContext,sqlSessionFactory);
            } catch (Exception e) {
                logger.error("",e);
            }

        };
    }


}

再給個開關注解

package com.rdpaas.dynamic.anno;

import com.rdpaas.dynamic.config.DynamicConfig;
import org.springframework.context.annotation.Import;

import java.lang.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 {
}

  好了,至此核心程式碼和功能都分享完了,詳細原始碼和使用說明見github:https://github.com/rongdi/springboot-dyn