前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java基础知识:探针技术

Java基础知识:探针技术

作者头像
DioxideCN
发布2022-08-05 19:44:55
1.2K0
发布2022-08-05 19:44:55
举报
文章被收录于专栏:用户4480853的专栏

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();
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-04-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JAVA探针技术
    • 1 主程序运行前的Agent
      • 1.1 探针程序
      • 1.2 在MANIFEST.MF配置环境参数
      • 1.3 使用探针
    • 2 主程序之后运行的Agent
      • 探针程序
      • 2.2 在MANIFEST.MF配置环境参数
      • 2.3 使用探针
      • 2.4 运行结果
    • 2 探针修改Class的限制
      • 2.1 主程序运行前Agent
      • 2.2 主程序运行中Agent
      • 2.3 两种修改类的方式
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档