# spring-agent **Repository Path**: tasfe/spring-agent ## Basic Information - **Project Name**: spring-agent - **Description**: java增强器 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2024-09-08 - **Last Updated**: 2024-09-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 字节码插件增强包 ## 1. 字节码说明 java写出的代码一般会通过编译器器,编译成class字节码文件,然后通过 **JVM虚拟机** 加载并且运行,而且每个 **class文件** 都具有标准的固定格式,具体的格式如下: ```java ClassFile { u4 magic; // 魔数,固定为0xCAFEBABE u2 minor_version; // 次版本 u2 major_version; // 主版本,常见版本:52对应1.8,51对应1.7,其他依次类推 u2 constant_pool_count; // 常量池个数 cp_info constant_pool[constant_pool_count-1]; // 常量池定义 u2 access_flags; // 访问标志:ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT等 u2 this_class; // 类索引 u2 super_class; // 父类索引 u2 interfaces_count;// 接口数 u2 interfaces[interfaces_count]; //接口信息数组 u2 fields_count; //字段数 field_info fields[fields_count]; //字段信息数组 u2 methods_count; //方法数 method_info methods[methods_count]; //方法信息数组 u2 attributes_count; attribute_info attributes[attributes_count]; } ``` 可以看到,class文件总是一个魔数开头,后面跟着版本号,然后就是常量定义、访问标志、类索引、父类索引、接口个数和索引表、字段个数和索引表、方法个数和索引表、属性个数和索引表 ; class文件本质上是一个字节码流,每个字节码所处的位置代表着一定的指令和含义。如何对class文件中定义的指令和字节码进行解读、增强定义、编排,这是字节码增强技术所要完成的事情。 ## 2. 字节码增强函数 JVM启动支持加载 **Agent代理**,而 **Agent代理** 本身就是一个 **JVM TI**的客户端,其通过监听事件的方式获取Java应用运行状态,调用 **JVM TI** 提供的接口对应用进行控制。Java agent代理的两个入口函数定义: ```java public class Agent { // 用于JVM刚启动时调用,其执行时应用类文件还未加载到JVM public static void premain(String agentArgs, Instrumentation inst) { } // 用于JVM启动后,在运行时刻加载 public static void agentmain(String agentArgs, Instrumentation inst) { } } ``` 这两个入口函数定义分别对应于JVM TI专门提供了执行 [字节码增强(bytecode instrumentation)](https://docs.oracle.com/javase/6/docs/platform/jvmti/jvmti.html#bci) 的两个接口: - 加载时刻增强(**JVM 启动时加载**):类字节码文件在JVM加载的时候进行增强。 - 动态增强(**JVM 运行时加载**):已经被JVM加载的class字节码文件,当被修改或更新时进行增强,从JDK 1.6开始支持 ### 2.1 JVM启动函数 JVM 首先寻找 **函数1**,如果没有发现 **函数1**,则会寻找 **函数2** ```java // 函数1 public static void premain(String agentArgs, Instrumentation inst); // 函数2 public static void premain(String agentArgs); ``` ### 2.2 JVM运行函数 JVM 首先寻找 **函数1**,如果没有发现 **函数1**,则会寻找 **函数2** ```java // 函数1 public static void agentmain(String agentArgs, Instrumentation inst); // 函数2 public static void agentmain(String agentArgs); ``` 这两组方法的第一个参数 `agentArgs` 是随同 **-javaagent** 一起传入的程序参数,如果这个字符串代表了多个参数,就需要自己解析这参数,`inst` 是 Instrumentation 类型的对象,是 JVM 自己传入的,我们可以通过这个参数进行参数的增强操作 ## 3. 类 ### 3.1 Instrumentation 上诉函数传递进来的 **Instrumentation** 接口可以通过文档查看其中的定义最核心的方法是 **addTransformer()** 用于添加 **ClassFileTransformer** 接口类型 ```java public interface Instrumentation { /** * ClassFileTransformer:类型转换器 * canRetransform:经过transformer转换过的类是否允许再次转换 */ void addTransformer(ClassFileTransformer transformer, boolean canRetransform); /** * 对于已经加载了的类,可以重新触发类的加载,从而使用上面的转换器进行增强 * 该方法可以修改方法体,常量池和属性值,但不能新增、删除、重命名属性或方法,也不能修改方法的签名 */ void retransformClasses(Class... classes) throws UnmodifiableClassException; /** * 此方法用于替换类的定义,而不引用现有类文件字节 */ void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException; /** * 获取一个对象的大小 */ long getObjectSize(Object objectToSize); /** * 将一个 jar 文件添加到 bootstrap classload 的 classPath 中 */ void appendToBootstrapClassLoaderSearch(JarFile jarfile); /** * 获取到所有的被当前JVM加载的对象 */ Class[] getAllLoadedClasses(); } ``` - redefineClasses:是自己提供字节码文件来替换已经存在的class文件 - retransformClasses :是在已经存在的字节码文件上修改后再进行替换 ### 3.2 ClassFileTransformer **ClassFileTransformer.transform()** 用于对加载的类进行增强重定义,返回新的类字节码流; 需要特别注意的是,若不进行任何增强,当前方法返回 **null** 即可,若需要增强转换,则需要先拷贝一份 **classfileBuffer**,在拷贝上进行增强转换,然后返回拷贝。 ```java public interface ClassFileTransformer { /** * loader:类加载器 * className:内部定义的类全路径 * classBeingRedefined:待重定义/转换的类 * protectionDomain:保护域 * classfileBuffer:需要增强的字节码流 */ default byte[] transform( ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { return null; } } ``` ## 4. 框架 ![img](images/3975ae6f69657a6f6ae6077f3a9bd216.png) **JDK Proxy** 和 **Cglib** 也是以代码方式进行类方法的切面增强,但它们都是以框架的方式实现了Java类的动态扩展,主要应用在框架级别的字节码增强,在某种程度上 **JDK Proxy** 和 **Cglib** 技术对应用是有代码侵入的,这里的侵入不仅仅是框架代码侵入,而且包括增强的类中依赖 **JDK Proxy** 和 **Cglib** 类。与此相比,**ButeBuddy API** 是以无侵入方式加强类代码,设计理念更优。 ### 4.1 Javassist [Javassist](https://www.javassist.org/) 一个非常早的字节码操作类库,开始于1999年,它能够支持两种编辑方式: - 源码级别 - 字节码级别指令 ```java CtMethod m = cc.getDeclaredMethod("sayHello"); m.insertBefore("{ System.out.println(\"begin of sayhello()\"); }"); ``` ### 4.2 ASM [ASM](https://asm.ow2.io/index.html) 是一个Java字节码解析和操作框架,整个类包非常小,还不到120KB,但其非常注重对类字节码的操作速度, 这种高性能来自于它的设计模式 - 访问者模式,即通过Reader、Visitor和Writer模式;直接操作的字节码数据,因此读取的是字节码指令: ```java mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"); mv.visitLdcInsn("begin of sayhello()."); mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false); ``` ### 4.3 Byte Buddy Byte Buddy是一个字节码生成和操作库,用于在Java应用程序运行时创建和修改Java类,而无需编译器的帮助。 Byte Buddy框架的核心类便是 **net.bytebuddy.ByteBuddy** 类,下面是一个测试例子: 创建一个类,动态写入方法等 ```java public class JavaAgentTest { @Test public void test_hello() throws InstantiationException, IllegalAccessException { String helloWorld = new ByteBuddy() .subclass(Object.class) .method(named("toString")) .intercept(FixedValue.value("Hello World!")) .make() .load(getClass().getClassLoader()) .getLoaded() .newInstance() .toString(); System.out.println(helloWorld); // Hello World! } } ``` #### 方法说明 - subClass(Object.class):创建一个叫 Object 类的子类 - name(String) :自定类的完全限定名 - method():需要拦截类中的方法,**ElementMatcher** 类型作为参数 - defineMethod():定义一个方法,参数是名称+返回值+访问修饰符级别 - intercept():当前方法需要拦截成什么样子,**Implementation** 的实现类 - make():创建上面生成的类型 - load():加载这个生成的类 - newInstance():实例化 #### 静态方法委托 通过 **intercept()** 方法,使用 **MethodDelegation** 类型遍可以将方法委托调用到同名的方法中 - 被委托的方法,需要是 public 类 - 被委托的方法与需要与原方法有着一样的入参、出参、方法名,否则不能映射上 ```java public class JavaAgentTest { /** * 测试静态方法委托 */ @Test public void test_helloWorldWithDelegate() { String className = "com.zhj.agent.HelloWorld"; DynamicType.Unloaded dynamicType = new ByteBuddy() .subclass(Object.class) .name(className) .defineMethod("get", String.class, Modifier.PUBLIC + Modifier.STATIC) .intercept(MethodDelegation.to(JavaAgentTest.class)) //委托调用到同名称的方法 .make(); DynamicType.Loaded type = dynamicType.load(getClass().getClassLoader()); outputClazz(dynamicType.getBytes(), className); } public static String get() { return "get"; } } ``` ![1679365036870](images/1679365036870.png) #### 委托动态方法 ```java MethodDelegation.to(JavaAgentTest.class) //委托到静态方法,注意名称、参数、返回值等影响 MethodDelegation.to(new JavaAgentTest()) //委托到实例方法 ``` #### 注解方式 除了通过上述 API 拦截方法并将方法实现委托给 Interceptor 增强之外,Byte Buddy 还提供了一些预定义的注解,通过这些注解我们可以告诉 Byte Buddy 将哪些需要的数据注入到 Interceptor 中 - @RuntimeType: 告诉 Byte Buddy 不要进行严格的参数类型检测,在参数匹配失败时,尝试使用类型转换方式(runtime type casting)进行类型转换,匹配相应方法 - @This: 注入被拦截的目标对象 - @AllArguments: 注入目标方法的全部参数 - @Origin: 注入目标方法对应的 Method 对象 - @Super: 注入目标对象 - @SuperCall: 我们要在 intercept() 方法中调用目标方法的话,需要通过这种方式注入;与aop中的 **ProceedingJoinPoint.proceed()** 方法有点类似 ## 5. 声明Agent包 要想目标的 JVM 认识这个**Agent**的包,那么就需要实现声明好这个Agent包,声明的格式为在 **resources/META-INF.MANIFEST.MF** 文件,当jar包打包时将文件一并打包: ```properties Manifest-Version: 1.0 Can-Redefine-Classes: true # true表示能重定义此代理所需的类,默认值为 false(可选) Can-Retransform-Classes: true # true 表示能重转换此代理所需的类,默认值为 false (可选) Premain-Class: com.zhj.agent.PluginAgent #premain方法所在类的位置 ``` 如果是 Maven的项目,声明下面的打包插件 ```xml spring-agent org.apache.maven.plugins maven-shade-plugin 3.0.0 package shade com.zhj.agent.PluginAgent ``` 在启动 Jar 包时通过下面指令就可以了,指定 **javaagent** 参数,然后后面跟上目标的 **jar包** > java -javaagent:-javaagent:D:\dev\spring-java.jar target.jar ## 6. 参考文档 - https://blog.csdn.net/crazymakercircle/article/details/126579528 - https://bytebuddy.net/#/tutorial-cn ```json apiVersion: apps/v1 kind: Deployment metadata: name: halo-deploy spec: replicas: 1 selector: matchLabels: app: halo template: metadata: labels: app: halo spec: containers: - name: halo image: halohub/halo:2.3 command: - --halo.external-url=http://localhost:8090/ - --halo.security.initializer.superadminusername=admin - --halo.security.initializer.superadminpassword=P@88w0rd ```