JavaAgent实战

相关资料

库房地址:gitee.com/ilovexp/jav…

什么是JavaAgent

JavaAgent是Java中的一种特殊类型的署理技术,它答应在Java使用程序编译期或运转时动态地监控和修正字节码。这使得开发者能够在不修正源代码的情况下,向使用程序增加新功用或履行一些特定的任务。

使用场景

  1. 功用监测与优化:JavaAgent能够监控使用程序的功用指标,如办法履行时间、内存运用情况、线程状况等。开发人员能够运用这些信息来识别功用瓶颈,并进行代码优化
  2. AOP(面向切面编程):JavaAgent能够完成AOP编程,经过字节码增强技术,将横切逻辑(如日志记录、安全检查等)从事务逻辑中分离出来,进步代码的可维护性和复用性。
  3. 代码注入:JavaAgent能够在运转时动态地向使用程序中注入代码。这能够用于完成一些特定的功用,如办法追踪、事情监听等。
  4. 动态修正字节码:JavaAgent能够在类加载时修正类的字节码。经过字节码增强,能够为类增加新的字段或办法,乃至能够修正现有办法的完成。
  5. 长途调试与监控:JavaAgent能够充当长途调试和监控的署理,让开发人员能够长途衔接到使用程序,并查看和修正运转时的状况。
  6. 使用程序安全:JavaAgent能够用于增强使用程序的安全性,例如,阻拦敏感信息的传输、检查权限等。
  7. 类加载器剖析:JavaAgent能够协助开发人员剖析类的加载情况,包括加载哪些类以及类加载器的层次结构。
  8. 代码掩盖率测验:JavaAgent能够用于代码掩盖率测验,协助开发人员评价测验用例的掩盖范围,以及发现未被掩盖的代码块。

核心原理

Instrumentation API

Instrumentation API 是 Java 供给的一组用于在 Java 虚拟机 (JVM) 运转时监控和修正类界说的东西。它答应开发人员在类加载时,动态地检测和修正类的字节码,然后完成一系列功用。首要作用如下:

  1. 类转化(Class Transformation):Instrumentation API 答应 JavaAgent 在类被加载到 JVM 之前,阻拦并修正类的字节码。这使得 JavaAgent 能够在运转时对类进行增强、修正或注入新的代码,然后完成各种功用,如 AOP、功用监测等。
  2. 类加载事情监听(Class Loading Events):Instrumentation API 答应 JavaAgent 监听类加载和卸载事情。开发人员能够在类被加载或卸载时,做一些额定的处理,例如记录日志、进行计算等。
  3. 获取已加载类信息(Class Information Retrieval):Instrumentation API 供给了一些办法,能够获取已加载类的信息,如类名、类的父类、已加载类的字节码等。这为开发人员供给了一些基础信息,能够用于自界说的类加载过程或类信息的剖析。
  4. 从头界说类(Redefine Classes):Instrumentation API 答应 JavaAgent 在运转时从头界说类。这个功用答应开发人员对现已加载的类进行修正,可是需求留意,这个功用的运用遭到一些约束,比方不能新增或删去字段。
  5. 动态注入(Dynamic Code Injection):借助 Instrumentation API,JavaAgent 能够在类加载时将一些额定的代码动态地注入到类中。这能够用于完成动态署理、事情监听等功用。

获取 Instrumentation 实例

开发人员能够经过调用 Agent.premainAgent.agentmain 办法来获取 Instrumentation 实例,然后运用 Instrumentation API。

JavaAgent实战 | 如何通过无侵入式打印方法耗时

  • addTransformer(ClassFileTransformer transformer, boolean canRetransform): 增加一个 ClassFileTransformer,用于在类被加载时转化类的字节码。canRetransform 参数表明是否答应对已加载类进行从头转化。
  • retransformClasses(Class<?>... classes): 对指定的一组类进行从头转化,这些类必须现已被加载。
  • getAllLoadedClasses(): 获取当时现已加载的所有类的数组。
  • getInitiatedClasses(ClassLoader loader): 获取由指定类加载器加载的所有类的数组。

ClassFileTransformer API

public interface ClassFileTransformer {
    byte[] transform(ClassLoader    loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;
}

ClassFileTransformer 接口:这是一个用于类转化的接口,开发人员需求完成它来界说类的转化行为。它只有一个办法 transform,在类被加载时会被调用,开发人员能够在这个办法中对类的字节码进行修正,并回来修正后的字节码。

  1. ClassLoader loader: 表明加载当时类的类加载器。该参数指示了加载当时类的类加载器实例,能够用于追溯类的类加载器层次结构。
  2. String className: 表明正在被转化的类的类名。这是一个完全限制的类名,例如 “com.example.MyClass”。
  3. Class<?> classBeingRedefined: 表明正在被从头界说的类。在进行类的从头界说时才会有值,否则为 null。通常情况下,咱们不需求在类加载时关怀这个参数。
  4. ProtectionDomain protectionDomain: 表明类的维护域。维护域描绘了类所属的安全策略和权限信息。它能够用于检查类的访问权限,或许获取与类相关的安全上下文。
  5. byte[] classfileBuffer: 表明类的字节码。这是一个字节数组,包含了正在加载的类的原始字节码。在 transform 办法中,能够修正这个字节码,并回来修正后的字节码。
  6. throws IllegalClassFormatException: 当 transform 办法无法处理类时,能够抛出这个反常。通常,假如你无法处理正在加载的类,能够直接将原始的 classfileBuffer 回来,不做任何修正。

VirtualMachine API

VirtualMachine 是一个类,它坐落 com.sun.tools.attach 包中。它供给了一组用于办理和操作正在运转的Java虚拟机的东西办法。首要作用是答应Java使用程序在运转时衔接和控制其他正在运转的Java虚拟机,然后完成一些高档的功用。

经过运用 VirtualMachine 类,Java使用程序能够在运转时:

  1. Attach to VM(衔接到虚拟机):Java使用程序能够运用 VirtualMachine.attach() 办法衔接到正在运转的Java虚拟机。这答应使用程序获得对方针虚拟机的访问权,并履行后续操作,例如获取虚拟机信息、履行确诊指令等。
  2. Load Agent(加载署理):一旦Java使用程序衔接到方针虚拟机,它能够运用 VirtualMachine.loadAgent() 办法来加载Java署理(JavaAgent)。JavaAgent是一种特殊类型的署理,它能够在方针虚拟机中运转,并对类加载进行阻拦和修正。
  3. 履行确诊指令:Java使用程序能够运用 VirtualMachine.startManagementAgent() 办法来发动方针虚拟机的办理署理(management agent)。然后,使用程序能够经过JMX(Java Management Extensions)衔接到办理署理,履行一些确诊指令、获取JVM运转时信息等。
  4. Detaching(断开衔接):Java使用程序还能够运用 VirtualMachine.detach() 办法断开与方针虚拟机的衔接,当不再需求衔接时,能够经过这种办法开释资源。

实战代码

整个项目目录

application项目是实践运转的项目,能够理解为实践履行的事务jar包

myAgent项目是编写agent代码的项目

middle项目是中间项目,在agentMain形式下,用于衔接application和agentMain的项目;

├─application # 实践运转项目
│  ├─src
│  │  ├─main
│  │  │  ├─java
│  │  │  │  └─cn
│  │  │  │      └─ixp
│  │  │  │          └─agent
│  │  │  └─resources
│  │  └─test
│  │      └─java
├─middle #中间项目,用于agent和application的衔接,仅agentMain形式有用 
│  ├─src
│  │  └─main
│  │      └─java
│  │          └─cn
│  │              └─ixp
│  │                  └─agent
├─myAgent # agent项目
│  ├─src
│  │  ├─main
│  │  │  ├─java
│  │  │  │  └─cn
│  │  │  │      └─ixp
│  │  │  │          └─agent
│  │  │  │              └─transformer
│  │  │  └─resources
│  │  └─test
│  │      └─java

PreMain办法

premain 办法的 JavaAgent 是在 Java 使用程序发动时,即主函数履行之前,经过指定发动参数 -javaagent 加载的。它的首要作用是在使用程序类加载之前,对类进行阻拦并修正类的字节码,然后完成对类的增强、动态注入等操作。

程序履行次序:premain() => main()

已知,咱们有一个Man类,使用程序发动时,循环调用Man类的getName()办法;屏幕上不断打印"Dragon like sheep";当发动Agent时,将打印的文字替换为:"This is ordertiny";

Application项目:

public class Man {
    public Man() {
        System.out.println("Create Man");
    }
    public void getName() {
        try {
            Thread.sleep((long) (Math.random()*10 * 1000));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("Dragon like sheep");
    }
}
// 发动类
public class ApplicationMain {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("U are in Application Main");
        while (true) {
            Man man = new Man();
            man.getName();
            Thread.sleep(1000);
        }
    }
}
编写Agent项目
引入Jar包
<!-- 引入两个修正Class的jar -->
<dependencies>
    <dependency>
        <groupId>com.squareup</groupId>
        <artifactId>javapoet</artifactId>
        <version>1.13.0</version>
    </dependency>
    <dependency>
        <groupId>org.javassist</groupId>
        <artifactId>javassist</artifactId>
        <version>3.27.0-GA</version>
    </dependency>
</dependencies>
<!-- 打包时要指明Premain-Class-->
<build>
    <plugins>
        <!-- 其他插件... -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>3.3.0</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                    <configuration>
                        <artifactSet>
                            <includes>
                                <include>org.javassist:javassist</include>
                                <!-- 其他依靠项... -->
                            </includes>
                        </artifactSet>
                        <transformers>
                            <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                <manifestEntries>
                                    <Premain-Class>cn.ixp.agent.InPreMain</Premain-Class>
                                    <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                    <Can-Retransform-Classes>true</Can-Retransform-Classes>
                                </manifestEntries>
                            </transformer>
                        </transformers>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

这样能够在打包的时候在MANIFEST.MF中显示声明Premain-Class

# MANIFEST.MF
Manifest-Version: 1.0
Premain-Class: cn.ixp.agent.InPreMain
Archiver-Version: Plexus Archiver
Built-By: lenovo
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Created-By: Apache Maven 3.6.0
Build-Jdk: 1.8.0_181
界说进口办法
package cn.ixp.agent;
import cn.ixp.agent.transformer.MyClassFileTransformer;
import java.lang.instrument.Instrumentation;
public class InPreMain {
    /**
     * 固定格式,声明premain函数,然后在其中指定咱们自己界说的署理类
     * @param agentArgs 捕获agent参数
     * 例如:java -javaagent:myagent.jar=myarg1,myarg2 -jar myapp.jar
     * 那么agentArgs就是myarg1,myarg2
     * @param inst 主动注入的Instrumentation实例
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        // 打印一下参数
        System.out.println("premain invoke!"+agentArgs);
        // 增加自界说Transformer
        inst.addTransformer(new MyClassFileTransformer(), true);
    }
}
自界说ClassFileTransformer

用于将原有cn.ixp.agent.Man.class中的getName()办法体修正为 输出”This is orderting”

package cn.ixp.agent.transformer;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class MyClassFileTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            System.out.println("transform invoke! "+className);
            // 修正指定类
            if (className.contains("cn/ixp/agent/Man")){
                final ClassPool classPool = ClassPool.getDefault();
                final CtClass clazz = classPool.get("cn.ixp.agent.Man");
                CtMethod convertToAbbr = clazz.getDeclaredMethod("getName");
                // 修正指定的办法体
                String methodBody = "System.out.println(\"This is ordertiny\");";
                convertToAbbr.setBody(methodBody);
                // 回来字节码,而且detachCtClass目标
                byte[] byteCode = clazz.toBytecode();
                //detach的意思是将内存中从前被javassist加载过的目标移除,假如下次有需求在内存中找不到会从头走javassist加载
                clazz.detach();
                return byteCode;
            }
        }catch (Exception e) {
            System.out.println("error");
        }
        // 表明不修正原有类
        return null;
    }
}

此刻将agent项目打成jar包

发动application项目后,Man的输出将被agent修正;

java -javaagent:myAgent-1.0-SNAPSHOT.jar -jar application.jar

AgentMain办法

完成一个办法的耗时打印

咱们完成一个办法的耗时打印,咱们在每个办法进入前和后参加时间戳,用来计算办法耗时;

修正Pom文件

指定Agent-Class所在类路径;

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>javaAgent</artifactId>
        <groupId>cn.ixp.agent</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <artifactId>myAgent</artifactId>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <dependency>
            <groupId>com.squareup</groupId>
            <artifactId>javapoet</artifactId>
            <version>1.13.0</version>
        </dependency>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.27.0-GA</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <!-- 其他插件... -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.3.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <artifactSet>
                                <includes>
                                    <include>org.javassist:javassist</include>
                                    <!-- 其他依靠项... -->
                                </includes>
                            </artifactSet>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <manifestEntries>
                                        <Premain-Class>cn.ixp.agent.InPreMain</Premain-Class>
                                        <Agent-Class>cn.ixp.agent.InAgentMain</Agent-Class>
                                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                                    </manifestEntries>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
指定AgentMain进口
package cn.ixp.agent;
import cn.ixp.agent.transformer.InjectMethodCostTimeTransformer;
import cn.ixp.agent.transformer.MyClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
public class InAgentMain {
    public static void agentmain(String agentArgs, Instrumentation inst){
        // 增加耗时计算的Transformer
        inst.addTransformer(new InjectMethodCostTimeTransformer(), true);
        try {
            for (Class innerClass : inst.getAllLoadedClasses()) {
                if (innerClass.getName().contains("cn.ixp")) {
                    // 指定某个包下的类
                    inst.retransformClasses(innerClass);
                }
            }
        } catch (UnmodifiableClassException e) {
            System.out.println("出错");
            throw new RuntimeException(e);
        }
    }
}
自界说Transformer
package cn.ixp.agent.transformer;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class InjectMethodCostTimeTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        System.out.println("InjectMethodCostTimeTransformer transform invoke! "+className);
        String targetClassName = className.replaceAll("/", ".");
        try {
            final ClassPool classPool = ClassPool.getDefault();
            final CtClass clazz = classPool.get(targetClassName);
            for (CtMethod declaredMethod : clazz.getDeclaredMethods()) {
                declaredMethod.addLocalVariable("_time", CtClass.longType);
                // 进入时间
                String param1 = "_time=System.currentTimeMillis();";
                // 打印语句
                String param2 = "System.out.println(\""+declaredMethod.getName()+" cost:\"+(System.currentTimeMillis()-_time));";
                declaredMethod.insertBefore(param1);
                declaredMethod.insertAfter(param2);
            }
            // 回来字节码,而且detachCtClass目标
            byte[] byteCode = clazz.toBytecode();
            //detach的意思是将内存中从前被javassist加载过的Date目标移除,假如下次有需求在内存中找不到会从头走javassist加载
            clazz.detach();
            return byteCode;
        } catch (Exception e) {
            System.out.println("has error"+targetClassName);
            e.printStackTrace();
        }
        return null;
    }
}
将Agent项目打成jar包
编写中间项目

middle项目仅仅一个桥接项目,它用于

package cn.ixp.agent;
import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;
import java.util.List;
public class Main {
    // 不会履行
    public static void main(String[] args) throws Exception {
        System.out.println("Here is MyAgent Main");
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            //假如虚拟机的名称为 xxx 则 该虚拟机为方针虚拟机,获取该虚拟机的 pid
            //然后加载 agent.jar 发送给该虚拟机
            System.out.println(vmd.displayName());
            if ("cn.ixp.agent.ApplicationMain".equals(vmd.displayName())) {
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                // 加载agent的jar
         		virtualMachine.loadAgent("myAgent-1.0-SNAPSHOT.jar");
                // 加载结束后 断开衔接
                virtualMachine.detach();
            }
        }
    }
}

首要保证Application正在运转,然后履行middle项目,此刻即可将 正在运转的java进程中类,经过agent注入了办法耗时计算功用;

留意

咱们的agent项目无法独自发动,需求调试时,应该debug使用进程,才干调试agent代码;