您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

Java 探针技术原理及实践

时间:2021-09-09 10:36:27  来源:  作者:悠闲鲸鱼L

1 背景

近日在给公司同事分享Arthas 工具使用时候,被它强悍的功能震撼到了就好奇研究了下它的原理及底层实现,其实它是通过JAVA agent 来实现的,也就深入地学习了一下Java agent 技术觉得蛮有意思,也得到了一些启发,通过Java agent 我们可以做到很多意想不到的事情,在我们日常开发过程中你可能已经无形地接触到了它,例如一些APM 工具如:pinpoint,skywalking,cat, arthas,BTrace... 甚至我们的IDEA debug工具无时无刻都有java agent 的身影...如果你是一个Java 开发者,你一定很有必要去研究一下它。


2 Java agent 介绍

Java agent 又名Java 探针活Java 代理,也有人称它为 “插桩”,说的都是一个意思,只是大家给它起了一个比较通俗易懂的名字而已。“探针” 这个说法我感觉非常形象,JVM 一旦跑起来,对于外界来说,它就是一个黑盒子。而 Java Agent 可以像一支针一样插到 JVM 内部,探到我们想要的东西,并且可以注入东西进去。就像我们生病时去医院看医生,医生怎么诊断你身体的健康状况呢,这时候往往借助一个听诊器,把它放在你的胸口去听诊,就能大概了解你的健康状态了,怎么样,是不是很形象? 那又如何理解代理呢?比方说我们需要了解目标 JVM 的一些运行指标,我们可以通过 Java Agent 来实现,这样看来它就是一个代理的效果,我们最后拿到的指标是目标 JVM ,但是我们是通过 Java Agent 来获取的,对于目标 JVM 来说,它就像是一个代理。

Java agent本质上可以理解为一个插件,该插件就是一个精心提供的jar包,这个jar包通过JVMTI(JVM Tool Interface)完成加载,最终借助JPLISAgent(Java Programming Language Instrumentation Services Agent)完成对目标代码的修改。

2.1 java agent 技术的主要功能

java agent技术的主要功能如下:

  • 可以在加载java文件之前做拦截把字节码做修改
  • 可以在运行期将已经加载的类的字节码做变更
  • 还有其他的一些小众的功能如:

获取所有已经被加载过的类

获取所有已经被初始化过了的类

获取某个对象的大小

将某个jar加入到bootstrapclasspath里作为高优先级被bootstrapClassloader加载

将某个jar加入到classpath里供AppClassloard去加载

2.2 java Instrumentation API

通过java agent技术进行类的字节码修改最主要使用的就是Java Instrumentation API。下面将介绍如何使用Java Instrumentation API进行字节码修改。

有两种方式拿到Instrumentation对象:

  1. jvm启动时指定agent,Instrumentation对象会通过agent的premain方法传递。它是Java 5 开始提供的方式。
  2. jvm启动后通过jvm提供的机制加载agent,Instrumentation对象会通过agent的agentmain方法传递。它是java6 开始提供的方式

Java Agent支持目标JVM启动时加载,也支持在目标JVM运行时加载,这两种不同的加载模式会使用不同的入口函数,如果需要在目标JVM启动的同时加载Agent,那么可以选择实现下面的方法:

[1] public static void premain(String agentArgs, Instrumentation inst); 
[2] public static void premain(String agentArgs);

JVM将首先寻找[1],如果没有发现[1],再寻找[2]。如果希望在目标JVM运行时加载Agent,则需要实现下面的方法:

[1] public static void agentmain(String agentArgs, Instrumentation inst); 
[2] public static void agentmain(String agentArgs);

这两组方法的第一个参数AgentArgs是随同 “–javaagent”一起传入的程序参数,如果这个字符串代表了多个参数,就需要自己解析这些参数。inst是Instrumentation类型的对象,是JVM自动传入的,我们可以拿这个参数进行类增强等操作。

 

3 两个小demo

下面我将用两种方式分别对premain 和 agentmain 两种方式介绍。

简单起见,我就对我的目标程序做一个耗时统计,在方法体前后声明两个变量并计算耗时。

3.1 premain 的方式

这里隐藏了一些公司的敏感的信息用“xxx” 代替

我的pom文件

<plugin>
   <groupId>org.Apache.maven.plugins</groupId>
   <artifactId>maven-jar-plugin</artifactId>
   <configuration>
      <archive>
         <manifest>
            <addClasspath>true</addClasspath>
         </manifest>
         <manifestEntries>
            <Premain-Class>com.xxx.xxx.xxx.PreMainAgent</Premain-Class>
         </manifestEntries>
      </archive>
   </configuration>
</plugin>
package com.xxx.xxx.capital;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;

/**
 * Created on 2021/9/5 10:07 下午. <br/>
 * Description: <br/>
 * description for class template.
 *
 * @author danniel.l
 */
public class PreMainAgent {

    private static Instrumentation instrumentation;
    public static void premain(String agentArgs, Instrumentation inst) {
        instrumentation = inst;
        System.err.println("我在main启动之前启动");

        inst.addTransformer(new MyTransformer());
    }

}
package com.xxx.xxx.capital;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

/**
 * Created on 2021/9/5 11:49 下午. <br/>
 * Description: <br/>
 * description for class template.
 *
 * @author danniel.l
 */
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){

        if (!className.startsWith("com/xxx/xxx/xxx/agenttest")) {
            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("==============Enter Method: " + className + "." + methodName + " ==============");");
                bodyStr.append(prefix);
                bodyStr.append(newMethodName + "($$);n");// 调用原有代码,类似于method();($$)表示所有的参数
                bodyStr.append(postfix);
                bodyStr.append("System.out.println("==============Exit 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;
    }
}


目标增强的类

package com.xxx.xxx.xxx.agenttest;

import org.springframework.web.bind.annotation.RestController;

/**
 * Created on 2021/9/6 12:20 上午. <br/>
 * Description: <br/>
 * description for class template.
 *
 * @author danniel.l
 */
@RestController
public class AgentTest {

    public void test1(){
        System.out.println("this is test1");
    }

    public void test2(){
        System.out.println("this is test2");
    }


}

 

启动时增加如下参数

-javaagent:/Users/user/IdeaProjects/bc/xxx-xxx/xxx-xxx-web/target/xxx-xxx-web-2.0.0-SNAPSHOT.jar

 

3.2 agentmain 的方式

3.2.1 pom.xml

<?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">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>agent-test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.24.0-GA</version>
        </dependency>

        <dependency>
            <groupId>com.sun</groupId>
            <artifactId>tools</artifactId>
            <version>1.8.0</version>
            <scope>system</scope>
            <systemPath>/Library/Java/JavaVirtualmachines/jdk1.8.0_291.jdk/Contents/Home/lib/tools.jar</systemPath>
        </dependency>
    </dependencies>

    <build>
        <finalName>agentmain-test</finalName>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.2</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Agent-Class>agent.test.myagent.MyAgentTransformer</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

3.2.2

package agent.test.target;

/**
 * Created on 2021/9/7 3:24 下午. <br/>
 * Description: <br/>
 * 需要增强的目标应用程序入口
 *
 * @author danniel.l
 */
public class TargetMain {

    public static void main(String[] args) {
        TargetTest targetTest = new TargetTest();
        while (true) {
            try {
                Thread.sleep(3000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
                break;
            }
            targetTest.test();
            targetTest.method();
        }
    }

}

 

package agent.test.target;
/**
 * Created on 2021/9/7 3:25 下午. <br/>
 * Description: <br/>
 * 需要增强的目标应用程序类
 *
 * @author danniel.l
 */

public class TargetTest {

    public void test(){
        System.out.println("this is a test!");
    }

    public void method() {
        System.err.println("this is a method!");
    }
}

 

package agent.test.myagent;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.util.List;

/**
 * Created on 2021/9/7 3:44 下午. <br/>
 * Description: <br/>
 * Agent 程序入口.
 *
 * @author danniel.l
 */
public class MyAgentMain {

    public static void main(String[] args) throws Exception {
        // 需要增强的目标程序的名称,可根据args 参数传递进来
        String targetApplicationName = "TargetMain";

        // 需要增强的目标类,可根据args 参数传递进来
        String targetClassName = "agent.test.target.TargetTest";

        // 获取本机已启动的应用程序名称集合列表
        List<VirtualMachineDescriptor> list = VirtualMachine.list();
        for (VirtualMachineDescriptor vmd : list) {
            if (vmd.displayName().endsWith(targetApplicationName)) {
                // 通过VirtualMachine.attach() 附着上目标程序
                VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());

                // 把探针代理程序插桩到目标程序里去。。
                virtualMachine.loadAgent("/Users/user/IdeaProjects/test/agent-test/target/agentmain-test.jar", targetClassName);
                System.out.println("Attached target application successfully!");
                virtualMachine.detach();
            }
        }
    }

}

 

package agent.test.myagent;

import agent.test.target.TargetTest;
import javassist.*;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.security.ProtectionDomain;

/**
 * Created on 2021/9/5 10:30 下午. <br/>
 * Description: <br/>
 * Agent 增强逻辑处理.
 *
 * @author danniel.l
 */
public class MyAgentTransformer {

    public static void agentmain(String agentArgs, Instrumentation instrumentation) throws UnmodifiableClassException {
        System.out.println("agentmain starts...."+ agentArgs);
        instrumentation.addTransformer(new Transformer(agentArgs), true);

        // 允许修改TargetTest类
        instrumentation.retransformClasses(TargetTest.class);
    }

    private static class Transformer implements ClassFileTransformer {
        private final String targetClassName;

        public Transformer(String targetClassName) {
            this.targetClassName = targetClassName;
        }

        @Override
        public byte[] transform(ClassLoader loader,
                                String className,
                                Class<?> classBeingRedefined,
                                ProtectionDomain protectionDomain,
                                byte[] classfileBuffer) {
            if (className == null) {
                return null;
            }

            className = className.replace("/", ".");
            if (!className.equals(targetClassName)) {
                return null;
            }
            System.out.println("transform className=: " + className);

            ClassPool classPool = ClassPool.getDefault();

            // 将要修改的类的classpath加入到ClassPool中,否则找不到该类
            classPool.appendClassPath(new LoaderClassPath(loader));
            try {
                CtClass ctClass = classPool.get(className);
                for (CtMethod ctMethod : ctClass.getDeclaredMethods()) {
                    if (Modifier.isPublic(ctMethod.getModifiers()) && !ctMethod.getName().equals("test")) {
                        // 修改字节码
                        ctMethod.addLocalVariable("begin", CtClass.longType);
                        ctMethod.addLocalVariable("end", CtClass.longType);

                        ctMethod.insertBefore("begin = System.currentTimeMillis();");
                        ctMethod.insertAfter("Thread.sleep(1000L);");
                        ctMethod.insertAfter("end = System.currentTimeMillis();");

                        ctMethod.insertAfter("System.out.println("方法" + ctMethod.getName() + "耗时"+ (end - begin) +"ms");");
                    }
                }
                ctClass.detach();
                return ctClass.toBytecode();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return classfileBuffer;
        }
    }

}

整个目录结构如下

Java 探针技术原理及实践

 

 

先执行TargetMain 这个目标程序启动效果如下:

Java 探针技术原理及实践

 

再启动MyAgentMain 代理程序

 

Java 探针技术原理及实践

 

 

最后再回到TargetMain 效果已经出来了

 

Java 探针技术原理及实践

 



Tags:Java 探针   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
1 背景近日在给公司同事分享Arthas 工具使用时候,被它强悍的功能震撼到了就好奇研究了下它的原理及底层实现,其实它是通过Java agent 来实现的,也就深入地学习了一下Java agent...【详细内容】
2021-09-09  Tags: Java 探针  点击:(66)  评论:(0)  加入收藏
▌简易百科推荐
一、Redis使用过程中一些小的注意点1、不要把Redis当成数据库来使用二、Arrays.asList常见失误需求:把数组转成list集合去处理。方法:Arrays.asList 或者 Java8的stream流式处...【详细内容】
2021-12-27  CF07    Tags:Java   点击:(3)  评论:(0)  加入收藏
文章目录 如何理解面向对象编程? JDK 和 JRE 有什么区别? 如何理解Java中封装,继承、多态特性? 如何理解Java中的字节码对象? 你是如何理解Java中的泛型的? 说说泛型应用...【详细内容】
2021-12-24  Java架构师之路    Tags:JAVA   点击:(5)  评论:(0)  加入收藏
大家好!我是老码农,一个喜欢技术、爱分享的同学,从今天开始和大家持续分享JVM调优方面的经验。JVM调优是个大话题,涉及的知识点很庞大 Java内存模型 垃圾回收机制 各种工具使用 ...【详细内容】
2021-12-23  小码匠和老码农    Tags:JVM调优   点击:(11)  评论:(0)  加入收藏
前言JDBC访问Postgresql的jsonb类型字段当然可以使用Postgresql jdbc驱动中提供的PGobject,但是这样在需要兼容多种数据库的系统开发中显得不那么通用,需要特殊处理。本文介绍...【详细内容】
2021-12-23  dingle    Tags:JDBC   点击:(12)  评论:(0)  加入收藏
Java与Lua相互调用案例比较少,因此项目使用需要做详细的性能测试,本内容只做粗略测试。目前已完成初版Lua-Java调用框架开发,后期有时间准备把框架进行抽象,并开源出来,感兴趣的...【详细内容】
2021-12-23  JAVA小白    Tags:Java   点击:(10)  评论:(0)  加入收藏
Java从版本5开始,在 java.util.concurrent.locks包内给我们提供了除了synchronized关键字以外的几个新的锁功能的实现,ReentrantLock就是其中的一个。但是这并不意味着我们可...【详细内容】
2021-12-17  小西学JAVA    Tags:JAVA并发   点击:(10)  评论:(0)  加入收藏
一、概述final是Java关键字中最常见之一,表示“最终的,不可更改”之意,在Java中也正是这个意思。有final修饰的内容,就会变得与众不同,它们会变成终极存在,其内容成为固定的存在。...【详细内容】
2021-12-15  唯一浩哥    Tags:Java基础   点击:(14)  评论:(0)  加入收藏
1、问题描述关于java中的日志管理logback,去年写过关于logback介绍的文章,这次项目中又优化了下,记录下,希望能帮到需要的朋友。2、解决方案这次其实是碰到了一个问题,一般的情况...【详细内容】
2021-12-15  软件老王    Tags:logback   点击:(17)  评论:(0)  加入收藏
本篇文章我们以AtomicInteger为例子,主要讲解下CAS(Compare And Swap)功能是如何在AtomicInteger中使用的,以及提供CAS功能的Unsafe对象。我们先从一个例子开始吧。假设现在我们...【详细内容】
2021-12-14  小西学JAVA    Tags:JAVA   点击:(21)  评论:(0)  加入收藏
一、概述观察者模式,又可以称之为发布-订阅模式,观察者,顾名思义,就是一个监听者,类似监听器的存在,一旦被观察/监听的目标发生的情况,就会被监听者发现,这么想来目标发生情况到观察...【详细内容】
2021-12-13  唯一浩哥    Tags:Java   点击:(16)  评论:(0)  加入收藏
相关文章
    无相关信息
最新更新
栏目热门
栏目头条