Java Agent实例:方法监控

Posted 风在哪

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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 实际编码

项目实际结构如下:

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篇四《通过字节码插桩监控方法采集运行时入参出参和异常信息》

以上是关于Java Agent实例:方法监控的主要内容,如果未能解决你的问题,请参考以下文章

Java Agent实例:方法监控

libvirt-java怎么获得kvm虚拟机内存使用率

libvirt-java怎么获得kvm虚拟机内存使用率

nmon +java nmon Alalizy agent 动态交互监控

zabbix-agent 使用普通用户来运行

性能监控:jvm+cpu+目标field自定义类加载器+Java agent+反射实现对tomcat的零侵入式服务监控