Java Agent实例:方法监控

Java Agent简单实例:方法监控

本篇文章主要介绍通过Java Agent和Javassist技术实现方法的自动监控,监控方法的参数值、返回结果以及方法耗时等信息,有助于我们快速复现并排查相关问题,提高系统稳定性

在开始阅读本篇文章之前,希望读者可以了解Java Agent和Javassist的基本原理和使用方法,有助于我们更加快速理解本篇文章的内容

0. 为什么使用Java Agent

在实际开发过程中,如果想要进行方法的监控,我们可以通过硬编码或者AOP完成这个需求;但是,在微服务的场景下,我们可能有成千上万个服务,每个服务又存在各种各样的方法,如果要对所有服务进行监控,我们可能需要在每个服务中都编写AOP的代码,这样的代码相当冗余并且耗时

如果我们开发一个监听方法的SDK,直接让目标服务引入我们的SDK进行方法监控,其实也能解决问题;但是我们需要与各个服务的负责人沟通,推荐他们引入SDK,这才是真正耗时的事情,可能要很久都无法取得很好的效果

那么有没有什么技术可以帮助我们简化这个操作呢,答案就是Java Agent。Java探针技术相当于JVM层面的AOP,我们不需要再对一个个的服务进行AOP编程,只需要通过Java探针,使用一个jar包和-javaagent命令参数来完成所有方法的监控,只需要经过简单的测试就能完成方法监控

1. Javassist-方法修改

我们的目的是对方法执行监控,那么首先就是确定在监控期间我们需要方法执行的哪些信息,然后通过方法的修改帮助我们获得这些信息,完成我们的目标;进而将方法的运行信息打印或者输出到日志中心(如es),从而实现方法的监控

1.1 监控信息

对于方法的监控,我们需要的监控信息大致如下:

  1. 方法名称
  2. 方法入参类型&方法入参
  3. 方法返回值类型&返回值(非void)
  4. 方法执行耗时
  5. 异常信息(假如方法执行过程中发生异常)

对于方法的监控大概需要上述信息

通过这些信息我们可以了解一个方法执行过程中所接收到的参数和执行结果,并了解到方法的耗时;如果发生异常还可以查询方法的异常信息。我们可以掌握方法每次执行的详细信息,如果发生问题可以轻松复现并寻找解决方法

1.2 实现细节

上小节中确定了方法监控所需要的各种信息,在本小节将讨论具体的实现细节

首先,我们需要获取方法的名称、入参类型和返回值类型,对于每一个方法来说这些信息是不变的,而且方法每次运行时监控都需要这些信息,那么我们可以将这些信息缓存起来,避免每次重复加载,提高效率

其次,方法入参值、方法返回值和方法耗时这些信息在方法的每次执行时都是不一样的,所以我们需要在方法执行过程中动态地获取这些参数并传递到我们自定义的方法监控方法

最后,为了简单起见,在本篇文章中,只是将这些监控信息打印出来,并未将其上传到统一的日志中心

要想将监控信息上传到统一的日志中心,那么就要为每个方法分布一个唯一的methodId,用于唯一标识该方法,可以通过雪花算法等方式生成这个methodId,这里直接使用AtomicInteger来生成methodId

在分布式情况下,上面提到的固定信息的缓存可以只缓存在本地,而不需要通过统一的缓存;因为每个服务只会调用服务本身的方法,方法内部的RPC或http调用不在当前服务的方法监控范围内

1.3 实际编码

项目实际结构如下:

image-20220211221552553

code

监控方法:

public class BlogService {

    /**
     * 获取博客内容
     * @param id        博客id
     * @param author    博客作者
     * @return
     */
    public String getBlog(Integer id, String author) {
        System.out.println("博客ID: " + id);
        System.out.println("博客作者: " + author);
        return "blog's content!";
    }

}

自定义描述方法详情的类:

public class MethodDescription {
    private String className;
    private String methodName;
    private List<String> parameterNameList;
    private List<String> parameterTypeList;
    private String returnType;

    public MethodDescription() {}

    public MethodDescription(String className, String methodName, List<String> parameterNameList,
                             List<String> parameterTypeList, String returnType) {
        this.className = className;
        this.methodName = methodName;
        this.parameterNameList = parameterNameList;
        this.parameterTypeList = parameterTypeList;
        this.returnType = returnType;
    }
    
    // 省略getter和setter方法
}

用于生成methodId的方法:

	public static final int MAX_NUM = 1024 * 32;
    private final static AtomicInteger index = new AtomicInteger(0);
	/**
     * key: hashcode
     * value: methodId
     */
    private final static Map<Integer, Integer> methodInfos = new ConcurrentHashMap<>();
	// 在分布式环境下使用ConcurrentHashMap缓存方法详情
	// private final static Map<Integer, MethodDescription> methodTagAttr = new ConcurrentHashMap<>();
	// 简单起见,这里使用AtomicReferenceArray缓存方法详情
    private final static AtomicReferenceArray<MethodDescription> methodTagArr = new AtomicReferenceArray<>(MAX_NUM);

	public static int generateMethodId(Integer hashcode, String clazzName, String methodName, List<String> parameterNameList, List<String> parameterTypeList, String returnType) {
        if (methodInfos.containsKey(hashcode)) {
            return methodInfos.get(hashcode);
        }

        MethodDescription methodDescription = new MethodDescription();
        methodDescription.setClassName(clazzName);
        methodDescription.setMethodName(methodName);
        methodDescription.setParameterNameList(parameterNameList);
        methodDescription.setParameterTypeList(parameterTypeList);
        methodDescription.setReturnType(returnType);

        int methodId = index.getAndIncrement();
        if (methodId > MAX_NUM) {
            return -1;
        }
        methodTagArr.set(methodId, methodDescription);
        methodInfos.put(hashcode, methodId);
        return methodId;
    }

首先定义监控方法的输出格式,再编写输出方法,我这里定义的输出格式如下:

正常运行方法:
方法监控 - BEGIN
方法名称: javassist.CtMethod.getBlog
方法入参: ["this","id"]
入参类型:["java.lang.Integer","java.lang.String"]
入参值: [1,"张三"]
方法出参: java.lang.String
出参值: "blog's content!"
方法耗时: 89(s)
方法监控 - END

异常运行方法:
方法监控 - BEGIN
方法名称: javassist.CtMethod.getBlog
方法异常: test
方法监控 - END

那么对于正常情况和非正常情况,我们需要编写两套监控信息输出方法:

	public static void point(final int methodId, final long startNanos, Object[] parameterValues, Object returnValues) {
        MethodDescription method = methodTagArr.get(methodId);
        System.out.println("方法监控 - BEGIN");
        System.out.println("方法名称: " + method.getClassName() + "." + method.getMethodName());
        System.out.println("方法入参: " + JSON.toJSONString(method.getParameterNameList()) + "\n"
                + "入参类型:" + JSON.toJSONString(method.getParameterTypeList()) + "\n"
                + "入参值: " + JSON.toJSONString(parameterValues));
        System.out.println("方法出参: " + method.getReturnType() + "\n"
                + "出参值: " + JSON.toJSONString(returnValues));
        System.out.println("方法耗时: " + (System.nanoTime() - startNanos) / 1000_000 + "(s)");
        System.out.println("方法监控 - END\r\n");
    }

    public static void point(final int methodId, Throwable throwable) {
        MethodDescription method = methodTagArr.get(methodId);
        System.out.println("方法监控 - BEGIN");
        System.out.println("方法名称: " + method.getClassName() + "." + method.getMethodName());
        System.out.println("方法异常: " + throwable.getMessage());
        System.out.println("方法监控 - END\r\n");
    }

接下来就是方法的改造了,我们需要改造原有的方法添加方法监控功能的实现:

	private static void newMethod(CtMethod method, ClassPool pool) throws CannotCompileException, NotFoundException {
        // 获取方法入参类型
        List<String> parameterTypeList = new ArrayList<>();
        for (CtClass parameterType : method.getParameterTypes()) {
            parameterTypeList.add(parameterType.getName());
        }

        // 获取方法入参名称
        List<String> parameterNameList = new ArrayList<>();
        CodeAttribute codeAttribute = method.getMethodInfo().getCodeAttribute();
        LocalVariableAttribute attribute = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
        for (int i = 0; i < parameterTypeList.size(); i++) {
            parameterNameList.add(attribute.variableName(i));
        }

        int idx = generateMethodId(method.hashCode(), method.getClass().getName(), method.getName(), parameterNameList, parameterTypeList, method.getReturnType().getName());


        method.addLocalVariable("startNanos", CtClass.longType);
        method.insertBefore("startNanos = System.nanoTime();");
        method.addLocalVariable("parameterValues", pool.get(Object[].class.getName()));
        method.insertBefore("parameterValues = $args;");
        method.insertAfter("cn.wygandwdn.transformer.MonitorTransformer.point(" + idx + ", startNanos, parameterValues, $_);", false);
        method.addCatch( "cn.wygandwdn.transformer.MonitorTransformer.point(" + idx + ", $e); throw $e;", pool.get("java.lang.Exception"));
    }

此时,我们可以通过一个main方法测试newMethod方法的正确性:

	public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass = pool.get("cn.wygandwdn.func.BlogService");
        CtMethod getBlog = ctClass.getDeclaredMethod("getBlog");
        newMethod(getBlog, pool);
        Class<?> aClass = ctClass.toClass();
        BlogService service = (BlogService) aClass.getDeclaredConstructor().newInstance();
        service.getBlog(1, "123");
    }

如果测试成功,将会得到如下测试结果:

博客ID: 1
博客作者: 123
方法监控 - BEGIN
方法名称: javassist.CtMethod.getBlog
方法入参: ["this","id"]
入参类型:["java.lang.Integer","java.lang.String"]
入参值: [1,"123"]
方法出参: java.lang.String
出参值: "blog's content!"
方法耗时: 72(s)
方法监控 - END

2. Java Agent-方法监控

在上节中,我们已经编写好了对代码进行转换的方法,实现了监控信息的输出,本节将结合Java Agent实现方法在类加载时自动转换

首先我们需要编写一个ClassFileTransformer的实现类,调用上节中提供的方法转换代码,将程序启动时加载的类的方法进行转换,这里通过Java Agent的参数设置要对哪些包下的类的方法进行转换

详细的ClassFileTransformer实现如下:

public class MonitorTransformer implements ClassFileTransformer {
    /**
    * 配置哪些包下的类可以被加载
    */
    private String config;
    private ClassPool pool;

    public MonitorTransformer(String config, ClassPool pool) {
        this.config = config;
        this.pool = pool;
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        if (className == null || !className.replaceAll("/", ".").startsWith(this.config)) {
            return null;
        }
        try {
            className = className.replaceAll("/", ".");
            CtClass ctClass = pool.get(className);
            // 这里对类的所有方法进行监控,可以通过config配置具体监控哪些包下的类
            for (CtMethod method : ctClass.getDeclaredMethods()) {
                newMethod(method, pool);
            }
            return ctClass.toBytecode();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }
}

premain方法:

public class MonitorAgent {
    public static void premain(String args, Instrumentation instrumentation) {
        ClassPool pool = ClassPool.getDefault();
        String config = args;

        MonitorTransformer transformer = new MonitorTransformer(config, pool);
        instrumentation.addTransformer(transformer);
    }
}

pom.xml的build配置如下:

	<build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.2.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>cn.wygandwdn.agent.MonitorAgent</Premain-Class>
                            <Agent-Class>cn.wygandwdn.agent.MonitorAgent</Agent-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

然后将该项目打包,得到最终的jar进行测试

测试程序如下:

public class TestMonitor {
    public static void main(String[] args) throws Exception {
        BlogService service = new BlogService();
        System.out.println(service.getBlog(-1, "张三"));
    }
}

命令行参数:

-javaagent:D:\java_project\trace\method-monitor\target\method-monitor-1.0-SNAPSHOT.jar=cn.wygandwdn.func

项目运行结果:

博客ID: 1
博客作者: 张三
方法监控 - BEGIN
方法名称: javassist.CtMethod.getBlog
方法入参: ["this","id"]
入参类型:["java.lang.Integer","java.lang.String"]
入参值: [1,"张三"]
方法出参: java.lang.String
出参值: "blog's content!"
方法耗时: 87(s)
方法监控 - END

blog's content!

3. 总结

通过上述简单的方法监控工具,可以帮助我们更加深入了解Java Agent和Javassist,并进行相关的实践

reference

字节码编程,Javassist篇四《通过字节码插桩监控方法采集运行时入参出参和异常信息》

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在Spring Boot应用程序中集成Java Agent,您可以按照以下步骤进行操作: 1. 首先,将SkyWalking的Java Agent添加到您的Spring Boot项目中。您可以将Agent的JAR文件下载到您的项目中,或者使用Maven或Gradle等构建工具来添加依赖。 2. 在您的Spring Boot应用程序的启动类(通常是一个带有`@SpringBootApplication`注解的类)中,添加`premain`方法来加载Java Agent。这个方法会在JVM加载应用程序之前调用,并将Agent加载到JVM中。例如: ```java public class YourApplication { public static void premain(String agentArgs) { String agentJarPath = "/path/to/skywalking-agent.jar"; // 替换为SkyWalking Agent的路径 try { // 加载Java Agent VirtualMachine vm = VirtualMachine.attach("pid"); // 替换为应用程序的进程ID vm.loadAgent(agentJarPath, agentArgs); vm.detach(); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { // 启动Spring Boot应用程序 SpringApplication.run(YourApplication.class, args); } } ``` 请注意,您需要将`agentJarPath`替换为SkyWalking Agent的实际路径,并将`pid`替换为您的应用程序的进程ID。如果您不知道进程ID,可以使用工具(如jps命令)来查找它。 3. 启动您的Spring Boot应用程序,它将会加载和运行SkyWalking AgentAgent会自动与SkyWalking服务器通信,并发送数据进行监控和分析。 请确保您已正确配置SkyWalking的相关属性,如服务名称和实例名称等。您可以在`config/agent.config`文件中进行配置。 希望这些步骤能够帮助您成功集成SkyWalking的Java Agent到Spring Boot应用程序中!如果您有任何进一步的问题,请随时提问。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值