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-dynamic
到此這篇關於springboot運行時新增/更新外部接口的實現方法的文章就介紹到這瞭,更多相關springboot外部接口內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- SpringBoot根據目錄結構自動配置Url前綴方式
- SpringBoot項目如何將Bean註入到普通類中
- SpringBoot根據目錄結構自動生成路由前綴的實現代碼
- SpringBoot中的main方法註入service
- Springboot 如何獲取上下文對象