Java生态中有一些非常规的技术,它们能达到一些特别的效果。这些技术的实现原理不去深究的话一般并不是广为人知。这种技术通常被称为黑科技。而这些黑科技中的绝大部分底层都是通过JVMTI实现的。深入了解文章最下面有解释!

黑科技举例:

对class文件加密、应用性能监控(APM)、产品运行时错误监测及调试、JAVA程序的调试(debug)、JAVA程序的诊断(profile)、热加载。

当然:当今的许多开源工具尤其是监控和诊断工具,很多都是基于Java Agent来实现的,如最近阿里刚开源的Arthas。

。。。。

演示效果

先看代码结构:

javassist 动态修改类 javaagent动态修改代码_javassist 动态修改类

 

//main方法测试类:
/**
 * @author lin
 * @version 1.0
 * @date 2021/3/8 22:16
 * @Description TODO
 */
public class MainTest {
    public static void main(String[] args) {
        System.out.println("hello world ");
    }
}

运行MainTest结果:

 

效果1、代码前置一个时间戳

javassist 动态修改类 javaagent动态修改代码_java_02

效果2、计算整个代码运行时间

javassist 动态修改类 javaagent动态修改代码_jar_03

可见效果还是很牛逼的,没有使用代码嵌入,没有使用spring的aop等技术。

打扰了,不BB了,直接上核心!

寄生虫

首先先了解一下:Agent,

Java Agent是依附于java应用程序并能对其字节码做相关更改的一项技术,它也是一个Jar包,但并不能独立运行,有点像寄生虫的感觉。

比如:自定义的**Agent**的jar包为 myagent.jar。业务jar为main‘.jar
运行方式:java -jar main.jar -javaagent:myagent.jar

一个Java Agent既可以在程序运行前加载(premain方式), 又可以在程序运行后加载(attach方式),还可以修改代码集。

定制化寄生虫Agent

定制化Agent的核心在于instrument 结果。

引入依赖

<dependencies>
      <dependency>
          <groupId>javassist</groupId>
          <artifactId>javassist</artifactId>
          <version>3.12.1.GA</version>
      </dependency>
  </dependencies>

java.lang.instrument 包的实现,也就是基于这种机制的:在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成 Java 类的动态操作。

Instrumentation 的最大作用,就是类定义动态改变和操作。在 Java SE 5 及其后续版本当中,开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 – javaagent参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。

1、编写代理类MyAgent

这个类中,premain方法是关键,对比于一般的入口main一样,这里的premain是在main之前执行的。它会告诉JVM如何处理加载上来的java字节码。如下例:

public class MyAgent {
   public static void premain(String args, Instrumentation inst){
       System.out.println("Hi, I'm agent!");
       inst.addTransformer(new MyTransformer());  // 计算程序运行时间的寄生虫
//       inst.addTransformer(new SimpleTransformer());// 代码前置打印时间戳的寄生虫
  }
}

addTransformer实现了对字节码处理的方法的回调

2、编写对java字节码的处理类

An agent provides an implementation of this interface in order to transform class files. The transformation occurs before the class is defined by the JVM

翻译过来也就是我们可以通过实现该接口来在虚拟机加载类之前对字节码进行相关更改。

实现ClassFileTransformer接口,实现transform方法!

2.1、代码前置打印时间戳的寄生虫

/**
* @author lin
* @version 1.0
* @date 2021/3/8 22:54
* @Description TODO
*/
public class SimpleTransformer implements ClassFileTransformer {
   @Override
   public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
       //java自带的方法不进行处理
       // 只进入 自己写的 test/MainTest 类里面
       if (className.startsWith("java") || className.startsWith("sun") || !"test/MainTest".equals(className)) {
           return null;
      }
       className = className.replace("/", ".");
       CtClass ctclass = null;
       try {
           ctclass = ClassPool.getDefault().get(className);// 使用全称,用于取得字节码类<使用javassist>
           CtMethod mainMethod = ctclass.getDeclaredMethods()[0];
           // 顶部 增加 一个当前的时间戳
           mainMethod.insertBefore("System.out.println(System.currentTimeMillis());");
           return ctclass.toBytecode();
      } catch (Exception e) {
           e.printStackTrace();
      }
       return null;
  }
}

2.2、计算程序运行时间的寄生虫

/**
* 检测方法的执行时间
*/
public class MyTransformer implements ClassFileTransformer {

   final static String prefix = "\nlong startTime = System.currentTimeMillis();\n";
   final static String postfix = "\nlong endTime = System.currentTimeMillis();\n";

   @Override
   public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                           ProtectionDomain protectionDomain, byte[] classfileBuffer) {
       //java自带的方法不进行处理
       if (className.startsWith("java") || className.startsWith("sun") || !"test/MainTest".equals(className)) {
           return null;
      }
       className = className.replace("/", ".");
       CtClass ctclass = null;
       try {
           ctclass = ClassPool.getDefault().get(className);// 使用全称,用于取得字节码类<使用javassist>
           for (CtMethod ctMethod : ctclass.getDeclaredMethods()) {
               String methodName = ctMethod.getName();
               String newMethodName = methodName + "$old";// 新定义一个方法叫做比如sayHello$old
               ctMethod.setName(newMethodName);// 将原来的方法名字修改

               // 创建新的方法,复制原来的方法,名字为原来的名字
               CtMethod newMethod = CtNewMethod.copy(ctMethod, methodName, ctclass, null);

               // 构建新的方法体
               StringBuilder bodyStr = new StringBuilder();
               bodyStr.append("{");
               bodyStr.append("System.out.println(\"==============进入 Method: " + className + "." + methodName + " ==============\");");
               bodyStr.append(prefix);
               bodyStr.append(newMethodName + "($$);\n");// 调用原有代码,类似于method();($$)表示所有的参数
               bodyStr.append(postfix);
               bodyStr.append("System.out.println(\"==============结束 Method: " + className + "." + methodName + " ==Cost:\" +(endTime - startTime) +\"ms " + "===\");");
               bodyStr.append("}");

               newMethod.setBody(bodyStr.toString());// 替换新方法
               ctclass.addMethod(newMethod);// 增加新方法
          }
           return ctclass.toBytecode();
      } catch (Exception e) {
           e.printStackTrace();
      }
       return null;
  }
}

打包寄生虫包

使用mave插件打包

<properties>
      <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>
   
<build>
      <finalName>myagent</finalName>
      <plugins>
          <plugin>
              <artifactId>maven-jar-plugin</artifactId>
              <version>2.4</version>
              <configuration>
                  <archive>
                      <manifest>
                          <addClasspath>true</addClasspath>
                          <mainClass>test.MainTest</mainClass>
                      </manifest>
                      <manifestEntries>
                          <Premain-Class>
                              agent.MyAgent
                          </Premain-Class>
                      </manifestEntries>
                  </archive>
              </configuration>
          </plugin>
      </plugins>
  </build>

打包结果:

得到 寄生虫 代理类


[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ myagent --- [INFO] Building jar: F:\java\workspace\myagent\target\myagent.jar [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS


运行业务程序,测试寄生虫是否执行

业务测试main方法:

/**
 * @author lin
 * @version 1.0
 * @date 2021/3/8 22:16
 * @Description TODO
 */
public class MainTest {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("hello world ");
        Thread.sleep(2000);
    }
}

执行测试程序时,添加“-javaagent:代理类的jar[=传入premain的参数]”选项。

修改运行main方法的参数

javassist 动态修改类 javaagent动态修改代码_jvm_04

运行看结果,卧槽!牛逼~~~

javassist 动态修改类 javaagent动态修改代码_java_05

运行的大致过程为:

首先在执行所有方法前,会执行MyAgent的premain方法。并且可以直观看到,MyAgentTest在运行时首先是进入main方法,然后再是test方法,执行完test方法逻辑后退出test方法,最后退出main方法,不仅能看到每个方法的最终耗时也能看到方法执行的轨迹。

目标达成,完。。。

扩展

     JVMTI(Java Virtual Machine Tool Interface)是一套本地编程接口集合,它提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。关于 JVMTI 的详细信息,请参考 Java SE 6 文档(请参见 参考资源)当中的介绍。 

    形象地说,JVMTI是Java虚拟机提供的一整套后门。通过这套后门可以对虚拟机方方面面进行监控,分析。甚至干预虚拟机的运行。

javassist 动态修改类 javaagent动态修改代码_java_06

JVMTI的定义及原理

   在介绍JVMTI之前,需要先了解下Java平台调试体系JPDA(Java PlatformDebugger Architecture)。它是Java虚拟机为调试和监控虚拟机专门提供的一套接口。如下图所示,JPDA被抽象为三层实现。其中JVMTI就是JVM对外暴露的接口。JDI是实现了JDWP通信协议的客户端,调试器通过它和JVM中被调试程序通信。

     JVMTI 本质上是在JVM内部的许多事件进行了埋点。通过这些埋点可以给外部提供当前上下文的一些信息。甚至可以接受外部的命令来改变下一步的动作。外部程序一般利用C/C++实现一个JVMTIAgent,在Agent里面注册一些JVM事件的回调。当事件发生时JVMTI调用这些回调方法。Agent可以在回调方法里面实现自己的逻辑。JVMTIAgent是以动态链接库的形式被虚拟机加载的。