Java agent

Java agent

1、Java agent是什么

Java agent是java命令的一个参数。参数javaagent可以用于指定一个jar包。

  1. 这个jar包的MAINFEST.MF文件必须指定Premain-Class项。
  2. Premain-Class指定的那个类必须实现premain()方法。

当Java虚拟机启动时,在执行main函数之前,JVM会先运行-javaagent所指定jar包内Premain-Class这个类的premain方法。

2、如何使用Java agent

2.1、步骤

使用java agent 需要几个步骤:

  1. 定义一个MANIFEST.MF文件,必须包含Premain-Class选项,通常也会加入Can-Redefi
    ne-Classes和Can-Retransform-Classes选项。
  2. 创建一个Premain-Class指定的类,类中包含premain方法,方法逻辑由户自己确定。
  3. 将premain的类和MANIFEST.MF文件打成jar包。
  4. 使用参数-javaagent: jar包路径启动要代理的方法。

2.2、premain()方法的参数

premain()方法包括两个参数:

  1. String agentArgs这个参数就是agent接收到的参数
  2. Instrumentation inst 这个参数能操作一些类的方法,提供了一些类转化的方法
public static void premain(String agentArgs, Instrumentation inst);  //【1】
public static void premain(String agentArgs);  //【2】

其中,【1】和【2】同时存在时,【1】会优先被执行,而【2】则会被忽略。

具体使用如下代码:

import java.lang.instrument.Instrumentation;
public class MyAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("this is an agent.");
        System.out.println("args:" + agentArgs + "\n");
    }
}

2.3、MANIFEST.MF文件

首先,需要在代码的resources目录下添加META-INF/MANIFEST.MF文件。其目的是指定Premain-Class的类。

Manifest-Version: 1.0
Premain-Class: agent.MyAgent
Can-Redefine-Classes: true

2.4、pom.xml文件中配置打包的相关配置

pom.xml文件中添加打包插件和打包的相关配置,指定Premain-Class:

    
        
            
                maven-assembly-plugin
                
                    false
                    
                        jar-with-dependencies
                    
                    
                        
                            true
                        
                        
                            PreMainAgent
                            PreMainAgent
                            true
                            true
                        
                    
                
                
                    
                        make-assembly
                        package
                        
                            single
                        
                    
                
            
        
    

注释:maven-assembly-plugin是maven的一个组装插件
在PreMainAgent中要指定实现premain()方法的类
jar-with-dependencies依赖打包,jar包会包含所有依赖
true自动生成MANIFEST.MF文件

2.5、jar打包

执行mvn clean package,就能生成一个my-agent.jar

2.6、Agent运行

使用如下设置运行Agent

-javaagent: 文件位置 [=参数]

备注:如果是运行多个agent,进行罗列即可。-javaagent: 文件位置 [=参数] -javaagent: 文件位置 [=参数],其中的参数就是premain函数中的agentArgs。

2.7、Agent使用

①首先,新建一个测试类

public class AgentTest {
    public static void main(String[] args) {
        System.out.println("this is main");
    }
}

②命令行运行:java -javaagent: 文件位置 [=参数]
idea运行:如果是Idea中,在VM options中配置。

-javaagent: 文件位置 [=参数]

2.8、结果

this is an agent.
args:first

this is main

3、ByteBuddy

3.1、ByteBuddy是什么

Byte Buddy是开源的、基于Apache2.0许可证的库,它致力于解決字节码操作和instrumentation API的复杂性,ByteBuddy所声称的目标是将显式的字节码操作隐藏在一个类型安全的领域特定语言背后,通过使用ByteBuddy,任何熟java编程语言的人都有望非常容易地进行字节码操作,ByteBuddy提供了额外的API来生成javaagent,可以轻松的増强我们已有的代码

3.2、为什么要用ByteBuddy

例如之前我们要对方法的调用时间来进行统计,是利用spring的AOP技术来进行统计的,但是由于探针并没有集成spring类似的框架,所以不能利用这个技术来实现,这就需要对字节码进行增强,增强后,通过拦截器进行拦截,从而达到统计方法时间的目的。

3.3、添加依赖

引入ByteBuddy的依赖



    net.bytebuddy
    byte-buddy
    1.9.2


    net.bytebuddy
    byte-buddy-agent
    1.9.2


3.4、实现一个Agent

在premain处进行处理,通过AgentBuilder方法,生成一个Agent。这里有两点需要特别说明:其一是在AgentBuilder.type处,这里可以指定需要拦截的类;其二是在builder.method处,这里可以指定需要拦截的方法。当然其API支持各种isStatic、isPublic等等一系列方式。

public class MyAgent {

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("this is an perform monitor agent.");

        AgentBuilder.Transformer transformer = new AgentBuilder.Transformer() {
            @Override
            public DynamicType.Builder transform(DynamicType.Builder builder,
                                                    TypeDescription typeDescription,
                                                    ClassLoader classLoader) {
                return builder
                        .method(ElementMatchers.any()) // 拦截任意方法
                        .intercept(MethodDelegation.to(TimeInterceptor.class)); // 委托
            }
        };

        new AgentBuilder
                .Default()
                .type(ElementMatchers.nameStartsWith("com.example.demo")) // 指定需要拦截的类
                .transform(transformer)
                .with(listener)
                .installOn(inst);
    }
}

3.5、实现一个用来委托的Interceptor

上一步实现Transformer的过程中,委托了一个TimeInterceptor.class。下面是其实现方式,整个的try语句是原有的代码执行,我们在之前打了时间戳,并在其结束后,计算并打印了其调用耗时。

public class TimeInterceptor  {
    @RuntimeType
    public static Object intercept(@Origin Method method,
                                   @SuperCall Callable callable) throws Exception {
        long start = System.currentTimeMillis();
        try {
            // 原有函数执行
            return callable.call();
        } finally {
            System.out.println(method + ": took " + (System.currentTimeMillis() - start) + "ms");
        }
    }
}

3.6、运行

这里需要注意的是,我们定义的包路径要和Agent中定义的相同,否则Agent无法Hook到这个类及其方法。

package com.example.demo;

public class AgentTest {

    private void fun1() throws Exception {
        System.out.println("this is fun 1.");
        Thread.sleep(500);
    }

    private void fun2() throws Exception {
        System.out.println("this is fun 2.");
        Thread.sleep(500);
    }

    public static void main(String[] args) throws Exception {
        AgentTest test = new AgentTest();
        test.fun1();
        test.fun2();

    }
}

3.7、运行结果

可以看到,我们的Agent成功Hook并增强了其调用方法。

this is an perform monitor agent.
this is fun 1.
private void com.example.demo.AgentTest.fun1() throws java.lang.Exception: took 501ms
this is fun 2.
private void com.example.demo.AgentTest.fun2() throws java.lang.Exception: took 500ms
public static void com.example.demo.AgentTest.main(java.lang.String[]) throws java.lang.Exception: took 1001ms

4、使用Java-Agent实现一个JVM监控工具

4.1、实现一个Agent

Agent的构造比较简单,我们只需要在premain方法中,加入一个线程池。其功能是每隔5秒输出一次内存信息和GC信息。

public class MyAgent {

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("this is an perform monitor agent.");

        Executors.newScheduledThreadPool(1).scheduleAtFixedRate(new Runnable() {
            public void run() {
                Metric.printMemoryInfo();
                Metric.printGCInfo();
            }
        }, 0, 5000, TimeUnit.MILLISECONDS);
    }
}

4.2、获取JVM 内存以及GC信息

public class Metric {
    private static final long MB = 1048576L;

    public static void printMemoryInfo() {
        MemoryMXBean memory = ManagementFactory.getMemoryMXBean();
        MemoryUsage headMemory = memory.getHeapMemoryUsage();

        //head堆、初始(M)、最大(上限)(M)、当前(已使用)(M)、提交的内存(已申请)(M)、使用率
        String info = String.format("\ninit: %s\t max: %s\t used: %s\t committed: %s\t use rate: %s\n",
                headMemory.getInit() / MB + "MB",
                headMemory.getMax() / MB + "MB", headMemory.getUsed() / MB + "MB",
                headMemory.getCommitted() / MB + "MB",
                headMemory.getUsed() * 100 / headMemory.getCommitted() + "%"

        );

        System.out.print(info);

        MemoryUsage nonheadMemory = memory.getNonHeapMemoryUsage();

        non-head非堆、初始(M)、最大(上限)(M)、当前(已使用)(M)、提交的内存(已申请)(M)、使用率
        info = String.format("init: %s\t max: %s\t used: %s\t committed: %s\t use rate: %s\n",
                nonheadMemory.getInit() / MB + "MB",
                nonheadMemory.getMax() / MB + "MB", nonheadMemory.getUsed() / MB + "MB",
                nonheadMemory.getCommitted() / MB + "MB",
                nonheadMemory.getUsed() * 100 / nonheadMemory.getCommitted() + "%"

        );
        System.out.println(info);
    }

    public static void printGCInfo() {
        //垃圾收集器、名称、次数、时间、内存区名称
        List garbages = ManagementFactory.getGarbageCollectorMXBeans();
        for (GarbageCollectorMXBean garbage : garbages) {
            String info = String.format("name: %s\t count:%s\t took:%s\t pool name:%s",
                    garbage.getName(),
                    garbage.getCollectionCount(),
                    garbage.getCollectionTime(),
                    Arrays.deepToString(garbage.getMemoryPoolNames()));
            System.out.println(info);
        }
    }
}

4.3、运行

public class AgentTest {
    
    public static void main(String[] args) throws Exception {
        boolean is = true;
        while (is) {
            List list = new ArrayList();
            list.add("hello world");
        }
    }
}

4.4、运行结果

init: 256MB	 max: 3641MB	 used: 2MB	 committed: 245MB	 use rate: 1%
init: 2MB	 max: 0MB	 used: 5MB	 committed: 7MB	 use rate: 72%

name: PS Scavenge	 count:0	 took:0	 pool name:[PS Eden Space, PS Survivor Space]
name: PS MarkSweep	 count:0	 took:0	 pool name:[PS Eden Space, PS Survivor Space, PS Old Gen]

init: 256MB	 max: 3641MB	 used: 8MB	 committed: 245MB	 use rate: 3%
init: 2MB	 max: 0MB	 used: 5MB	 committed: 7MB	 use rate: 77%

name: PS Scavenge	 count:0	 took:0	 pool name:[PS Eden Space, PS Survivor Space]
name: PS MarkSweep	 count:0	 took:0	 pool name:[PS Eden Space, PS Survivor Space, PS Old Gen]

你可能感兴趣的:(java,skywalking,java,开发语言)