当前位置: 首页 > 后端技术 > Java

使用在javaagent中运行的类

时间:2023-04-01 14:26:40 Java

重新定义tomcat后台最近在优化自己的工作流程,希望能提高工作效率。工作中的痛点之一就是从编译打包本地代码到启动服务的过程冗长,因为项目庞大复杂,通常需要10多分钟才能完成整个过程。由于我没有申请正版intellijidea,所以无法使用tomcat插件运行程序,也无法使用debug热插拔功能。因此,在开发环境中,我选择使用rawtomcat,打包后将war包复制到tomcat的webapps目录下下载,使用bin目录下的startup.sh脚本启动服务。在本地调试新功能时,一些逻辑错误可能会导致整个功能的代码调试失败。这时候通常需要修复bug重新打包重启服务,耗时较长。解决方案研究如果只能修改有问题的文件重新编译,直接替换tomcat中运行的相应文件,就可以完美解决问题。JRebel是一个不错的选择,但是因为我们公司需要使用定制的jdk,我们尝试发现JRebel不能用我们的jdk正常运行,所以我们放弃了这个选择。下一个最吸引人的选择是javaagent+javaassist,它可以在tomcat运行时附加,并且可以通过检测重新定义类。选择这个选项!POC和陷阱我在github上找到了一个开源代码repo:https://github.com/turn/Redef...,貌似可以满足我的需求。使用sprintbootinitializer初始化一个简单的springbootmvc项目来模拟运行时tomcat,直接将上面repo中唯一的类复制到本地并新建一个agent项目,并添加maven框架支持。这里遇到了第一个坑,因为tools.jar默认没有包含在classpath中,所以代码编译出现了问题。解决方法很简单,直接在pom依赖中添加tools.jar:com.suntools1.8.0system${YOUR_JAVA_HOME}/lib/tools.jar接下来是打包,因为agent工程需要一个MANIFEST.MF文件来描述agent类,所以我们需要在pom文件中做一些配置。配置方案有两种,一种是手写MANIFEST.MF文件并在pom中指定路径,另一种是在pom中添加配置,打包时插件自动生成文件。插件使用maven-jar-plugin,org.apache.maven.pluginsmaven-jar-plugin2.3.1下面是两个例子:手写文件Manifest-Version:1.0Can-Redefine-Classes:trueCan-Retransform-Classes:trueAgent-Class:YOUR_AGENT_CLASS_QUALIFIED_NAME在pom中指定手写文件路径src/main/resources/META-INF/MANIFEST.MF在pom中添加配置自动生成MANIFEST.MFtrueYOUR_AGENT_CLASS_QUALIFIED_NAMEtruetrue代理打包基本就结束了,接下来就是将jar包附加到运行时服务上了。我们需要一个简单的main方法:publicstaticvoidmain(String[]args)throwsException{Stringpid="${pid}";虚拟机vm=VirtualMachine.attach(pid);vm.loadAgent("/path/to/agent","class_full_name,/path/to/absolute/class/file");vm.detach();}注意这里loadAgent方法的两个参数,第一个是agent.jar的绝对路径,第二个是逗号分隔的字符串。我这里传入了两个参数:1.类全名,2.修改类的绝对路径。这两个参数会被agentmain(StringagentArgs,Instrumentationinst)中的第一个参数接收。这里遇到第二个坑,loadAgent这一步一直报:Exceptioninthread"AttachListener"java.lang.NoClassDefFoundError:javassist/CannotCompileExceptionatjava.lang.Class.getDeclaredMethods0(NativeMethod)atjava.lang.Class。privateGetDeclaredMethods(Class.java:2701)在java.lang.Class.getDeclaredMethod(Class.java:2128)在sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:327)在sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain(Inplstrument:411))引起:java.lang.ClassNotFoundException:java.net.URLClassLoader.findClass(URLClassLoader.java:387)在sun.misc的java.lang.ClassLoader.loadClass(ClassLoader.java:418)的javassist.CannotCompileException。Launcher$AppClassLoader.loadClass(Launcher.java:355)atjava.lang.ClassLoader.loadClass(ClassLoader.java:351)...5more发现有javassist.CannotCompileException的NoClassDefFoundError但是没有更详细的信息,我怀疑是代码中使用了javaassist的CannotCompileException但不在classpath中,所以导致无法正常编译代理代码。可以看到Agent类中使用javaassist的地方是:https://github.com/turn/Redef...尝试注释掉javaassist相关的代码,发现agent加载成功!但是把这段代码注释掉基本上就意味着这个项目的可用性基本为0,还是从头开始吧。写一个简单的HelloWorldAgent试试,publicstaticvoidagentmain(StringagentArgs,Instrumentationinst){System.out.println("agentArgs:"+agentArgs);仪器=安装;System.out.println("enteredagentmainmethod")}尝试发现这个简单的HelloWorldAgent可以加载成功。我们直接在agentmain里面添加内容,尽量让我们的重定义起作用,想要重定义class,获取Class对象是必不可少的一步,所以第一步就是想办法获取我们的目标类,在HelloWorldAgent的agentmain方法中添加:类clazz=Class.forName(className);打印clazz内容,发现clazz对象一直为null。这是遇到的第三个坑,是什么原因呢?让我们先打印出我们可以获得的所有对象:Class[]allClasses=instrumentation.getAllLoadedClasses();我们发现我们的目标物体明明就在里面!那是类加载器问题吗?ClassLoaderclassLoader=null;for(Classclz:allClasses){if(clz.getName().contains(className)){classLoader=clz.getClassLoader();}clazz=classLoader.loadClass(类名);成功加载到对象中!为什么会这样?我们把所有的类名和对应的类加载器打印出来,发现我们的springbootmvc中自己实现的类都是通过:org.springframework.boot.loader.LaunchedURLClassLoader加载的,还有spring的一些框架类和HelloWorldAgent类本身加载的通过sun.misc.Launcher$AppClassLoader!当使用Classclazz=Class.forName(className);直接,因为运行环境的classloader是后者,所以找不到我们的目标类。下一步是尝试将我们新编译的类的字节转换为运行时类:URLurl=newFile(newClassFile).toURI().toURL();InputStreamclassStream=url.openStream();byte[]bytecode=IOUtils.toByteArray(classStream);ClassDefinitiondefinition=newClassDefinition(clazz,bytecode);HelloWorldAgent.redefineClasses(定义);这里遇到了第四个坑:Exceptioninthread"AttachListener"java.lang.reflect.InvocationTargetExceptionatsun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethod)atsun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)atsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)在java.langth.invoke(Method.java:498)在sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:386)在sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain(InstrumentationImpl.java:411)引起:java.lang.NoClassDeforg/FoundError:apache/commons/io/IOUtils在com.zaniu.learn.MyRedefineClassAgent.agentmain(MyRedefineClassAgent.java:110)...6更多由:java.lang.ClassNotFoundException:org.apache.commons.io.IOUtils在java.net.URLClassLoader.findClass(URLClassLoader.java:387)在java.lang.ClassLoader.loadClass(ClassLoader.java:418)在sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)在java.lang.ClassLoader.loadClass(ClassLoader.java:351))...7moreAgent启动失败!找不到Apache的IOUtils!其实和我们的第二个坑很像。看来我们绕不过去了。必须要解决这个问题,继续怀疑运行时apache的jar没有包含在classpath中。第三方包如何包含在类路径中?可以通过maven插件做一个fatjar。详情请参考StackOverflow上的这个回答https://stackoverflow.com/que...然后我们重新attach到进程中,成功!目标类重新定义成功!综上所述,在使用javaagent的过程中还是有很多坑。比如还有一个小坑:如果运行进程attach一次,即使修改了agent类的代码重新attach,javaagent还是运行旧的agent代码。您需要重新启动服务并重新附加。虽然陷阱很多,但我们总能通过大胆的假设和仔细的验证找到解决方案。