很久之前,为了诊断线上的问题,就想要是能有工具可以在线上出问题的时候,放个诊断包进去马上生效,就能看到线上问题的所在,那该是多么舒服的事情。后来慢慢的切换到java领域后,这种理想也变成了现实,小如IDEA中更改页面就能马上生效,大如利用Althas工具进行线上数据诊断,可谓是信手拈来,极大的方便了开发和诊断。后来深入研究之后,就慢慢的不满足框架本身带来的便利了,造轮子的想法慢慢在脑中挥之不去,这也是本文产生的原因了。接下来,你无需准备任何前置知识,因为我已经为你准备好了ClassLoader甜点,Javassist配菜,JavaAgent高汤,手写插件加载器框架主食,外加SPI知识做调料,且让我们整理餐具,开始这一道颇有点特色的吃播旅程吧。,开始前,让我们先聊聊双亲委派这个话题,因为无论是做热部署,还是做字节码增强,甚至于日常的编码,这都是绕不开的一个话题。先看如下图示:,从如上图示,我们可以看到双亲委派模型整体的工作方式,整体讲解如下:,类加载器的findClass(loadClass)被调用,通过上面的整体流程描述,是不是感觉双亲委派机制也不是那么难理解。本质就是先查缓存,缓存中没有就委托给父加载器查询缓存,直至查到Bootstrap加载器,如果Bootstrap加载器在缓存中也找不到,就抛错,然后这个错误再被一层层的捕捉,捕捉到错误后就查自己的类搜索路径,然后层层处理。,了解了双亲委派机制后,那么如果要实现类的热更换或者是jar的热部署,就不得不涉及到自定义ClassLoader了,实际上其本质依旧是利用ClassLoader的这种双亲委派机制来进行操作的。遵循上面的流程,我们很容易的来实现利用自定义的ClassLoader来实现类的热交换功能:,这里需要注意的是,在自定义的类加载器中,我们可以覆写findClass,然后利用defineClass加载类并返回。,上面这段代码,我们就实现了一个最简单的自定义类加载器,但是能映射出双亲委派模型呢?,首先点开ClassLoader类,在里面翻到这个方法:,如果对比着双亲委派模型来看,则loadClass方法对应之前提到的步骤1-8,点进去findLoadedClass方法,可以看到底层实现是native的native final Class<?> findLoadedClass0 方法,这个方法会从JVM缓存中进行数据查找。后面的分析方法类似。,而自定义类加载器中的findClass方法,则对应步骤9:,看看,整体是不是很清晰?,写完自定义类加载器,来看看具体的用法吧,我们创建一个类,拥有如下内容:,顾名思义,此类只要调用sayHello方法,便会打印出hello world22222! (version 11)出来。,热交换处理过程如下:,当我们运行起来后,我们会将提前准备好的另一个Foo.class来替换当前这个,来看看结果吧(直接将新的Foo.class类拷贝过去覆盖即可):,可以看到,当我们替换掉原来运行的类的时候,输出也就变了,变成了新类的输出结果。整体类的热交换成功。,不知道我们注意到一个细节没有,在上述代码中,我们先创建出Object的类对象,然后利用Method.invoke方法来调用类:,有人在这里会疑惑,为啥不直接转换为Foo类,然后调用类的Foo.sayHello方法呢?像下面这种方式:,这种方式是不行的,但是大家知道为啥不行吗?,我们知道,我们写的类,一般都是被AppClassloader加载的,也就是说,你写在main启动类中的所有类,只要你写出来,那么就会被AppClassloader加载,所以,如果这里我们强转为Foo类型,那铁定是会被AppClassloader加载的,但是由于我们的clazz对象是由CustomerClassloader加载的,所以这里就会出现这样的错误:,那有什么方法可以解决这个问题吗?其实是有的,就是对Foo对象抽象出一个Interface,比如说IFoo,然后转换的时候,转换成接口,就不会有这种问题了:,通过接口这种方式,我们就很容易对运行中的组件进行类的热交换了,属实方便。,需要注意的是,主线程的类加载器,一般都是AppClassLoader,但是当我们创建出子线程后,其类加载器都会继承自其创建者的类加载器,但是在某些业务中,我想在子线程中使用自己的类加载器,有什么办法吗?其实这里也就是打断双亲委派机制。,由于Thread对象中已经附带了ContextClassLoader属性,所以这里我们可以很方便的进行设置和获取:,说完基于自定义ClassLoader来进行类的热交换后,我们再来说说Java中的SPI。说到SPI相信大家都听过,因为在java中天生集成,其内部机制也是利用了自定义的类加载器,然后进行了良好的封装暴露给用户,具体的源码大家可以自定翻阅ServiceLoader类。,这里我们写个简单的例子:,然后我们基于接口的包名+类名作为路径,创建出com.tinywhale.deploy.spi.HelloService文件到resources中的META-INF.services文件夹,里面放入如下内容:,然后在启动类中运行:,可以看到,在启动类中,我们利用ServiceLoader类来遍历META-INF.services文件夹下面的provider,然后执行,则输出结果为两个类的输出结果。之后在执行过程中,我们去target文件夹中,将com.tinywhale.deploy.spi.HelloService文件中的NameServiceProvider注释掉,然后保存,就可以看到只有一个类的输出结果了。,这种基于SPI类的热交换,比自己自定义加载器更加简便,推荐使用。,上面讲解的内容,一般是类的热交换,但是如果我们需要对整个jar包进行热部署,该怎么做呢?虽然现在有很成熟的技术,比如OSGI等,但是这里我将从原理层面来讲解如何对Jar包进行热部署操作。,由于内置的URLClassLoader本身可以对jar进行操作,所以我们只需要自定义一个基于URLClassLoader的类加载器即可:,注意,我们打的jar包,最好打成fat jar,这样处理起来方便,不至于少打东西:,之后,我们就可以使用了:,启动起来,看下输出,之后用一个新的jar覆盖掉,来看看结果吧:,可以看到,jar包被自动替换了。当然,如果想卸载此包,我们可以调用如下语句进行卸载:,需要注意的是,jar包中不应有长时间运行的任务或者子线程等,因为调用类加载器的close方法后,会释放一些资源,但是长时间运行的任务并不会终止。所以这种情况下,如果你卸载了旧包,然后马上加载新包,且包中有长时间的任务,请确认做好业务防重,否则会引发不可知的业务问题。,由于Spring中已经有对jar包进行操作的类,我们可以配合上自己的annotation实现特定的功能,比如扩展点实现,插件实现,服务检测等等等等,用途非常广泛,大家可以自行发掘。,上面讲解的基本是原理部分,由于目前市面上有很多成熟的组件,比如OSGI等,已经实现了热部署热交换等的功能,所以很推荐大家去用一用。,说到这里,相信大家对类的热交换,jar的热部署应该有初步的概念了,但是这仅仅算是开胃小菜。由于热部署一般都是和字节码增强结合着来用的,所以这里我们先来大致熟悉一下Java Agent技术。,话说在JDK中,一直有一个比较重要的jar包,名称为rt.jar,他是java运行时环境中,最核心和最底层的类库的来源。比如java.lang.String, java.lang.Thread, java.util.ArrayList等均来源于这个类库。今天我们所要讲解的角色是rt.jar中的java.lang.instrument包,此包提供的功能,可以让我们在运行时环境中动态的修改系统中的类,而Java Agent作为其中一个重要的组件,极具特色。,现在我们有个场景,比如说,每次请求过来,我都想把jvm数据信息或者调用量上报上来,由于应用已经上线,无法更改代码了,那么有什么办法来实现吗?当然有,这也是Java Agent最擅长的场合,当然也不仅仅只有这种场合,诸如大名鼎鼎的热部署JRebel,阿里的arthas,线上诊断工具btrace,UT覆盖工具JaCoCo等,不一而足。,在使用Java Agent前,我们需要了解其两个重要的方法:,还有个必不可少的东西是MANIFEST.MF文件,此文件需要放置到resources/META-INF文件夹下,此文件一般包含如下内容:,在对jar进行打包的时候,最好打成fat jar,可以减少很多不必要的麻烦,maven加入如下打包内容:,而MF配置文件,可以利用如下的maven内容进行自动生成:,工欲善其事必先利其器,准备好了之后,先来手写个Java Agent尝鲜吧,模拟premain调用,main调用和agentmain调用。,首先是premain调用类 ,agentmain调用类,main调用类:,可以看到,逻辑很简单,输出了方法执行体中打印的内容。之后编译jar包,则会生成fat jar。需要注意的是,MANIFEST.MF文件需要手动创建下,里面加入如下内容:,由于代码是在IDEA中启动,所以想要执行premain,需要在App4a启动类上右击:Run App.main(),之后IDEA顶部会出现App的执行配置,我们需要点击Edit Configurations选项,然后在VM options中填入如下命令:,之后启动App,就可以看到输出结果了。注意这里最好用fat jar,减少出错的机率。,但是这里的话,我们看不到agentmain输出,是因为agentmain的运行,是需要进行attach的,这里我们对agentmain进行attach:,启动app后,得到的结果为:,可以看到,整个执行都被串起来了。,讲到这里,相信大家基本上理解java agent的执行顺序和配置了吧, premain执行需要配置-javaagent启动参数,而agentmain执行需要attach vm pid。,看到这里,相信对java agent已经有个初步的认识了吧。接下来,我们就基于Java SPI + Java Agent + Javassist来实现一个插件系统,这个插件系统比较特殊的地方,就是可以增强spring框架,使其路径自动注册到component-scan路径中,颇有点霸道(鸡贼)的意思。Javassist框架的使用方式。,首先来说下这个框架的主体思路,使用Java SPI来做插件系统;使用Java Agent来使得插件可以在main主入口方法前或者是方法后执行;使用Javassist框架来进行字节码增强,即实现对spring框架的增强。,针对插件部分,我们可以定义公共的接口契约:,然后针对premain和agentmain,利用策略模式进行组装如下:,premain处理策略类,agentmain处理策略类,针对premain和agentmain,执行器工厂如下:,编写Premain-Class和Agent-Class指定的类:,配置文件中指定相应的类:,框架搭好后,来编写插件部分,插件的话,需要继承自org.tiny.upgrade.sdk.IPluginService并实现:,这里需要注意的是,在插件load的时候,我们做了class retransform操作,这样操作的原因是因为,在程序启动的时候,有时候比如一些类,会在JavaAgent之前启动,这样会造成有些类在进行增强的时候,无法处理,所以这里需要遍历并操作下,避免意外情况。,下面是具体的增强操作:,从上面可以看出,我们是修改了spring中的ComponentScanBeanDefinitionParser类,并将里面的parser方法中将org.tiny.upgrade包扫描路径自动注册进去,这样当别人集成我们的框架的时候,就无须扫描到框架也能执行了。,写到这里,相信大家对整体框架有个大概的认识了。但是这个框架有个缺陷,就是我的插件jar写完后,一定要放到项目的maven dependency中,然后打包部署才行。实际上有时候,我项目上线后,根本就没有机会重新打包部署,那么接下来,我们就通过自定义Classloader来让我们的插件不仅仅可以本地集成,而且可以从网络中集成。,首先,我们需要定义自定义类加载器:,这个类加载器,是不是很眼熟,和前面讲的类似,但是带了个parent classloader的标记,这是为什么呢?这个标记的意思是,当前自定义的TinyPluginClassLoader的父classloader是谁,这样的话,这个自定义类加载器就可以继承父类加载器中的信息了,避免出现问题,这个细节大家注意。,这里需要说明的是,从本地jar文件加载还是从网络jar文件加载,本质上是一样的,因为TinyPluginClassLoader是按照URL来的。,针对于本地jar文件,我们构造如下URL即可:,针对于网络jar文件,我们构造如下URL即可:,这样,我们只需要定义好自定义类加载器加载逻辑即可:,之后,我们就可以用如下代码对一个具体的jar路径进行加载就行了:,最终,我们只需要利用SPI进行动态加载:,这样,我们不仅实现了插件化,而且我们的插件还支持从本地jar文件或者网络jar文件加载。由于我们利用了agentmain对代码进行增强,所以当系统检测到我这个jar的时候,下一次执行会重新对代码进行增强并生效。,到这里,我们的用餐进入到尾声了。也不知道这餐,您享用的是否高兴?,其实本文的技术,从双亲委派模型到自定义类加载器,再到基于自定义类加载器实现的类交换,基于Java SPI实现的类交换,最后到基于Java SPI+ Java Agent + Javassist实现的插件框架及框架支持远程插件化,来一步一步的向读者展示所涉及的知识点。当然,由于笔者知识有限,疏漏之处,还望海涵,真诚期待我的抛砖,能够引出您的玉石之言。
© 版权声明
文章版权归作者所有,未经允许请勿转载。