最近有个需求:须要让现有springboot项目能够加载外部的jar包实现新增、更新接口逻辑。本着拿来主义的思惟网上找了半天没有找到相似的东西,惟一有点类似的仍是spring-loaded可是这个东西据我网上了解有以下缺点:java
一、使用java agent启动,我的倾向于直接使用pom依赖的方式git
二、不支持新增字段,新增方法,估计也不支持mybatis的xml加载那些吧,没了解过github
三、只适合在开发环境IDE中使用,无法生产使用web
无奈之下,我只能本身实现一个了,我须要实现的功能以下spring
一、加载外部扩展jar包中的新接口,屡次加载须要能彻底更新sql
二、应该能加载mybatis、mybatis-plus中放sql的xml文件apache
三、应该能加载@Mapper修饰的mybatis的接口资源json
四、须要能加载其它被spring管理的Bean资源bootstrap
五、须要能在加载完成后更新swagger文档api
总而言之就是要实现一个可以扩展完整接口的容器,其实相似于热加载也不一样于热加载,热部署是监控本地的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那里先后加三句话, * 一、获取并使用一个变量保存当前线程类加载器 * 二、将自定义类加载器设置到当前线程类加载器 * 三、还原当前线程类加载器为第一步保存的类加载器 * 这样以后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", "495194630@qq.com")) //版本号 .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", "495194630@qq.com")) //版本号 .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