背景
最近在优化本人的工作流程,心愿可能借此进步工作效率。工作中的一个痛点是本地代码编译打包到启动服务过程简短,因为我的项目庞杂,通常须要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.0Can-Redefine-Classes: trueCan-Retransform-Classes: trueAgent-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 moreCaused 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 moreAgent 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才行。
尽管有很多的坑,然而大胆假如小心求证,咱们总能找到解决方案。