springboot 动态加载
- 背景及实现思路
- 加载jar的技术栈
- 实现加载
- load class
- 通常bean注册过程
- controller加载
- controller的加载机制
- 关于IOC
- 关于AOP
- service加载
- mapper加载
- 注册别名
- 解析XML文件
- 其他类加载
背景及实现思路
想要设计一个stater,可以方便加载一个可以单独运行的springboot单体jar包,为了在已执行的服务上面快速的扩展功能而不需要重启整个服务,又或者低代码平台生成代码之后可以快速预览。
加载jar的技术栈
- springboot 2.2.6.RELEASE
- mybatis-plus 3.4.1
实现加载
想要完成类加载要熟悉spring中类加载机制,以及java中classloader的双亲委派机制。
加载分为两大步
第一步需要将对应的jar中的class文件加载进当前运行内存中,第二步则是将对应的bean注册到spring,交由spring管理。
load class
load class主要使用jdk中URLClassLoader工具类,但是这里要注意一点,构建classloader时,构造函数可以指定父类加载器,如果指定之后,java才会将两个classloader加载的同一个class视作类型一致,如果不指定会出现 com.demo.A can not cast to com.demo.A这样的情况。
但是我这里依旧没有指定父类加载器,原因如下:
- 我要加载的jar都是可以独立运行的,没有必须要依赖别的工程的文件
- 我需要可以卸载掉,如果制定了父类加载器,那么会到这这个classloader不能回收,那么该加载器就一直在内存中。
加载jar的代码
/**
* 加载jar包
*
* @param jarPath jar路径
* @param packageName 扫面代码的路径
* @return
*/
public boolean loadJar(String jarPath, String packageName) {
try {
File file = FileUtil.file(jarPath);
URLClassLoader classloader = new URLClassLoader(new URL[]{file.toURI().toURL()}, this.applicationContext.getClassLoader());
JarFile jarFile = new JarFile(file);
// 获取jar包下所有的classes
String pkgPath = packageName.replace(".", "/");
Enumeration<JarEntry> entries = jarFile.entries();
Class<?> clazz = null;
List<JarEntry> xmlJarEntry = new ArrayList<>();
List<String> loadedAliasClasses = new ArrayList<>();
List<String> otherClasses = new ArrayList<>();
// 首先加载model
while (entries.hasMoreElements()) {
JarEntry jarEntry = entries.nextElement();
String entryName = jarEntry.getName();
if (entryName.charAt(0) == '/') {
entryName = entryName.substring(1);
}
if (entryName.endsWith("Mapper.xml")) {
xmlJarEntry.add(jarEntry);
} else {
if (jarEntry.isDirectory() || !entryName.contains(pkgPath) || !entryName.endsWith(".class")) {
continue;
}
String className = entryName.substring(0, entryName.length() - 6);
otherClasses.add(className.replace("/", "."));
log.info("load class : " + className.replace("/", "."));
// 将变量首字母置小写
String beanName = StringUtils.uncapitalize(className);
if (beanName.contains(LoaderConstant.MODEL)) {
// 加载所有的class
clazz = classloader.loadClass(className.replace("/", "."));
SqlSessionFactory sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
sqlSessionFactory.getConfiguration().getTypeAliasRegistry().registerAlias(beanName.replace("/", "."), clazz);
loadedAliasClasses.add(beanName.replace("/", ".").toLowerCase());
doMap.put(className.replace("/", "."), clazz);
}
}
}
// 再加载其他class
for (String otherClass : otherClasses) {
// 加载所有的class
clazz = classloader.loadClass(otherClass.replace("/", "."));
log.info("load class : " + otherClass.replace("/", "."));
// 将变量首字母置小写
String beanName = StringUtils.uncapitalize(otherClass);
if (beanName.endsWith(LoaderConstant.MAPPER)) {
mapperMap.put(beanName, clazz);
} else if (beanName.endsWith(LoaderConstant.CONTROLLER)) {
controllerMap.put(beanName, clazz);
} else if (beanName.endsWith(LoaderConstant.SERVICE_IMPL)) {
serviceImplMap.put(beanName, clazz);
} else if (beanName.endsWith(LoaderConstant.SERVICE)) {
serviceMap.put(beanName, clazz);
}
}
// 加载所有XML
for (JarEntry jarEntry : xmlJarEntry) {
SqlSessionFactory sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
mybatisXMLLoader.xmlReload(sqlSessionFactory, jarFile, jarEntry, jarEntry.getName());
}
Jar jar = new Jar();
jar.setName(jarPath);
jar.setJarFile(jarFile);
jar.setLoader(classloader);
jar.setLoadedAliasClasses(loadedAliasClasses);
// 开始加载bean
registerBean(jar);
registry.registerJar(jarPath, jar);
} catch (Exception e) {
log.error(e.getLocalizedMessage());
return false;
}
return true;
}
通常bean注册过程
想要实现热加载,一定得了解在spring中类的加载机制,大体上spring在扫描到@Component注解的类时,会根据其class生成对应的BeanDefinition,然后在将其注册在BeanDefinitionRegistry(这是个接口,最终由DefaultListableBeanFactory实现)。当其备引用注入实例时即getBean时被实例化并被注册到DefaultSingletonBeanRegistry中。后续单例都将由DefaultSingletonBeanRegistry所管理。
controller加载
controller的加载机制
controller所特殊的是,spring会将其注册到RequestMappingHandlerMapping中。所以想要热加载controller 就需要三步。
- 生成并注册BeanDefinition
- 生成并注册实例
- 注册RequestMappingHandlerMapping
代码如下
// 获取bean工厂并转换为DefaultListableBeanFactory
DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) ((ConfigurableApplicationContext)
applicationContext).getBeanFactory();
// 定义BeanDefinition
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
GenericBeanDefinition beanDefinition = (GenericBeanDefinition) beanDefinitionBuilder.getRawBeanDefinition();
//设置当前bean定义对象是单利的
beanDefinition.setScope("singleton");
// 将变量首字母置小写
beanName = StringUtils.uncapitalize(beanName);
// 将构建的BeanDefinition交由Spring管理
beanFactory.registerBeanDefinition(beanName, beanDefinition);
// 手动构建实例,并注入base service 防止卸载之后不再生成
Object obj = clazz.newInstance();
beanFactory.registerSingleton(beanName, obj);
log.info("register Singleton :" + beanName);
final RequestMappingHandlerMapping requestMappingHandlerMapping =
applicationContext.getBean(RequestMappingHandlerMapping.class);
if (requestMappingHandlerMapping != null) {
String handler = beanName;
Object controller = null;
try {
controller = applicationContext.getBean(handler);
} catch (Exception e) {
e.printStackTrace();
}
if (controller == null) {
return beanName;
}
// 注册Controller
Method method = requestMappingHandlerMapping.getClass().getSuperclass().getSuperclass().
getDeclaredMethod("detectHandlerMethods", Object.class);
// 将private改为可使用
method.setAccessible(true);
method.invoke(requestMappingHandlerMapping, handler);
}
关于IOC
其实只要注册BeanDefinition之后,你getBean的时候spring会自动帮你完成@Autowired @Resouce 以及构造方法的注入,这里我自己完成实例化是想完成一些业务上的处理,如自定义注入一些代理类。
关于AOP
这样写有一个弊端就是无法使用AOP,因为AOP是在getBean的时候三层缓存中完成代理的生成的,这里如果你要用这种方式注入可以参考spring源码,构建出来代理类再注入
service加载
service加载我这里直接将service对应的实现类实例化再加载进去就可以了,不需要什么特殊的处理,所以这里就不贴代码了,加载同controller的第一步
mapper加载
mapper的加载时最复杂的一部分,首先针mapper有两种,一种是纯Mapper接口文件的加载,一种是xml文件的加载。并且你需要分析本身Mybatis是如何加载的,这样才能完整的降mapper加载到内存中。这里我将步骤分解为以下几步
- 注册别名(主要是为了XML使用)
- 解析XML文件
- 解析Mapper接口,注册mapper并注册
注册别名
mybatis对于别名的管理是存在SqlSessionFactory的Configuration(这个对象很重要,mybatis加载的资源之类的都在这个对象中管理)对象的TypeAliasRegistry中。TypeAliasRegistry是使用HashMap来维护别名的,这里我们直接调用registerAliases方法就好
SqlSessionFactory sqlSessionFactory = applicationContext.getBean(SqlSessionFactory.class);
sqlSessionFactory.getConfiguration().getTypeAliasRegistry().registerAlias(beanName.replace("/", "."), clazz);
解析XML文件
解析XML文件其实比较简单只要调用XMLMapperBuilder来解析就好了,XMLMapperBuilder.parse方法会解析XML文件并注册resultMaps、sqlFragments、mappedStatements。但是这里需要注意一点,那就是你解析的时候需要判断一下把之前加载的数据需要删除掉,同理resultMaps、sqlFragments、mappedStatements这些数据都是在SqlSessionFactory的Configuration中维护的,我们只要通过反射取得这些对象然后修改就可以了,代码如下
/**
* 解析加载XML
*
* @param sqlSessionFactory
* @param jarFile jar对象
* @param jarEntry jar包中的XML对象
* @param name XML名称
* @throws IOException
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
public void xmlReload(SqlSessionFactory sqlSessionFactory, JarFile jarFile, JarEntry jarEntry, String name) throws IOException, NoSuchFieldException, IllegalAccessException {
// 2. 取得Configuration
Configuration targetConfiguration = sqlSessionFactory.getConfiguration();
Class<?> aClass = targetConfiguration.getClass();
if (targetConfiguration.getClass().getSimpleName().equals("MybatisConfiguration")) {
aClass = Configuration.class;
}
Set<String> loadedResources = (Set<String>) ObjectUtil.getFieldValue(targetConfiguration, aClass, "loadedResources");
loadedResources.remove(name);
// 3. 去掉之前加载的数据
Map<String, ResultMap> resultMaps = (Map<String, ResultMap>) ObjectUtil.getFieldValue(targetConfiguration, aClass, "resultMaps");
Map<String, XNode> sqlFragmentsMaps = (Map<String, XNode>) ObjectUtil.getFieldValue(targetConfiguration, aClass, "sqlFragments");
Map<String, MappedStatement> mappedStatementMaps = (Map<String, MappedStatement>) ObjectUtil.getFieldValue(targetConfiguration, aClass, "mappedStatements");
XPathParser parser = new XPathParser(jarFile.getInputStream(jarEntry), true, targetConfiguration.getVariables(), new XMLMapperEntityResolver());
XNode mapperXNode = parser.evalNode("/mapper");
List<XNode> resultMapNodes = mapperXNode.evalNodes("/mapper/resultMap");
String namespace = mapperXNode.getStringAttribute("namespace");
for (XNode xNode : resultMapNodes) {
String id = xNode.getStringAttribute("id", xNode.getValueBasedIdentifier());
resultMaps.remove(namespace + "." + id);
}
List<XNode> sqlNodes = mapperXNode.evalNodes("/mapper/sql");
for (XNode sqlNode : sqlNodes) {
String id = sqlNode.getStringAttribute("id", sqlNode.getValueBasedIdentifier());
sqlFragmentsMaps.remove(namespace + "." + id);
}
List<XNode> msNodes = mapperXNode.evalNodes("select|insert|update|delete");
for (XNode msNode : msNodes) {
String id = msNode.getStringAttribute("id", msNode.getValueBasedIdentifier());
mappedStatementMaps.remove(namespace + "." + id);
}
try {
// 4. 重新加载和解析被修改的 xml 文件
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(jarFile.getInputStream(jarEntry),
targetConfiguration, name, targetConfiguration.getSqlFragments());
xmlMapperBuilder.parse();
} catch (Exception e) {
log.error(e.getMessage(), e);
}
log.info("Parsed mapper file: '" + name + "'");
}
其他类加载
其他类加载就比较简单了,直接使用classloader将这些类load进去就好,如果是单例需要被spring管理的则registerBeanDefinition就可以了