背景
最近在优化本人的工作流程,心愿可能借此进步工作效率。工作中的一个痛点是本地代码编译打包到启动服务过程简短,因为我的项目庞杂,通常须要 10 多分钟能力实现整个过程。因为没有申请正版的 intellij idea 所以不能应用 Tomcat plugin 的形式运行程序,也不能应用 debug 的 hot swap 性能,因而在开发环境中抉择了 raw tomcat 的应用形式,打包完将 war 包 copy 到 tomcat 的 webapps 目录下并应用 bin 目录下的 startup.sh 脚本启动服务。在本地调试新性能的时候,会因为一些逻辑 bug 导致无奈实现整个 feature 的代码调试,这时通常须要修复 bug 并从新打包重启服务,破费工夫较长。
解决方案调研
如果可能只批改有问题的文件并从新编译,间接替换掉 tomcat 中正在运行的对应文件,那么就能够完满解决问题。JRebel 是一个很好的选项,然而因为我司要求应用定制的 jdk,尝试发现 JRebel 无奈搭配我司 jdk 失常运行,因而放弃该选项。接下来最有吸引力的一个选项就是 javaagent+javaassist 了,能够在 tomcat 运行时 attach,能够通过 instrumentation redefine class。选定该计划!
POC 以及遇到的坑
在 github 上发现了一个开源代码 repo: https://github.com/turn/Redef…,看起来合乎我的须要。用 sprintboot initializer 初始化了一个简略的 springboot mvc 我的项目模仿运行时 tomcat,间接把下面 repo 中的惟一一个类 copy 到本地并新建一个 agent 我的项目,增加 maven 框架反对。这里遇到了第一个坑点,因为 tools.jar 默认不蕴含在 classpath 中,因而代码编译出了问题,解决方案很简略,间接将 tools.jar 增加到 pom 依赖中解决:
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>${YOUR_JAVA_HOME}/lib/tools.jar</systemPath>
</dependency>
接下来是打包,因为 agent 我的项目须要有 MANIFEST.MF 文件来形容 agent 类,所以咱们要在 pom 文件中做一些配置。有两种配置计划,一是手写 MANIFEST.MF 文件并在 pom 中指定门路,二是能够在 pom 中增加 configuration 在打包时 plugin 主动生成该文件。plugin 应用 maven-jar-plugin,
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.3.1</version>
上面是两种示例:
-
手写文件
Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true Agent-Class: YOUR_AGENT_CLASS_QUALIFIED_NAME
在 pom 中指定手写的文件门路
<configuration> <archive> <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile> </archive> </configuration>
-
在 pom 中增加配置主动生成 MANIFEST.MF
<archive> <manifest> <addClasspath>true</addClasspath> </manifest> <manifestEntries> <Agent-Class>YOUR_AGENT_CLASS_QUALIFIED_NAME</Agent-Class> <Can-Redefine-Classes>true</Can-Redefine-Classes> <Can-Retransform-Classes>true</Can-Retransform-Classes> </manifestEntries> </archive>
Agent 打包根本完结,接下来就是把 jar 包 attach 到运行时服务了。咱们须要一个简略的 main 办法:
public static void main(String[] args) throws Exception {String pid = "${pid}"; VirtualMachine 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(String agentArgs, Instrumentation inst)
中的第一个参数接管。
这里遇到了第二个坑,在 loadAgent 这一步始终报:Exception in thread "Attach Listener" java.lang.NoClassDefFoundError: javassist/CannotCompileException at java.lang.Class.getDeclaredMethods0(Native Method) at java.lang.Class.privateGetDeclaredMethods(Class.java:2701) at java.lang.Class.getDeclaredMethod(Class.java:2128) at sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:327) at sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain(InstrumentationImpl.java:411) Caused by: java.lang.ClassNotFoundException: javassist.CannotCompileException at java.net.URLClassLoader.findClass(URLClassLoader.java:387) at java.lang.ClassLoader.loadClass(ClassLoader.java:418) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355) at java.lang.ClassLoader.loadClass(ClassLoader.java:351) ... 5 more
咱们发现这里有一个 javassist.CannotCompileException 的 NoClassDefFoundError 异样然而没有更具体的信息了,狐疑是代码中用到了 javaassist 的 CannotCompileException 然而在 classpath 外面没有,所以导致 agent 代码不能失常编译,能够看到 Agent 类中应用到 javaassist 的中央是:https://github.com/turn/Redef…。尝试正文掉跟 javaassist 相干的代码,发现 agent 胜利加载!然而正文掉这段代码基本上等于这个我的项目可用性根本为 0 了。从头开始吧。
写一个简略的 HelloWorldAgent 来尝试,
public static void agentmain(String agentArgs, Instrumentation inst) {System.out.println("agentArgs :" + agentArgs);
instrumentation = inst;
System.out.println("entered agentmain method")
}
尝试发现这个简略的 HelloWorldAgent 能够胜利加载。
咱们间接在 agentmain 中增加内容,尝试让咱们的 redefine work,想要 redefine class,拿到 Class 对象是必不可少的一步,所以第一步就是咱们要想方法拿到咱们的 target class,在 HelloWorldAgent 的 agentmain 办法中增加:
Class clazz = Class.forName(className);
打印 clazz 内容发现 clazz 对象始终是 null。这是遇到的第三个坑,起因是什么呢?咱们先把咱们能拿到的对象全副打印进去:
Class[] allClasses = instrumentation.getAllLoadedClasses();
发现咱们的 target 对象明明是在外面的!那难道是 classloader 的问题?
ClassLoader classLoader = null;
for (Class clz : allClasses) {if (clz.getName().contains(className)) {classLoader = clz.getClassLoader();
}
clazz = classLoader.loadClass(className);
胜利加载到对象了!为什么会这样,咱们把所有的类名和对应的 classloader 全副打印进去发现,咱们的 springboot mvc 外面本人实现的类都是通过:org.springframework.boot.loader.LaunchedURLClassLoader
加载的,而 spring 的一些框架类以及 HelloWorldAgent 类自身是通过 sun.misc.Launcher$AppClassLoader
加载的!间接应用 Class clazz = Class.forName(className);
时因为运行环境的 classloader 是后者,所以找不到咱们的 target 类。
接下来就是尝试把咱们新编译好的类的 byte transform 到运行时的 Class 中:
URL url = new File(newClassFile).toURI().toURL();
InputStream classStream = url.openStream();
byte[] bytecode = IOUtils.toByteArray(classStream);
ClassDefinition definition = new ClassDefinition(clazz, bytecode);
HelloWorldAgent.redefineClasses(definition);
这里咱们遇到了第四个坑:
Exception in thread "Attach Listener" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:386)
at sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain(InstrumentationImpl.java:411)
Caused by: java.lang.NoClassDefFoundError: org/apache/commons/io/IOUtils
at com.zaniu.learn.MyRedefineClassAgent.agentmain(MyRedefineClassAgent.java:110)
... 6 more
Caused by: java.lang.ClassNotFoundException: org.apache.commons.io.IOUtils
at java.net.URLClassLoader.findClass(URLClassLoader.java:387)
at java.lang.ClassLoader.loadClass(ClassLoader.java:418)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:355)
at java.lang.ClassLoader.loadClass(ClassLoader.java:351)
... 7 more
Agent failed to start!
apache 的 IOUtils not found!其实跟咱们第二个坑很像,看来是绕不过了,必须解决这个问题。持续狐疑是因为运行时 apache 的 jar 没有蕴含在 classpath 下,怎么样可能让第三方的包蕴含在 classpath 下呢?能够通过 maven plugin 打一个 fat jar,具体能够参考这个 Stack Overflow 上的这个答复 https://stackoverflow.com/que…。
而后咱们再从新 attach 到过程,胜利!target class 被胜利 redefine!
总结
应用 javaagent 过程中坑还是挺多的,比方还有一个小坑是:运行中的过程如果 attach 了一次之后,即便你批改了 agent 类的代码打包从新 attach,javaagent 运行的还是旧的 agent 代码,须要重启服务从新 attach 才行。
尽管有很多的坑,然而大胆假如小心求证,咱们总能找到解决方案。