我正在参加「掘金·启航计划」
0. 插件化部署
插件化部署:spring-boot 服务启动后,不停止服务,对服务的功能进行模块化的新增、更新;热插拔的形式提供功能集成;
本文内容:插件化部署原理简介和项目demo;
1. 运行原理
- 使用
自定义类加载器加载我们需要的类; - 使用
spring管理自定义类加载器的生命周期;
核心代码
类加载器:
@Slf4j
public class HotClassLoader extends URLClassLoader {
/**
* <pre>
* jar 包更新时间记录,避免重复下载
* key : jar 绝对路径
* value : jar 更新时间,0-正在加载,>0 上一次更新时间
* </pre>
*
*/
private static final Map<String,Long> jarUpdateTimeMap;
private static final Map<String,List<String>> jarClassNameMap;
static {
jarUpdateTimeMap = new HashMap<>();
jarClassNameMap = new HashMap<>();
}
private SpringInjectService springInjectService;
public HotClassLoader(ClassLoader parent) {
super(new URL[0], parent);
}
public HotClassLoader(SpringInjectService springInjectService, ClassLoader parent) {
super(new URL[0], parent);
this.springInjectService = springInjectService;
}
public void loadJar(String jarPath) {
Long lastModifyTime = jarUpdateTimeMap.get(jarPath);
if(Objects.equals(lastModifyTime,0L)){
log.warn("HotClassLoader.loadJar loading ,please not repeat the operation, jarPath = {}", jarPath);
return;
}
// 1. 将jar 包加载到JVM
File file = new File(jarPath);
if (!file.exists()) {
log.warn("HotClassLoader.loadJar fail file not exist, jarPath = {}", jarPath);
return;
}
long currentJarModifyTime = file.getAbsoluteFile().lastModified();
if(Objects.equals(lastModifyTime,currentJarModifyTime)){
log.warn("HotClassLoader.loadJar current version has bean loaded , jarPath = {}", jarPath);
return;
}
try {
super.addURL(file.toURI().toURL());
} catch (MalformedURLException e) {
throw new PluginException("通过url 添加 jar 是失败");
}
// 记录jar 加载时间
jarUpdateTimeMap.put(jarPath, 0L);
List<String> classNameList = new ArrayList<>();
// 2. 遍历jar 包中的类
try (JarFile jarFile = new JarFile(jarPath)) {
List<JarEntry> jarEntryList = jarFile.stream().sequential().collect(Collectors.toList());
for (JarEntry loopJar : jarEntryList) {
// 2.1 将class 类加载到JVM
String jarName = loopJar.getName();
if (!jarName.endsWith(".class")) {
continue;
}
String className = jarName.replace(".class", "").replace("/", ".");
// 2.2 将class 注册到spring容器
boolean beanExist = springInjectService.containsBean(className);
if(beanExist){
springInjectService.removeBean(className);
}
Class<?> clazz = loadClass(className, false);
springInjectService.registerBean(className, clazz);
classNameList.add(className);
}
} catch (IOException | ClassNotFoundException e) {
throw new PluginException("jar包解析失败");
}
// 记录jar包中的 class 文件相对路径
jarClassNameMap.put(jarPath, classNameList);
// 记录jar 文件的更新时间
jarUpdateTimeMap.put(jarPath, currentJarModifyTime);
}
public void unloadJar(String jarPath) {
// 1. 校验文件是否存在
File file = new File(jarPath);
if (!file.exists()) {
log.warn("HotClassLoader.loadJar fail file not exist, jarPath = {}", jarPath);
return;
}
List<String> classNameList = jarClassNameMap.get(jarPath);
if(CollectionUtils.isEmpty(classNameList)){
log.warn("HotClassLoader.loadJar fail,the jar no class, jarPath = {}", jarPath);
return;
}
// 2.1 遍历移除spring中对应的bean, 关闭类加载器,移除引用
for (String loopClassName : classNameList) {
boolean beanExist = springInjectService.containsBean(loopClassName);
if(beanExist){
springInjectService.removeBean(loopClassName);
}
}
// 2.2 关闭 classloader
try {
close();
} catch (IOException e) {
throw new PluginException("HotClassLoader 加载失败");
}
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if(name.startsWith("java.")){
return ClassLoader.getSystemClassLoader().loadClass(name);
}
Class<?> clazz = findLoadedClass(name);
if (clazz != null) {
if (resolve) {
return loadClass(name);
}
return clazz;
}
return super.loadClass(name, resolve);
}
}
spring管理帮助服务
@Slf4j
@Component
public class SpringInjectService implements ApplicationContextAware {
private DefaultListableBeanFactory defaultListableBeanFactory;
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
ConfigurableApplicationContext configurableApplicationContext = (ConfigurableApplicationContext) applicationContext;
this.defaultListableBeanFactory = (DefaultListableBeanFactory) configurableApplicationContext.getBeanFactory();
}
/**
* 注册bean到spring容器中
*
* @param beanName 名称
* @param clazz class
*/
public void registerBean(String beanName, Class<?> clazz) {
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getRawBeanDefinition();
beanDefinitionBuilder.setScope(ConfigurableBeanFactory.SCOPE_SINGLETON);
// 注册bean
defaultListableBeanFactory.registerBeanDefinition(beanName, beanDefinition);
}
public <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
public <T> T getBean(String name, Class<T> clazz) {
return applicationContext.getBean(name, clazz);
}
public boolean containsBean(String name) {
return applicationContext.containsBean(name);
}
public void removeBean(String name){
defaultListableBeanFactory.removeBeanDefinition(name);
}
}
spring配置类
@Configuration
public class HotClassLoaderAutoConfigure {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public HotClassLoader hotClassLoader(SpringInjectService springInjectService) {
return new HotClassLoader(springInjectService,this.getClass().getClassLoader());
}
}
2.项目介绍
简单来讲,如下:
- 1)spring-boot 项目发布成功后;若有新的功能需要添加,直接将新功能打包成jar,放到指定目录,
- 2)然后加载,即可完成新功能的添加;
demo如何运行
- 1)查看:husky-use 项目查看,只有接口没有实现类;
- 2)我们先启动项目,访问:http://localhost:8080/eat ,发现如下异常;
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'com.xiaoyuxx.intf.impl.EatPluginImpl' available
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:874) ~[spring-beans-5.3.22.jar:5.3.22]
at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1344) ~[spring-beans-5.3.22.jar:5.3.22]
- 3)将 husky-eat-plugin打包成jar
# 运行,得到:husky-eat-plugin-1.0-SNAPSHOT.jar
mvn clean package,
- 4)将husky-eat-plugin-1.0-SNAPSHOT.jar放到husky-base/plugin目录下
- 5)加载jar,访问:http://localhost:8080/load/,结果如下:
load success
you eat wtf morning tea
- 7)测试插件卸载功能
修改husky-eat-plugin eat 方法返回值为:morning you eat wtf tea
-
9)重复3、4、5、6 步骤,结果如下:
morning you eat wtf tea