很久之前,为了诊断线上的问题,就想要是能有工具可以在线上出问题的时候,放个诊断包进去马上生效,就能看到线上问题的所在,那该是多么舒服的事情。后来慢慢的切换到java领域后,这种理想也变成了现实,小如IDEA中更改页面就能马上生效,大如利用Althas工具进行线上数据诊断,可谓是信手拈来,极大的方便了开发和诊断。后来深入研究之后,就慢慢的不满足框架本身带来的便利了,造轮子的想法慢慢在脑中挥之不去,这也是本文产生的原因了。接下来,你无需准备任何前置知识,因为本文已经为你准备好了ClassLoader甜点,Javassist配菜,JavaAgent高汤,手写插件加载器框架主食,外加SPI知识做调料,且让我们整理餐具,开始这一道颇有点特色的吃播旅程吧。 01hr双亲委派模型 在今年的敏捷团队建设中,我通过Suite执行器实现了一键自动化单元测试。Juint除了Suite执行器还有哪些执行器呢?由此我的Runner探索之旅开始了! 开始前,先聊聊双亲委派这个话题,因为无论是做热部署,还是做字节码增强,甚至于日常的编码,这都是绕不开的一个话题。先看如下图示: 从如上图示,可以看到双亲委派模型整体的工作方式,整体讲解如下: 1。类加载器的findClass(loadClass)被调用 2。进入AppClassLoader中,先检查缓存中是否存在,如果存在,则直接返回 3。步骤2中的缓存中不存在,则被代理到父加载器,即ExtensionClassLoader 4。检查ExtensionClassLoader缓存中是否存在 5。步骤4中的缓存中不存在,则被代理到父加载器,即BootstrapClassLoader 6。检查BootstrapClassLoader缓存中是否存在 7。步骤6中的缓存中不存在,则从BootstrapClassLoader的类搜索路径下的文件中寻找,一般为rt。jar等,如果找不到,则抛出ClassNotFoundException 8。ExtensionClassLoader会捕捉ClassNotFound错误,然后从ExtensionClassLoader的类搜索路径下的文件中寻找,一般为环境变量JREHOMElibext路径下,如果也找不到,则抛出ClassNotFoundException 9。AppClassLoader会捕捉ClassNotFound错误,然后从AppClassLoader的类搜索路径下的文件中寻找,一般为环境变量CLASSPATH路径下,如果找到,则将其读入字节数组,如果也找不到,则抛出ClassNotFoundException。如果找到,则AppClassLoader调用defineClass()方法。 通过上面的整体流程描述,是不是感觉双亲委派机制也不是那么难理解。本质就是先查缓存,缓存中没有就委托给父加载器查询缓存,直至查到Bootstrap加载器,如果Bootstrap加载器在缓存中也找不到,就抛错,然后这个错误再被一层层的捕捉,捕捉到错误后就查自己的类搜索路径,然后层层处理。 02hr自定义ClassLoader 理解,首先MCube会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕 了解了双亲委派机制后,那么如果要实现类的热更换或者是jar的热部署,就不得不涉及到自定义ClassLoader了,实际上其本质依旧是利用ClassLoader的这种双亲委派机制来进行操作的。遵循上面的流程,可以很容易的来实现利用自定义的ClassLoader来实现类的热交换功能:publicclassCustomClassLoaderextendsClassLoader{需要该类加载器直接加载的类文件的基目录privateStringbaseDir;publicCustomClassLoader(StringbaseDir,String〔〕classes)throwsIOException{super();this。baseDirbaseDir;loadClassByMe(classes);}privatevoidloadClassByMe(String〔〕classes)throwsIOException{for(inti0;iclasses。length;i){findClass(classes〔i〕);}}重写findclass方法在ClassLoader中,loadClass方法先从缓存中找,缓存中没有,会代理给父类查找,如果父类中也找不到,就会调用此用户实现的findClass方法paramnamereturnOverrideprotectedClassfindClass(Stringname){Classclazznull;StringBufferstringBuffernewStringBuffer(baseDir);StringclassNamename。replace(。,File。separatorChar)。class;stringBuffer。append(File。separatorclassName);FileclassFnewFile(stringBuffer。toString());try{clazzinstantiateClass(name,newFileInputStream(classF),classF。length());}catch(IOExceptione){e。printStackTrace();}returnclazz;}privateClassinstantiateClass(Stringname,InputStreamfin,longlen)throwsIOException{byte〔〕rawnewbyte〔(int)len〕;fin。read(raw);fin。close();returndefineClass(name,raw,0,raw。length);}} 这里需要注意的是,在自定义的类加载器中,可以覆写findClass,然后利用defineClass加载类并返回。 上面这段代码,就实现了一个最简单的自定义类加载器,但是能映射出双亲委派模型呢? 首先点开ClassLoader类,在里面翻到这个方法:protectedClasslt;?loadClass(Stringname,booleanresolve)throwsClassNotFoundException{synchronized(getClassLoadingLock(name)){First,checkiftheclasshasalreadybeenloadedClasslt;?cfindLoadedClass(name);if(cnull){longt0System。nanoTime();try{if(parent!null){cparent。loadClass(name,false);}else{cfindBootstrapClassOrNull(name);}}catch(ClassNotFoundExceptione){ClassNotFoundExceptionthrownifclassnotfoundfromthenonnullparentclassloader}if(cnull){Ifstillnotfound,theninvokefindClassinordertofindtheclass。longt1System。nanoTime();cfindClass(name);thisisthedefiningclassloader;recordthestatssun。misc。PerfCounter。getParentDelegationTime()。addTime(t1t0);sun。misc。PerfCounter。getFindClassTime()。addElapsedTimeFrom(t1);sun。misc。PerfCounter。getFindClasses()。increment();}}if(resolve){resolveClass(c);}returnc;}} 如果对比着双亲委派模型来看,则loadClass方法对应之前提到的步骤18,点进去findLoadedClass方法,可以看到底层实现是native的nativefinalClasslt;?findLoadedClass0方法,这个方法会从JVM缓存中进行数据查找。后面的分析方法类似。 而自定义类加载器中的findClass方法,则对应步骤9:clazzinstantiateClass(name,newFileInputStream(classF),classF。length());省略部分逻辑returndefineClass(name,raw,0,raw。length); 看看,整体是不是很清晰? 03hr自定义类加载器实现类的热交换 理解,首先MCube会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。 写完自定义类加载器,来看看具体的用法吧,先创建一个类,拥有如下内容:packagecom。tw。client;publicclassFoo{publicFoo(){}publicvoidsayHello(){System。out。println(helloworld22222!(version11));}} 顾名思义,此类只要调用sayHello方法,便会打印出helloworld22222!(version11)出来。 热交换处理过程如下:publicstaticvoidmain(String〔〕args)throwsException{while(true){run();Thread。sleep(1000);}}ClassLoader用来加载class类文件的,实现类的热替换注意,需要在swap目录下,一层层建立目录comtwclient,然后将Foo。class放进去throwsExceptionpublicstaticvoidrun()throwsException{CustomClassLoadercustomClassLoadernewCustomClassLoader(swap,newString〔〕{com。tw。client。Foo});ClassclazzcustomClassLoader。loadClass(com。tw。client。Foo);Objectfooclazz。newInstance();Methodmethodfoo。getClass()。getMethod(sayHello,newClass〔〕{});method。invoke(foo,newObject〔〕{});} 当运行起来后,会将提前准备好的另一个Foo。class来替换当前这个,来看看结果吧(直接将新的Foo。class类拷贝过去覆盖即可):helloworld22222!(version11)helloworld22222!(version11)helloworld22222!(version11)helloworld22222!(version11)helloworld22222!(version11)helloworld2222!(version2)helloworld2222!(version2)helloworld2222!(version2)helloworld2222!(version2) 可以看到,当替换掉原来运行的类的时候,输出也就变了,变成了新类的输出结果。整体类的热交换成功。 不知道大家注意到一个细节没有,在上述代码中,先创建出Object的类对象然后利用Method。invoke方法来调用类:Objectfooclazz。newInstance();Methodmethodfoo。getClass()。getMethod(sayHello,newClass〔〕{});method。invoke(foo,newObject〔〕{}); 有人在这里会疑惑,为啥不直接转换为Foo类,然后调用类的Foo。sayHello方法呢?像下面这种方式:Foofoo2(Foo)clazz。newInstance();foo2。sayHello(); 这种方式是不行的,但是大家知道为啥不行吗? 大家都知道,我们写的类,一般都是被AppClassloader加载的,也就是说,你写在main启动类中的所有类,只要你写出来,那么就会被AppClassloader加载,所以,如果这里强转为Foo类型,那铁定是会被AppClassloader加载的,但是由于clazz对象是由CustomerClassloader加载的,所以这里就会出现这样的错误:java。lang。ClassCastException:com。tw。client。Foocannotbecasttocom。tw。client。Foo 那有什么方法可以解决这个问题吗?其实是有的,就是对Foo对象抽象出一个Interface,比如说IFoo,然后转换的时候,转换成接口,就不会有这种问题了:IFoofoo2(IFoo)clazz。newInstance();foo2。sayHello(); 通过接口这种方式,就很容易对运行中的组件进行类的热交换了,属实方便。 需要注意的是,主线程的类加载器,一般都是AppClassLoader,但是当创建出子线程后,其类加载器都会继承自其创建者的类加载器,但是在某些业务中,我想在子线程中使用自己的类加载器,有什么办法吗?其实这里也就是打断双亲委派机制。 由于Thread对象中已经附带了ContextClassLoader属性,所以这里可以很方便的进行设置和获取:设置操作ThreadtThread。currentThread();t。setContextClassLoader(loader);获取操作ThreadtThread。currentThread();ClassLoaderloadert。getContextClassLoader();Classlt;?clloader。loadClass(className); 04SPI实现类的热交换 说完基于自定义ClassLoader来进行类的热交换后,再来说说Java中的SPI。说到SPI相信大家都听过,因为在java中天生集成,其内部机制也是利用了自定义的类加载器,然后进行了良好的封装暴露给用户,具体的源码大家可以自定翻阅ServiceLoader类。 这里写个简单的例子:publicinterfaceHelloService{voidsayHello(Stringname);}publicclassHelloServiceProviderimplementsHelloService{OverridepublicvoidsayHello(Stringname){System。out。println(Helloname);}}publicclassNameServiceProviderimplementsHelloService{OverridepublicvoidsayHello(Stringname){System。out。println(Hi,yournameisname);}} 然后基于接口的包名类名作为路径,创建出com。tinywhale。deploy。spi。HelloService文件到resources中的METAINF。services文件夹,里面放入如下内容:com。tinywhale。deploy。spi。HelloServiceProvidercom。tinywhale。deploy。spi。NameServiceProvider 然后在启动类中运行:publicstaticvoidmain(String。。。args)throwsException{while(true){run();Thread。sleep(1000);}}privatestaticvoidrun(){ServiceLoaderHelloServiceserviceLoaderServiceLoader。load(HelloService。class);for(HelloServicehelloWorldService:serviceLoader){helloWorldService。sayHello(myname);}} 可以看到,在启动类中,利用ServiceLoader类来遍历METAINF。services文件夹下面的provider,然后执行,则输出结果为两个类的输出结果。之后在执行过程中,需要去target文件夹中,将com。tinywhale。deploy。spi。HelloService文件中的NameServiceProvider注释掉,然后保存,就可以看到只有一个类的输出结果了。HellomynameHi,yournameismynameHellomynameHi,yournameismynameHellomynameHi,yournameismynameHellomynameHellomynameHellomynameHellomyname 这种基于SPI类的热交换,比自己自定义加载器更加简便,推荐使用。 05hr自定义类加载器实现Jar热部署 理解,首先MCube会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。 上面讲解的内容,一般是类的热交换,但是如果需要对整个jar包进行热部署,该怎么做呢?虽然现在有很成熟的技术,比如OSGI等,但是这里本文将从原理层面来讲解如何对Jar包进行热部署操作。 由于内置的URLClassLoader本身可以对jar进行操作,所以只需要自定义一个基于URLClassLoader的类加载器即可:publicclassBizClassLoaderextendsURLClassLoader{publicBizClassLoader(URL〔〕urls){super(urls);}} 注意,这里打的jar包,最好打成fatjar,这样处理起来方便,不至于少打东西:plugingroupIdorg。apache。maven。pluginsgroupIdmavenshadepluginartifactIdversion2。4。3versionconfiguration!自动将所有不使用的类排除minimizeJartrueminimizeJarconfigurationexecutionsexecutionphasepackagephasegoalsgoalshadegoalgoalsconfigurationshadedArtifactAttachedtrueshadedArtifactAttachedshadedClassifierNamebizshadedClassifierNameconfigurationexecutionexecutionsplugin 之后,就可以使用了:publicstaticvoidmain(String。。。args)throwsException{while(true){loadJarFile();Thread。sleep(1000);}}URLClassLoader用来加载Jar文件,直接放在swap目录下即可动态改变jar中类,可以实现热加载throwsExceptionpublicstaticvoidloadJarFile()throwsException{FilemoduleFilenewFile(swaptinywhaleclient0。0。1SNAPSHOTbiz。jar);URLmoduleURLmoduleFile。toURI()。toURL();URL〔〕urlsnewURL〔〕{moduleURL};BizClassLoaderbizClassLoadernewBizClassLoader(urls);ClassclazzbizClassLoader。loadClass(com。tw。client。Bar);Objectfooclazz。newInstance();Methodmethodfoo。getClass()。getMethod(sayBar,newClass〔〕{});method。invoke(foo,newObject〔〕{});bizClassLoader。close();} 启动起来,看下输出,之后用一个新的jar覆盖掉,来看看结果吧:Iambar,Foossister,canyoucatchme?????????????Iambar,Foossister,canyoucatchme?????????????Iambar,Foossister,canyoucatchme!!!!Iambar,Foossister,canyoucatchme!!!!Iambar,Foossister,canyoucatchme!!!!Iambar,Foossister,canyoucatchme!!!! 可以看到,jar包被自动替换了。当然,如果想卸载此包,可以调用如下语句进行卸载:bizClassLoader。close(); 需要注意的是,jar包中不应有长时间运行的任务或者子线程等,因为调用类加载器的close方法后,会释放一些资源,但是长时间运行的任务并不会终止。所以这种情况下,如果你卸载了旧包,然后马上加载新包,且包中有长时间的任务,请确认做好业务防重,否则会引发不可知的业务问题。 由于Spring中已经有对jar包进行操作的类,可以配合上自己的annotation实现特定的功能,比如扩展点实现,插件实现,服务检测等等等等,用途非常广泛,大家可以自行发掘。 上面讲解的基本是原理部分,由于目前市面上有很多成熟的组件,比如OSGI等,已经实现了热部署热交换等的功能,所以很推荐大家去用一用。 说到这里,相信大家对类的热交换,jar的热部署应该有初步的概念了,但是这仅仅算是开胃小菜。由于热部署一般都是和字节码增强结合着来用的,所以这里先来大致熟悉一下JavaAgent技术。 06hr代码增强技术拾忆 理解,首先MCube会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。 话说在JDK中,一直有一个比较重要的jar包,名称为rt。jar,他是java运行时环境中,最核心和最底层的类库的来源。比如java。lang。String,java。lang。Thread,java。util。ArrayList等均来源于这个类库。今天要讲解的角色是rt。jar中的java。lang。instrument包,此包提供的功能,可以让我们在运行时环境中动态的修改系统中的类,而JavaAgent作为其中一个重要的组件,极具特色。 现在有个场景,比如说,每次请求过来,我都想把jvm数据信息或者调用量上报上来,由于应用已经上线,无法更改代码了,那么有什么办法来实现吗?当然有,这也是JavaAgent最擅长的场合,当然也不仅仅只有这种场合,诸如大名鼎鼎的热部署JRebel,阿里的arthas,线上诊断工具btrace,UT覆盖工具JaCoCo等,不一而足。 在使用JavaAgent前,需要了解其两个重要的方法:main方法执行之前执行,manifest需要配置属性PremainClass,参数配置方式载入publicstaticvoidpremain(StringagentArgs,Instrumentationinst);程序启动后执行,manifest需要配置属性AgentClass,Attach附加方式载入publicstaticvoidagentmain(StringagentArgs,Instrumentationinst); 还有个必不可少的东西是MANIFEST。MF文件,此文件需要放置到resourcesMETAINF文件夹下,此文件一般包含如下内容:Premainclass:main方法执行前执行的agent类。Agentclass:程序启动后执行的agent类。CanRedefineClasses:agent是否具有redifine类能力的开关,true表示可以,false表示不可以。CanRetransformClasses:agent是否具有retransform类能力的开关,true表示可以,false表示不可以。CanSetNativeMethodPrefix:agent是否具有生成本地方法前缀能力的开关,trie表示可以,false表示不可以。BootClassPath:此路径会被加入到BootstrapClassLoader的搜索路径。 在对jar进行打包的时候,最好打成fatjar,可以减少很多不必要的麻烦,maven加入如下打包内容:plugingroupIdorg。apache。maven。pluginsgroupIdmavenshadepluginartifactIdexecutionsexecutionphasepackagephasegoalsgoalshadegoalgoalsexecutionexecutionsplugin 而MF配置文件,可以利用如下的maven内容进行自动生成:plugingroupIdorg。apache。maven。pluginsgroupIdmavenjarpluginartifactIdversion3。2。0versionconfigurationmanifestFilesrcmainresourcesMETAINFMANIFEST。MFmanifestFilearchiveconfigurationplugin 工欲善其事必先利其器,准备好了之后,先来手写个JavaAgent尝鲜吧,模拟premain调用,main调用和agentmain调用。 首先是premain调用类,agentmain调用类,main调用类:main执行前调用publicclassAgentPre{publicstaticvoidpremain(StringagentArgs,Instrumentationinst){System。out。println(executepremainmethod);}}main主方法入口publicclassApp{publicstaticvoidmain(String。。。args)throwsException{System。out。println(executemainmethod);}}main执行后调用publicclassAgentMain{publicstaticvoidagentmain(StringagentArgs,Instrumentationinst){System。out。println(executeagentmainmethod);}} 可以看到,逻辑很简单,输出了方法执行体中打印的内容。之后编译jar包,则会生成fatjar。需要注意的是,MANIFEST。MF文件需要手动创建下,里面加入如下内容:ManifestVersion:1。0PremainClass:com。tinywhale。deploy。javaAgent。AgentPreAgentClass:com。tinywhale。deploy。javaAgent。AgentMain 由于代码是在IDEA中启动,所以想要执行premain,需要在App4a启动类上右击:RunApp。main(),之后IDEA顶部会出现App的执行配置,需要点击EditConfigurations选项,然后在VMoptions中填入如下命令:javaagent:D:appinywhaleinywhaledeployargetinywhaledeploy1。0SNAPSHOTbiz。jar 之后启动App,就可以看到输出结果了。注意这里最好用fatjar,减少出错的机率。executepremainmethodexecutemainmethod 但是这里看不到agentmain输出,是因为agentmain的运行,是需要进行attach的,这里对agentmain进行attach:publicclassApp{publicstaticvoidmain(String。。。args)throwsException{System。out。println(executemainmethod);attach();}privatestaticvoidattach(){FileagentFilePaths。get(D:apptinywhaletinywhaledeploytargettinywhaledeploy1。0SNAPSHOT。jar)。toFile();try{StringnameManagementFactory。getRuntimeMXBean()。getName();Stringpidname。split()〔0〕;VirtualMachinejvmVirtualMachine。attach(pid);jvm。loadAgent(agentFile。getAbsolutePath());}catch(Exceptione){System。out。println(e);}}} 启动app后,得到的结果为:executepremainmethodexecutemainmethodexecuteagentmainmethod 可以看到,整个执行都被串起来了。 讲到这里,相信大家基本上理解javaagent的执行顺序和配置了吧,premain执行需要配置javaagent启动参数,而agentmain执行需要attachvmpid。 看到这里,相信对javaagent已经有个初步的认识了吧。接下来就基于JavaSPIJavaAgentJavassist来实现一个插件系统,这个插件系统比较特殊的地方,就是可以增强spring框架,使其路径自动注册到componentscan路径中,颇有点霸道(鸡贼)的意思。Javassist框架的使用方式,本文这里不细细的展开,感兴趣的可以看翻译的中文版:javassist中文技术文档 (https:www。cnblogs。comscy251147p11100961。html) 07hr插件框架玉汝于成 理解,首先MCube会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。 首先来说下这个框架的主体思路,使用JavaSPI来做插件系统;使用JavaAgent来使得插件可以在main主入口方法前或者是方法后执行;使用Javassist框架来进行字节码增强,即实现对spring框架的增强。 针对插件部分,可以定义公共的接口契约:publicinterfaceIPluginExecuteStrategy{执行方法paramagentArgsparaminstvoidexecute(StringagentArgs,Instrumentationinst);} 然后针对premain和agentmain,利用策略模式进行组装如下: premain处理策略类publicclassPluginPreMainExecutorimplementsIPluginExecuteStrategy{扫描加载的plugin,识别出PreMainCondition并加载执行Overridepublicvoidexecute(StringagentArgs,Instrumentationinst){获取前置执行集合ListStringpluginNamesAgentPluginAnnotationHelper。annoProcess(PreMainCondition。class);ServiceLoaderIPluginServicepluginServiceLoaderServiceLoader。load(IPluginService。class);只执行带有PreMainCondition的插件for(IPluginServicepluginService:pluginServiceLoader){if(pluginNames。contains(pluginService。getPluginName())){pluginService。pluginLoad(agentArgs,inst);}}}} agentmain处理策略类publicclassPluginAgentMainExecutorimplementsIPluginExecuteStrategy{扫描加载的plugin,识别出AgentMainCondition并加载执行Overridepublicvoidexecute(StringagentArgs,Instrumentationinst){获取后置执行集合ListStringpluginNamesAgentPluginAnnotationHelper。annoProcess(AgentMainCondition。class);ServiceLoaderIPluginServicepluginServiceLoaderServiceLoader。load(IPluginService。class);for(IPluginServicepluginService:pluginServiceLoader){只执行带有AgentMainCondition的插件if(pluginNames。contains(pluginService。getPluginName())){pluginService。pluginLoad(agentArgs,inst);}}}} 针对premain和agentmain,执行器工厂如下:publicclassAgentPluginContextFactory{创建agentpre执行上下文returnpublicstaticPluginExecutorContextmakeAgentPreExecuteContext(){IPluginExecuteStrategystrategynewPluginPreMainExecutor();PluginExecutorContextcontextnewPluginExecutorContext(strategy);returncontext;}创建agentmain执行上下文returnpublicstaticPluginExecutorContextmakeAgentMainExecuteContext(){IPluginExecuteStrategystrategynewPluginAgentMainExecutor();PluginExecutorContextcontextnewPluginExecutorContext(strategy);returncontext;}} 编写PremainClass和AgentClass指定的类:publicclassAgentPluginPreWrapper{publicstaticvoidpremain(StringagentArgs,Instrumentationinst){AgentPluginContextFactory。makeAgentPreExecuteContext()。execute(agentArgs,inst);}}publicclassAgentPluginMainWrapper{publicstaticvoidagentmain(StringagentArgs,Instrumentationinst){AgentPluginContextFactory。makeAgentMainExecuteContext()。execute(agentArgs,inst);}} 配置文件中指定相应的类:ManifestVersion:1。0PremainClass:org。tiny。upgrade。core。AgentPluginPreWrapperAgentClass:org。tiny。upgrade。core。AgentPluginMainWrapperPermissions:allpermissionsCanRetransformClasses:trueCanRedefineClasses:true 框架搭好后,来编写插件部分,插件的话,需要继承自org。tiny。upgrade。sdk。IPluginService并实现:AgentMainConditionSlf4jpublicclassCodePadPluginServiceProviderimplementsIPluginService{OverridepublicStringgetPluginName(){return增强插件;}OverridepublicvoidpluginLoad(StringagentArgs,Instrumentationinst){获取已加载的所有类Classlt;?〔〕classesinst。getAllLoadedClasses();if(classesnullclasses。length0){return;}需要将业务类进行retransform一下,这样可以避免在transform执行的时候,找不到此类的情况for(Classlt;?clazz:classes){if(clazz。getName()。contains(entity。getClassName())){try{inst。retransformClasses(clazz);}catch(UnmodifiableClassExceptione){log。error(retransformclassfail:clazz。getName(),e);}}}进行增强操作inst。addTransformer(newByteCodeBizInvoker(),true);}OverridepublicvoidpluginUnload(){}} 这里需要注意的是,在插件load的时候,本文做了classretransform操作,这样操作的原因是因为,在程序启动的时候,有时候比如一些类,会在JavaAgent之前启动,这样会造成有些类在进行增强的时候,无法处理,所以这里需要遍历并操作下,避免意外情况。 下面是具体的增强操作:Slf4jpublicclassByteCodeBizInvokerimplementsClassFileTransformer{在此处加载tprdut并利用类加载器加载paramloaderparamclassNameparamclassBeingRedefinedparamprotectionDomainparamclassfileBufferreturnthrowsIllegalClassFormatExceptionOverridepublicbyte〔〕transform(ClassLoaderloader,StringclassName,Classlt;?classBeingRedefined,ProtectionDomainprotectionDomain,byte〔〕classfileBuffer)throwsIllegalClassFormatException{java自带的方法不进行处理if(loadernull){returnnull;}增强spring5的componetscan,将org。tiny路径塞入if(className。contains(ComponentScanBeanDefinitionParser)){try{System。out。println(增强spring);ClassPoolclassPoolnewClassPool(true);classPool。appendClassPath(ByteCodeBizInvoker。class。getName());CtClassctClassclassPool。get(className。replace(,。));ClassFileclassFilectClass。getClassFile();MethodInfomethodInfoclassFile。getMethod(parse);CtMethodctMethodctClass。getDeclaredMethod(parse);addComponentScanPackage(methodInfo,ctMethod);returnctClass。toBytecode();}catch(Exceptione){log。error(handlespring5ComponentScanBeanDefinitionParsererror,e);}}}遍历method,直至找到ReportTracer标记类paramctMethodprivatevoidaddComponentScanPackage(MethodInfomethodInfo,CtMethodctMethod)throwsCannotCompileException{finalboolean〔〕success{false};CodeAttributecamethodInfo。getCodeAttribute();CodeIteratorcodeIteratorca。iterator();行遍历方法体while(codeIterator。hasNext()){ExprEditorexprEditornewExprEditor(){publicvoidedit(MethodCallm)throwsCannotCompileException{StringmethodCallNamem。getMethodName();if(methodCallName。equals(getAttribute)){将org。tiny追加进去m。replace({proceed();,org。tiny。upgrade;});success〔0〕true;}}};ctMethod。instrument(exprEditor);if(success〔0〕){break;}}}} 从上面可以看出,本文是修改了spring中的ComponentScanBeanDefinitionParser类,并将里面的parser方法中将org。tiny。upgrade包扫描路径自动注册进去,这样当别人集成我们的框架的时候,就无须扫描到框架也能执行了。 写到这里,相信大家对整体框架有个大概的认识了。但是这个框架有个缺陷,就是插件jar写完后,一定要放到项目的mavendependency中,然后打包部署才行。实际上有时候,项目上线后,根本就没有机会重新打包部署,那么接下来,就通过自定义Classloader来让插件不仅仅可以本地集成,而且可以从网络中集成。 首先,需要定义自定义类加载器:publicclassTinyPluginClassLoaderextendsURLClassLoader{带参构造paramurlspublicTinyPluginClassLoader(URL〔〕urls,ClassLoaderparent){super(urls,parent);}添加URL路径paramurlpublicvoidaddURL(URLurl){super。addURL(url);}} 这个类加载器,是不是很眼熟,和前面讲的类似,但是带了个parentclassloader的标记,这是为什么呢?这个标记的意思是,当前自定义的TinyPluginClassLoader的父classloader是谁,这样的话,这个自定义类加载器就可以继承父类加载器中的信息了,避免出现问题,这个细节大家注意。 这里需要说明的是,从本地jar文件加载还是从网络jar文件加载,本质上是一样的,因为TinyPluginClassLoader是按照URL来的。 针对于本地jar文件,构造如下URL即可:URLurlnewURL(jar:file:D:projecttinypluginhellotargettinypluginhello1。0SNAPSHOT。jar!) 针对于网络jar文件,构造如下URL即可:URLurlnewURL(jar:http:111。111。111。111tinypluginhello1。0SNAPSHOT。jar!) 这样,只需要定义好自定义类加载器加载逻辑即可:从jar文件中提取出对应的插件类parampluginClassparamjarFilereturnpublicstaticSetClassloadPluginFromJarFile(ClasspluginClass,JarFilejarFile,TinyPluginClassLoadertinyPluginClassLoader){SetClasspluginClassesnewHashSetClass();EnumerationJarEntryjarsjarFile。entries();while(jars。hasMoreElements()){JarEntryjarEntryjars。nextElement();StringjarEntryNamejarEntry。getName();if(jarEntryName。charAt(0)){jarEntryNamejarEntryName。substring(1);}if(jarEntry。isDirectory()!jarEntryName。endsWith(。class)){continue;}StringclassNamejarEntryName。substring(0,jarEntryName。length()6);try{ClassclazztinyPluginClassLoader。loadClass(className。replace(,。));if(clazz!null!clazz。isInterface()pluginClass。isAssignableFrom(clazz)){pluginClasses。add(clazz);}}catch(ClassNotFoundExceptione){log。error(PluginUtil。loadPluginFromJarFilefail,e);}}returnpluginClasses;} 之后,就可以用如下代码对一个具体的jar路径进行加载就行了:加载插件returnOverridepublicSetClassloadPlugins(URLjarURL){try{JarFilejarFile((JarURLConnection)jarURL。openConnection())。getJarFile();getTinyPluginClassLoader()。addURL(jarURL);returnPluginUtil。loadPluginFromJarFile(IPluginService。class,jarFile,getTinyPluginClassLoader());}catch(IOExceptione){log。error(LoadPluginViaJarStrategy。loadPluginsfail,e);returnnull;}} 最终,只需要利用SPI进行动态加载:执行插件publicvoidprocessPlugins(URL。。。urls){if(urlsnullurls。length0){log。error(jarurlpathempty);return;}for(URLurl:urls){pluginLoadFactory。loadJarPlugins(url);}ServiceLoaderIPluginServiceserviceLoaderServiceLoader。load(IPluginService。class,pluginLoadFactory。getPluginLoader());for(IPluginServicepluginService:serviceLoader){pluginService。Process();}} 这样,本文不仅实现了插件化,而且插件还支持从本地jar文件或者网络jar文件加载。由于利用了agentmain对代码进行增强,所以当系统检测到这个jar的时候,下一次执行会重新对代码进行增强并生效。 08hr总结 理解,首先MCube会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。 到这里,我们的用餐进入到尾声了。也不知道这餐,您享用的是否高兴? 其实本文的技术,从双亲委派模型到自定义类加载器,再到基于自定义类加载器实现的类交换,基于JavaSPI实现的类交换,最后到基于JavaSPIJavaAgentJavassist实现的插件框架及框架支持远程插件化,来一步一步的向读者展示所涉及的知识点。当然,由于笔者知识有限,疏漏之处,还望海涵,真诚期待我的抛砖,能够引出您的玉石之言。 作者:石朝阳