Java基础知识:探针技术

2022-08-05 19:44:55 浏览数 (1)

JAVA探针技术

JavaAgent是一个JVM插件,它能够利用jvm提供的 Instrumentation API(Java1.5开始提供)实现字节码修改的功能。Agent分为2种:主程序运行前的Agent,主程序之后运行的Agent(Jdk1.6增加)。 JavaAgent常用于 代码热更新,AOP,JVM监控等功能。

1 主程序运行前的Agent

1.1 探针程序

  • 名称必须为:premain
  • 参数可以是:premain(String agentOps, Instrumentation inst) 也可以是:premain(String agentOps)
  • 优先执行premain(String agentOps)
代码语言:javascript复制
public class AgentTest {

    该方法在main方法之前运行,与main方法运行在同一个JVM中
    并被同一个System ClassLoader装载
    被统一的安全策略(security policy)和上下文(context)管理
    public static void  premain(String agentOps, Instrumentation inst){
        System.out.println("====premain 方法执行");
        System.out.println("参数为:" agentOps);
    }

    如果不存在 premain(String agentOps, Instrumentation inst)
    则会执行 premain(String agentOps)
    public static void premain(String agentOps){

        System.out.println("====premain方法执行2====");
        System.out.println(agentOps);
    }
}

1.2 在MANIFEST.MF配置环境参数

普通项目配置:

代码语言:javascript复制
Manifest-Version: 1.0
Premain-Class: com.agent.AgentTest
Can-Redefine-Classes: true
Can-Retransform-Classes: true

可以配置的属性:

代码语言:javascript复制
Premain-Class	指定代理类
Agent-Class	指定代理类
Boot-Class-Path	指定bootstrap类加载器的搜索路径,在平台指定的查找路径失败的时候生效, 可选
Can-Redefine-Classes	是否需要重新定义所有类,默认为false,可选。
Can-Retransform-Classes	是否需要retransform,默认为false,可选

Maven配置:

manifestEntries里面的元素就与上面的配置对应

代码语言:javascript复制
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>2.4</version>
            <configuration>
                <archive>
                    <manifest>
                        <addClasspath>true</addClasspath>
                    </manifest>

                    <manifestEntries>
                        <Premain-Class>
                            com.agent.AgentTest
                        </Premain-Class>

                        <Can-Redefine-Classes>
                            true
                        </Can-Redefine-Classes>

                        <Can-Retransform-Classes>
                            true
                        </Can-Retransform-Classes>

                        <Manifest-Version>
                            true
                        </Manifest-Version>

                    </manifestEntries>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

1.3 使用探针

  • 打包探针项目 为 preAgent.jar
  • 启动主函数的时候添加jvm参数
  • params 就对应 premain 函数中 agentOps 参数

2 主程序之后运行的Agent

启动前探针使用方式比较局限,而且每次探针更改的时候,都需要重新启动应用,而主程序之后的探针程序就可以直接连接到已经启动的 jvm 中。可以实现例如动态替换类,查看加载类信息的一些功能。

实现一个指定动态类替换的功能

下面就实现一个指定类,指定class文件动态替换,实现动态日志增加的功能。

探针程序

- 主程序后的探针程序名称必须为 agentmain

  • 通过 agentOps 参数将需要替换的类名和 Class 类文件路径传递进来
  • 然后获取全部加载的 Class 去,通过类名筛选出来要替换的 Class
  • 通过传递进行的 Class 类文件路径加载数据
  • 通过 redefineClasses 进行类文件的热替换
  • 使用 redefineClasses 函数必须将 Can-Redefine-Classes 环节变量设置为 true
代码语言:javascript复制
public static void  agentmain(String agentOps, Instrumentation inst) {
	System.out.println("====agentmain 方法开始");

	String[] split = agentOps.split(",");

	String className = split[0];
	String classFile = split[1];

	System.out.println("替换类为:   " className);

	Class<?> redefineClass = null;
	Class<?>[] allLoadedClasses = inst.getAllLoadedClasses();
	for (Class<?> clazz : allLoadedClasses) {
		if (className.equals(clazz.getCanonicalName())){
			redefineClass = clazz;
		}
	}

	if (redefineClass==null){
		return;
	}

	//热替换
	try {
		byte[] classBytes = Files.readAllBytes(Paths.get(classFile));
		ClassDefinition classDefinition = new ClassDefinition(redefineClass, classBytes);
		inst.redefineClasses(classDefinition);
	} catch (ClassNotFoundException | UnmodifiableClassException | IOException e) {
		e.printStackTrace();
	}
	System.out.println("====agentmain 方法结束");
}

2.2 在MANIFEST.MF配置环境参数

普通项目配置:

代码语言:javascript复制
Manifest-Version: 1.0
Agent-Class: com.agent.AgentDynamic
Can-Redefine-Classes: true
Can-Retransform-Classes: true

Maven配置:

代码语言:javascript复制
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jar-plugin</artifactId>
            <version>2.4</version>
            <configuration>
                <archive>
                    <manifest>
                        <addClasspath>true</addClasspath>
                    </manifest>

                    <manifestEntries>
                        <Agent-Class>
                            com.agent.AgentDynamic
                        </Agent-Class>

                        <Can-Redefine-Classes>
                            true
                        </Can-Redefine-Classes>

                        <Can-Retransform-Classes>
                            true
                        </Can-Retransform-Classes>

                        <Manifest-Version>
                            true
                        </Manifest-Version>

                    </manifestEntries>
                </archive>
            </configuration>
        </plugin>
    </plugins>
</build>

2.3 使用探针

先使用 jps 指令 或者 ps -aux|grep java 找到目标 JVM 线程 ID

  1. 编写使用探针程序
  2. 将目标线程attach到VirtualMachine
  3. 配置参数agentOps ,加载探针,此时就会执行探针中的程序
  4. 通过VirtualMachine还能获取到对应JVM的系统参数,以及探针的一些参数
代码语言:javascript复制
public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
    VirtualMachine target = VirtualMachine.attach("96003");//目标VM线程ID

    String agentOps = "com.api.rcode.controller.HomeController,/Users/hailingliang/app/workspace/jave-code-note/server-api/target/classes/com/api/rcode/controller/HomeController.class";
    target.loadAgent("/Users/hailingliang/app/workspace/jave-code-note/java-learn/agent/target/agent-1.0-SNAPSHOT.jar",
            agentOps);

    Properties agentProperties = target.getAgentProperties();
    System.out.println(agentProperties);

    Properties systemProperties = target.getSystemProperties();
    System.out.println(systemProperties);

    target.detach();
}

2.4 运行结果

通过这个方法,我们就可以实现在运行时,对Class文件的动态修改替换

代码语言:javascript复制
@RestController("/home")
public class HomeController {
    @RequestMapping("/h2")
    public String h2(boolean p1, int p2){
        System.out.println(p1 " " p2);
        System.out.println("这个是热加载的效果");
        return "h2 p1: " p1 " p2:" p2;
    }
}

====agentmain 方法开始
参数为:   com.api.rcode.controller.HomeController,/Users/hailingliang/app/workspace/jave-code-note/server-api/target/classes/com/api/rcode/controller/HomeController.class
objectSize   1040
====agentmain 方法完成
true 122112
这个是额外加的一段话12312312312132123
====agentmain 方法开始
参数为:   com.api.rcode.controller.HomeController,/Users/hailingliang/app/workspace/jave-code-note/server-api/target/classes/com/api/rcode/controller/HomeController.class
objectSize   1040
====agentmain 方法完成
true 122112
这个是热加载的效果

2 探针修改Class的限制

2.1 主程序运行前Agent

  • 除了名称以外,可以更改任意内容,名称改了,ClassLoad 就会出问题

2.2 主程序运行中Agent

  • 不能修改Class的文件结构,即不能添加方法,不能添加字段,只能修改方法体的内容,否则就会报
  • UnsupportedOperationException: class redefinition failed: attempted to change the schema (add/remove fields) 类似的异常

2.3 两种修改类的方式

2.3.1 redefineClasses:

重新定义class 自身提供的Class字节码替换掉已存在的Class 应用于线上debug的时候比较方便

2.3.2 retransformClasses:

修改class 在已存在的Class字节码上修改后再进行替换,类似于对Class进行包装。 应用于通用aop服务的时候比较方便

2.3.3 redefineClasses

可以直接采用指定文件进行读取,然后直接进行替换 一般实现的方式是下面这种方式:

代码语言:javascript复制
byte[] classBytes = Files.readAllBytes(Paths.get(classFile));
ClassDefinition classDefinition = new ClassDefinition(redefineClass, classBytes);
inst.redefineClasses(classDefinition); 作者:我是小河神 https://www.bilibili.com/read/cv16232206 出处:bilibili
2.3.4 retransformClasses

retransformClasses 的使用需要 Transformer 类的配合,使用 Transformer 的包装对 Class 进行包装,然后替换

2.3.5 ClassFileTransformer

对类进行包装的转换类

2.3.6 接口定义
代码语言:javascript复制
public interface ClassFileTransformer {
    byte[] transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;
}
2.3.7 实现方式

一般的实现方式是在transform方法中,一般是使用ASM,javassist之类的字节码操纵技术对字节码进行包装。

代码语言:javascript复制
@Override
public byte[] transform(ClassLoader loader,
                        //要转换的类的定义加载程序,
                        String className,
                        //Java虚拟机规范中定义的完全限定类和接口名称的内部形式的类名称。
                        // 例如,“java/util/List”。
                        Class<?> classBeingRedefined,
                        //如果这是由重定义或重传触发的,则被重定义或重传的类;如果这是类加载,则为null
                        ProtectionDomain protectionDomain,
                        //正在定义或重新定义的类的保护域
                        byte[] classfileBuffer
                        //类格式的输入字节缓冲区——不得修改
) throws IllegalClassFormatException {
    ClassReader classReader = new ClassReader(classfileBuffer);
    PreClassVisitor preClassVisitor = new PreClassVisitor( new ClassWriter(ClassWriter.COMPUTE_MAXS));
    classReader.accept(preClassVisitor,0);
    return preClassVisitor.toByteArray();
}

0 人点赞