乐趣区

关于java:使用javaagent-redefine-tomcat中运行的类

背景

最近在优化本人的工作流程,心愿可能借此进步工作效率。工作中的一个痛点是本地代码编译打包到启动服务过程简短,因为我的项目庞杂,通常须要 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>

上面是两种示例:

  1. 手写文件

    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>
  2. 在 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 才行。
尽管有很多的坑,然而大胆假如小心求证,咱们总能找到解决方案。

退出移动版