jdk: 17
springboot:2.6.6
一、需求
正在运行的springboot程序,上传一个jar包,完成对已有接口的实现(更新),如果不用了随时可以卸载掉。并且插件内部可以使用主程序里边已有的各类bean。
二、实现思路
- 我们的springboot应用程序提前预留好了扩展接口,但是在应用程序内部并未做任何的实现,而是交由独立的插件来完成。
- jar上传之后,涉及到了jar的热加载,我们可以使用 java.net.URLClassLoader 来完成。加载完成之后,将实现类注册成为bean。
- 因为jar需要更新,所以我们每次是new URLClassLoader,并把之前的资源释放掉,防止OOM。同时移除旧的bean,再添加新的bean,完成插件更新的功能。
- 在new URLClassLoader的时候,需要指定他的父级ClassLoader,这一步很关键,这是为了让插件内部可以直接使用spring的各类bean。
三、实践
1.idea本地实践
1.1 插件接口
创建一个springboot应用程序,并在应用程序内部规定好插件的接口,但是不去做具体的实现。调用者通过获取bean的方式,来检查是否有可执行的插件,如果有就执行,没有则按照实际的业务自行处理。
import java.util.Map;
public interface IPluginService {
/**
* 安装插件
*/
void install();
/**
* 插件是否安装完成
*/
boolean installComplete();
/**
* 插件卸载
*/
void uninstall();
/**
* 获取插件的作者信息
*/
String getAuthor();
/**
* 获取插件的名称
*/
String getPluginName();
/**
* 获取插件的版本号
*/
String getVersion();
/**
* 插件的具体描述
*/
String getDescription();
default Object processIfComplete(Map<String, Object> params) throws Exception {
if (!installComplete()) {
throw new RuntimeException(getPluginName() + " 插件尚未安装完成!");
}
return process(params);
}
Object process(Map<String, Object> params) throws Exception;
}
1.2 创建ClassLoader,并加载插件
上传jar之后,需要使用URLClassLoader来对其进行加载。这里的父级ClassLoader可以使用JDK提供的 ClassLoader.getSystemClassLoader() 来获取,本地启动一个springboot项目后就可以看到实际获取的其实就是AppClassLoader,并且spring的几千上万个类基本都是由他来加载的,所以将其指定为父级之后,我们加载进来的插件就可以使用主应用里边的bean了。
接着我们通过new URLClassLoader结合 java.net.URL 加载自己的jar文件。
// 这里我们直接从本地获取,实际可能来自本地文件/网络等等地方
URL url = new URL("jar:file:/home/zhangsan/plugins/send-message.jar!/");
URLClassLoader classLoader = new URLClassLoader(new URL[]{url}, ClassLoader.getSystemClassLoader());
// 我们规定实现类的全限定类名就是 com.zhangsan.plugin.SendMessagePlugin,通过loadClass获取到他,然后注册成为bean
Class<?> clazz = classLoader.loadClass("com.zhangsan.plugin.SendMessagePlugin");
springUtil.registerBean(clazz.getName(), clazz);
// 注册完成之后,执行插件的安装操作
IPluginService pluginService = (IPluginService) springUtil.getBean("com.zhangsan.plugin.SendMessagePlugin");
pluginService.install();
assert pluginService.installComplete();
1.3 卸载插件
URLClassLoader提供了close()方法,可以用来关闭当前ClassLoader,但是已经被其加载的class不会被这个操作所释放掉,具体可以查阅其源码的注释。
// 关闭我们创建的ClassLoader
classLoader.close();
// 执行插件的卸载操作
pluginService.uninstall();
// 最后将该bean移除
springUtil.removeBean("com.zhangsan.plugin.SendMessagePlugin");
1.4 更新插件
其实就是先卸载插件,再次安装即可。
1.5 制作插件
我们已经完成了springboot主体应用的开发,然后我们新建一个项目,这个项目里边引入了springboot项目打包好的jar文件,从而完成插件的开发操作。插件代码编写完成之后,将其打包一下就成为了我们可以安装的插件。
import java.util.Map;
public class SendMessagePlugin implements IPluginService {
@Override
public void install() {
System.out.println(getVersion() + "插件安装了");
}
@Override
public boolean installComplete() {
return true;
}
@Override
public void uninstall() {
System.out.println("插件卸载了");
}
@Override
public String getAuthor() {
return "张三";
}
@Override
public String getPluginName() {
return "发送消息插件";
}
@Override
public String getVersion() {
return "999999";
}
@Override
public String getDescription() {
return "发送短消息的插件";
}
@Override
public Object process(Map<String, Object> map) throws Exception {
System.err.println("版本为" + getVersion() + "的插件执行了");
return null;
}
}
1.6 本地idea测试
启动springboot应用,将打包好的jar放到 /home/zhangsan/plugins/send-message.jar 这里(你也可以写个接口去上传,模拟实际的情况),然后我们来触发一下应用程序的插件安装操作,在控制台可以看见,插件被成功的安装了。
接着试试看卸载
最后,我们把插件版本修改一下,变成666666,然后打包之后,放到 /home/zhangsan/plugins/send-message.jar ,再次执行插件安装。
1.7 测试结束
至此,在本地idea环境下,已然完全实现了我们的整体需求,插件可以在程序运行阶段正常的进行安装、卸载、更新操作,从而完成功能的扩展。
2.线上验证
我们通过maven将springboot项目打包成为jar,并以java -jar方式在测试服务器上运行起来。我们可以使用命令 java -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar xxxxx.jar 运行,方便debug调试。
2.1 安装插件
这里我们通过接口上传jar,为jar创建一个临时文件,文件名每次都保证相同为send-message.jar,然后依然正常执行后续的插件安装操作,这个时候我们会发现报错了。
org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.NoClassDefFoundError: com/zhangsan/plugin/IPluginService
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1082) ~[spring-webmvc-5.3.18.jar!/:5.3.18]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963) ~[spring-webmvc-5.3.18.jar!/:5.3.18]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.3.18.jar!/:5.3.18]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.3.18.jar!/:5.3.18]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:655) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.3.18.jar!/:5.3.18]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:764) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:227) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.60.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.3.18.jar!/:5.3.18]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.18.jar!/:5.3.18]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.3.18.jar!/:5.3.18]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.18.jar!/:5.3.18]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter.doFilterInternal(WebMvcMetricsFilter.java:96) ~[spring-boot-actuator-2.6.6.jar!/:2.6.6]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.18.jar!/:5.3.18]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.3.18.jar!/:5.3.18]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117) ~[spring-web-5.3.18.jar!/:5.3.18]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:189) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:162) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:197) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:135) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:360) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:399) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:889) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1743) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-9.0.60.jar!/:na]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-9.0.60.jar!/:na]
at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]
Caused by: java.lang.NoClassDefFoundError: com/zhangsan/plugin/IPluginService
at java.base/java.lang.ClassLoader.defineClass1(Native Method) ~[na:na]
at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1012) ~[na:na]
at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:150) ~[na:na]
at java.base/java.net.URLClassLoader.defineClass(URLClassLoader.java:524) ~[na:na]
at java.base/java.net.URLClassLoader$1.run(URLClassLoader.java:427) ~[na:na]
at java.base/java.net.URLClassLoader$1.run(URLClassLoader.java:421) ~[na:na]
at java.base/java.security.AccessController.doPrivileged(AccessController.java:712) ~[na:na]
at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:420) ~[na:na]
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:587) ~[na:na]
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520) ~[na:na]
at com.zhangsan.plugin.PluginRegisterService.install(PluginRegisterService.java:79) ~[classes!/:1.0-SNAPSHOT]
at com.zhangsan.controller.TestController.installPlugin(TestController.java:64) ~[classes!/:1.0-SNAPSHOT]
at com.zhangsan.controller.TestController$$FastClassBySpringCGLIB$$230d6e46.invoke(<generated>) ~[classes!/:1.0-SNAPSHOT]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.3.18.jar!/:5.3.18]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:783) ~[spring-aop-5.3.18.jar!/:5.3.18]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163) ~[spring-aop-5.3.18.jar!/:5.3.18]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.18.jar!/:5.3.18]
at org.springframework.aop.aspectj.AspectJAfterThrowingAdvice.invoke(AspectJAfterThrowingAdvice.java:64) ~[spring-aop-5.3.18.jar!/:5.3.18]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175) ~[spring-aop-5.3.18.jar!/:5.3.18]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.18.jar!/:5.3.18]
at org.springframework.aop.framework.adapter.AfterReturningAdviceInterceptor.invoke(AfterReturningAdviceInterceptor.java:57) ~[spring-aop-5.3.18.jar!/:5.3.18]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175) ~[spring-aop-5.3.18.jar!/:5.3.18]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.18.jar!/:5.3.18]
at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:58) ~[spring-aop-5.3.18.jar!/:5.3.18]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175) ~[spring-aop-5.3.18.jar!/:5.3.18]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.18.jar!/:5.3.18]
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97) ~[spring-aop-5.3.18.jar!/:5.3.18]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.3.18.jar!/:5.3.18]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:753) ~[spring-aop-5.3.18.jar!/:5.3.18]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:698) ~[spring-aop-5.3.18.jar!/:5.3.18]
at com.zhangsan.controller.TestController$$EnhancerBySpringCGLIB$$385afcc9.installPlugin(<generated>) ~[classes!/:1.0-SNAPSHOT]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:568) ~[na:na]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:205) ~[spring-web-5.3.18.jar!/:5.3.18]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:150) ~[spring-web-5.3.18.jar!/:5.3.18]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-5.3.18.jar!/:5.3.18]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895) ~[spring-webmvc-5.3.18.jar!/:5.3.18]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808) ~[spring-webmvc-5.3.18.jar!/:5.3.18]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.3.18.jar!/:5.3.18]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1067) ~[spring-webmvc-5.3.18.jar!/:5.3.18]
... 43 common frames omitted
Caused by: java.lang.ClassNotFoundException: com.zhangsan.plugin.IPluginService
at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:445) ~[na:na]
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:587) ~[na:na]
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:520) ~[na:na]
... 85 common frames omitted
这里最后显示 Caused by: java.lang.ClassNotFoundException: com.zhangsan.plugin.IPluginService ,结合前面的堆栈信息,可以看到是在loadClass的时候找不到这个类。也就是说,我们new出来的URLClassLoader,在加载我们插件里边那个实现类的时候找不到插件的接口了。
还记得前面的 ClassLoader.getSystemClassLoader() 么,我们现在再次来debug一下他,只不过现在是在测试服务器上远程debug了,可以看到如下的信息:
这里的AppClassLoader,仅仅加载了47个类,前边debug的时候可是加载了5000多个类。并且在已有的47个类里边,是找不到我们的 com.zhangsan.plugin.IPluginService。也就是说这个class是被别的ClassLoader加载了!
如果熟悉springboot启动的小伙伴,应该知道这是什么原因了。服务器上我们是通过java -jar方式启动的,通过压缩文件打开我们运行的jar包,在里边找到 META-INF/MANIFEST.MF 文件,打开它就可以看到如下的信息:
其实它并不是直接通过我们项目里边的Application来启动的,而是通过spring自己的JarLauncher来启动的,打开JarLauncher源码找到main方法,然后进入launch方法,在这里我们可以看到spring自己创建了ClassLoader:
继续跟入之后可以看到,实际上就是spring自己的LaunchedURLClassLoader,他也实现了URLClassLoader,并且重写了loadClass方法:
2.2 替换父级ClassLoader
spring提供了 ClassUtils.getDefaultClassLoader() 可以用来获取到LaunchedURLClassLoader(里边逻辑不做过多解释,自行参阅源码),所以回到最开始我们new URLClassLoader的地方,把这里获取父级ClassLoader替换即可。
注:tomcat自己的ClassLoader对这里是没有影响的,不需要去关注它。
2.3 再次验证
首先是本地,安装、卸载、更新都没问题。其实通过idea本地运行的时候,ClassUtils.getDefaultClassLoader() 和 ClassLoader.getSystemClassLoader() 都是获取到了AppClassLoader,可以自行查阅源码逻辑并且debug看一下。
接着测试服务器验证,安装、卸载一切正常,但是更新的时候出问题了!出现了很玄学的问题:
- 上传了新的插件,卸载和安装操作都在正常执行,但是观察到了两种情况,一个是更新成功没有任何问题,一个是更新失败,插件还是旧的插件;
- 更新失败时候是这样的现象:从执行插件install时候的输出来看,执行的还是最开始安装那个插件的install!而不是本次上传的;
- 偶尔一次可以更新成功,输出都正常。但是继续更新还是存在失败的情况,并且失败时候install输出的内容,就是刚才更新成功时候的内容。
这是缓存?而且还是带有一定删除或过期机制的缓存?假如是过期,那么一般情况下他的过期时间应该比较的固定,但是实际测试下来并不是,有时候几十秒内连续五六个插件都可以更新成功,有时候两三分钟都不见得能够更新成功一次!
虽然插件都是保存到本地固定的 /home/zhangsan/plugins/send-message.jar 下,但是每次卸载的时候都是将其删除掉,然后安装时候重新写入的,所以这个文件每次都是最新的。
2.4 org.springframework.boot.loader.jar.Handler
我们先简单介绍一下org.springframework.boot.loader.jar.Handler。
还记得刚才spring启动的那张截图吗?我们再来看一下:
这次的重点不再是ClassLoader了,而是 JarFile.registerUrlProtocolHandler(); 。我们跟进去:
在这里,他把 org.springframework.boot.loader.jar.Handler 注册了进来,熟悉这玩意的小伙伴应该知道,他就是spring自己的URL处理器,它还支持jar in jar的加载等等。
2.5 检查jar资源的加载逻辑
我们自己创建的URLClassLoader父级是LaunchedURLClassLoader(tomcat的可以不去管它),LaunchedURLClassLoader的父级就是AppClassLoader。他们完整关系应该是这样的:
自己的URLClassLoader -> LaunchedURLClassLoader -> AppClassLoader -> PlatformClassLoader -> BootClassLoader
我们自己的URLClassLoader在加载jar资源的时候,通过双亲委派给父级,其实几个父级都是加载不了我们自己的jar(自行debug一下),那么最后还是回到了我们自己的URLClassLoader,从loadClass的逻辑我们可以知道,由于父级都加载不了,并且我们自己的URLClassLoader本地也没有这个class的时候,会执行到findClass方法:
而URLClassLoader是重写了findClass方法的:
在这里我们可以看到,findClass会通过 jdk.internal.loader.URLClassPath 去获取相关的资源,我们继续跟进去,发现这里有一个getResource的操作:
继续跟进,可以发现这里有一个打开资源的操作:
跟进
跟进
这个时候我们发现,已经进入了 org.springframework.boot.loader.jar.Handler 里边(上边2.4的简单介绍),也就是说我们jar文件资源的加载是通过他来完成的,其实继续跟进下去就可以看到问题的根源所在:
他就是在这里做了缓存!还记得我们插件放置的位置么,每次都是 /home/zhangsan/plugins/send-message.jar , 所以更新的时候,他会直接取缓存,而不是去加载我们新的jar文件,也就导致更新的时候,还是执行最开始的install,因为加载的jar文件还是以前的!
但是前边说了,还有更新成功的情况,这是怎么回事呢?找到这个缓存变量的定义:
他是一个软引用!!!!所以才导致了时而成功时而失败的情况!
2.6 解决
至此,问题根源找到了,那么就来解决它。
- 方式一:最简单粗暴,既然是文件名(路径)相同导致走了缓存,那么我每次换一个文件名就行了;而且这个缓存是软引用,也无需担心更新jar次数过多导致内存撑爆的问题!
- 方式二:可以自己实现一个ClassLoader,然后自己加载这个jar,也就不存在缓存的问题了。参考一下LaunchedURLClassLoader的实现,我们可以继承URLClassLoader,然后重写loadClass和findClass方法,在loadClass的时候,如果是自己插件规范里边的类,就先本地加载,否则交给父级。
方式二可以参考如下代码:
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
@Slf4j
public class PluginClassLoader extends URLClassLoader {
private static final int BUFFER_SIZE = 4096;
private final static String CLASS_SUFFIX = ".class";
private final Map<String, byte[]> classMap = new HashMap<>();
public PluginClassLoader(URL url, ClassLoader parent) throws IOException {
super(new URL[]{url}, parent);
parseJar(url);
}
/**
* 将jar文件里边的class都解析出来缓存到本地
*/
private void parseJar(URL url) throws IOException {
String path = url.getPath();
path = path.substring(5, path.length() - 2);
JarFile jarFile = new JarFile(path);
//解析jar包每一项
Enumeration<JarEntry> en = jarFile.entries();
InputStream input = null;
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
while (en.hasMoreElements()) {
JarEntry jarEntry = en.nextElement();
String name = jarEntry.getName();
if (name.endsWith(CLASS_SUFFIX)) {
String className = name.replace(CLASS_SUFFIX, "").replaceAll("/", ".");
input = jarFile.getInputStream(jarEntry);
byte[] buffer = new byte[BUFFER_SIZE];
int index = 0;
while ((index = input.read(buffer)) != -1) {
bos.write(buffer, 0, index);
}
classMap.put(className, bos.toByteArray());
bos.reset();
}
}
} catch (IOException e) {
log.error(e.getClass().getSimpleName() + " " + e.getMessage(), e);
} finally {
if (input != null) {
try {
input.close();
} catch (IOException ignore) {
}
}
}
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 改变加载逻辑,这里只要是 com.zhangsan.plugin 开头的,都是我们插件规范里边的类,直接加载本地的class
if (name.startsWith("com.zhangsan.plugin.")) {
try {
return loadClassInLaunchedClassLoader(name);
} catch (ClassNotFoundException ignore) {
// 加载不了的,应该交由父类去加载
}
}
// 其余的类还是交由父类去完成
return super.loadClass(name);
}
private Class<?> loadClassInLaunchedClassLoader(String name) throws ClassNotFoundException {
byte[] classBytes = classMap.get(name);
if (classBytes == null) {
throw new ClassNotFoundException(name);
}
try (ByteArrayInputStream inputStream = new ByteArrayInputStream(classBytes);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[BUFFER_SIZE];
int bytesRead = -1;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
byte[] bytes = outputStream.toByteArray();
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", e);
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String className = name.replace(CLASS_SUFFIX, "").replaceAll("/", ".");
byte[] bytes = classMap.get(className);
if (bytes == null) {
throw new ClassNotFoundException(name);
}
return this.defineClass(name, bytes, 0, bytes.length);
}
}
在加载插件那里,创建类加载器的时候,我们就可以直接使用我们自定义的ClassLoader,同样可以解决问题。
2.7 测试
通过上诉方式,无论本地idea还是测试服务器都正常的完成了我们需要的功能,并且插件内部是可以直接注入springboot应用程序里边已有的bean。
四、问题
已经加载的类是会被卸载?如果无法被卸载是可能导致oom的。
其实JVM是会卸载类的,打开jdk提供的工具jconsole,选择Classes这一页,左下边是可以看到unload的类有多少个:
但是一个类是否会被卸载,需要满足的条件还是蛮多的:
- 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例;
- 加载该类的ClassLoader已经被回收;
- 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
五、总结
通过创建自己的URLCLassLoaderr,并指定正确的父级之后,便可以在插件内部使用各种bean,开发插件就和正常开发没有太多的区别。从而在运行阶段,动态的扩展程序的功能。
应用场景:其实对于toC的应用而言,应该是没有这类需求的,这种插件主要是针对私有化部署的应用。部署到客户这里之后难免存在对接、定制的情况,通常可以选择去修改源代码,但这是最不希望看到的一件事情。所以在程序设计阶段,可以将一些变化比较大,不是那么核心的业务预留好接口,交由插件去实现。
个人能力有限,如有错误之处,还望指出!