java agent技术

Agent 是一个运行在目标 JVM 的特定程序,它的职责是负责从目标 JVM 中获取数据,然后将数据传递给外部进程。加载 Agent 的时机可以是目标 JVM 启动之时,也可以是在目标 JVM 运行时进行加载,而在目标 JVM 运行时进行 Agent 加载具备动态性。

 

基础概念

  • JVMTI(JVM Tool Interface):是 JVM 暴露出来的一些供用户扩展的接口集合,JVMTI 是基于事件驱动的,JVM 每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。
  • JVMTIAgent(JVM Tool Interface):是一个动态库,利用 JVMTI 暴露出来的一些接口帮助我们在程序启动时或程序运行时 JVM Attach 机制,将 Agent 加载到目标 JVM 中。
  • JPLISAgent(Java Programming Language Instrumentation Services Agent):它的作用是初始化所有通过 Java Instrumentation API 编写的 Agent,并且也承担着通过 JVMTI 实现 Java Instrumentation 中暴露 API 的责任。
  • VirtualMachine :提供了Attach 动作和 Detach 动作,允许我们通过 attach 方法,远程连接到 JVM 上,然后通过 loadAgent 方法向 JVM 注册一个代理程序 agent ,在该 agent 的代理程序中会得到一个 Instrumentation 实例,该实例可以在 class 加载前改变 class 的字节码,也可以在 class 加载后重新加载。
  • Instrumentation:可以在 class 加载前改变 class 的字节码(premain),也可以在 class 加载后重新加载(agentmain)。

实现方式

当前 Java 提供了两种方式可以将代码注入到 JVM 中:

premain:在启动时通过 javaagent 命令,将代理注入到指定的 JVM 中。
agentmain:运行时通过 attach 工具激活指定代理。

premain 只能在类加载之前修改字节码,类加载之后无能为力,只能通过重新创建ClassLoader 这种方式重新加载。而 agentmain 就是为了弥补这种缺点而诞生的。简而言之,agentmain 可以在类加载之后再次加载一个类,也就是重定义,你就可以通过在重定义的时候进行修改类了,甚至不需要创建新的类加载器,JVM 已经在内部对类进行了重定义(重定义的过程相当复杂)。

其中agentmain 还有一个强大的特点,就是目标程序什么都不需要管,就能够被代理

具体使用

1.定义一个MANIFEST.MF 文件,文件中必须包含 Agent-Class;

Manifest-Version: 1.0
Can-Redefine-Classes: true
Agent-Class: com.zy.agent.AgentMain
Can-Retransform-Classes: true

2.创建一个 Agent-Class 指定的类,该类必须包含 agentmain 方法 ;

public class AgentMain {

    public static void agentmain(String agentArgs, Instrumentation inst) throws UnmodifiableClassException, ClassNotFoundException {
        inst.addTransformer(new InterceptorTransformer(agentArgs), true);
        System.out.println("Agent Main called");
        System.out.println("agentArgs : " + agentArgs);
        inst.retransformClasses(PrintParamTargetTest.class);
    }

上面代码主要逻辑是添加自定义的转换器,然后打印一些日志参数,最后执行retransformClasses方法。执行了 inst.retransformClasses(PrintParamTargetTest.class); 这段代码的意思是,重新转换目标类,也就是 PrintParamTargetTest类。也就是说,你需要重新定义哪个类,需要指定,否则 JVM 不可能知道。还有一个类似的方法 redefineClasses ,注意,这个方法是在类加载前使用的。类加载后需要使用 retransformClasses 方法。

InterceptorTransformer 拦截类的加载,通过javassist工具对我们的目标类实现增强: 

public class InterceptorTransformer implements ClassFileTransformer {

    private String agentArgs;

    public InterceptorTransformer(String agentArgs) {
        this.agentArgs = agentArgs;
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        //javassist的包名是用点分割的,需要转换下
        if (className != null && className.indexOf("/") != -1) {
            className = className.replaceAll("/", ".");
        }
        if (!className.contains("PrintParamTargetTest")) {
            return null;
        }

        try {
            //通过包名获取类文件
            CtClass cc = ClassPool.getDefault().get(className);
            //获得指定方法名的方法
            CtMethod m = cc.getDeclaredMethod(agentArgs.split(",")[1]);
            //在方法执行前插入代码
            m.insertBefore("{ System.out.println(\"=========开始执行=========\"); }");
            m.insertAfter("{ System.out.println(\"=========结束执行=========\"); }");
            return cc.toBytecode();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

3.是用maven等工具将MANIFEST.MF 和 Agent 类打成 jar 包;

redis-study-1.0-SNAPSHOT.jar

 4.将 jar 包载入目标虚拟机。目标虚拟机将会自动执行 agentmain 方法执行方法逻辑,同时,ClassFileTransformer 也会长期有效,在每一个类加载器加载 Class 的时候都会拦截。

public class AttachMain {
	public static void main(String[] args) throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
        //1.获取系统中所有虚拟机,类似jps命令;
		List<VirtualMachineDescriptor> list = VirtualMachine.list();
		for (VirtualMachineDescriptor vmd : list) 
            {
            //2.遍历获取PrintParamTargetTest虚拟机;
			if (vmd.displayName().endsWith("PrintParamTargetTest")) {
                //3.将当前虚拟机连接到目标虚拟机上;
				VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                //4.并在当前虚拟机上加载目标jar包和启动参数
				virtualMachine.loadAgent("D:\\java\\redisstudy\\target\\redis-study-1.0-SNAPSHOT.jar", "PrintParamTargetTest,running");
				System.out.println("ok");
                //5.卸载虚拟机
				virtualMachine.detach();
			}
		}
	}
}

注意:写这段代码的时候 IDE 可能提示找不到 jar 包,这时候将 jdk/lib/tools.jar 添加的项目的 classpath 中。

5.执行测试类,循环打印时间,让目标类一直运行以便观察被agent代理增强的效果:

public class PrintParamTargetTest {

    public static void main(String[] args) {
        // 打印当前进程ID
        System.out.println(ManagementFactory.getRuntimeMXBean().getName());
        Random random = new Random();
        while (true) {
            int sleepTime = 5 + random.nextInt(5);
            running(sleepTime);
        }
    }

    private static void running(int sleepTime) {
        try {
            TimeUnit.SECONDS.sleep(sleepTime);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("running sleep time " + sleepTime);
    }
}

运行结果如下,可以看到在启动代理虚拟机之后,在我们的目标方法通过字节码增强后,输出了前后置处理。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

知始行末

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值