「字节码插桩」统计方法耗时(第三篇:叱咤风云)- 第313篇

「字节码插桩」统计方法耗时(第三篇:叱咤风云)- 第313篇_第1张图片

 

相关历史文章(阅读本文之前,您可能需要先看下之前的系列

 

国内最全的Spring Boot系列之三

Java语言的优雅停机 - 第308篇

SpringBoot 优雅停止服务的几种方法 - 第309篇

Docker优雅的关闭SpringBoot - 第310篇

「字节码插桩」统计方法耗时(第一篇:初出茅庐)- 第311篇

「字节码插桩」统计方法耗时(第二篇:崭露头角)- 第311篇

师傅:上一节,我们对于javaagent有了一个简单的认知,但是对于如何修改修改字节码,我们还没进行讲解,这节,我们一起来学习下。

徒儿:实在是太棒了,终于到了重点了,等的花都谢了。

「字节码插桩」统计方法耗时(第三篇:叱咤风云)- 第313篇_第2张图片

 

师傅:没听说过好事多谋嘛。

徒儿:师傅,你都要把我的心魔化了。

「字节码插桩」统计方法耗时(第三篇:叱咤风云)- 第313篇_第3张图片

 

师傅:你看你连这么一丁点的耐性都没有,以后可怎么追女孩子呢。

「字节码插桩」统计方法耗时(第三篇:叱咤风云)- 第313篇_第4张图片

徒儿:师傅,搞得你已经有女朋友了似的。

师傅:你这不怼为师,你会死么?

徒儿:师傅,我错了,我口快心非,咱们还是赶紧进入今天的正题吧。

「字节码插桩」统计方法耗时(第三篇:叱咤风云)- 第313篇_第5张图片

 

一、技术点

1.1 技术点

         在之前的方法中有一个参数:

public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("Hello javaagent permain:"+agentArgs);
}

         参数Instrumentation是一个接口,我们可以看下:

public interface Instrumentation {
    /**
    注册一个Transformer,从此之后的类加载都会被Transformer拦截。
 * Transformer可以直接对类的字节码byte[]进行修改
	
     * @since 1.6
     */
    void
    addTransformer(ClassFileTransformer transformer);

    
    /**
     
 * 对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
 * retransformation可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
    *
     * @see #isRetransformClassesSupported
     * @see #addTransformer
     * @see java.lang.instrument.ClassFileTransformer
     * @since 1.6
     */
    void
    retransformClasses(Class... classes) throws UnmodifiableClassException;

   
   
    
    /**
      获取当前被JVM加载的所有类对象
     */
    @SuppressWarnings("rawtypes")
    Class[]
    getAllLoadedClasses();

  
}

前面两个方法比较重要,addTransformer 方法配置之后,后续的类加载都会被 Transformer 拦截。对于已经加载过的类,可以执行 retransformClasses 来重新触发这个 Transformer 的拦截。类加载的字节码被修改后,除非再次被 retransform,否则不会恢复。

 

1.2 javaassist

直接修改字节码有点麻烦,因此我们借助神器javaassist来修改字节码。

Javassist (JAVA programming ASSISTant) 是在 Java 中编辑字节码的类库;它使 Java 程序能够在运行时定义一个新类, 并在 JVM 加载时修改类文件。

我们常用到的动态特性主要是反射,在运行时查找对象属性、方法,修改作用域,通过方法名称调用方法等。在线的应用不会频繁使用反射,因为反射的性能开销较大。其实还有一种和反射一样强大的特性,但是开销却很低,它就是Javassit。

 

二、实现

         在接下来我看下如何统计方法的耗时。

 

2.1 添加依赖

         在pom.xml文件添加依赖:

	
		
        org.javassist
        javassist
        3.27.0-GA
    

2.2 实现自定义的ClassFileTransformer

         实现自定义的ClassFileTransformer,代码如下

package com.kfit.test4;

import java.io.ByteArrayInputStream;
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
public class TimeConsumingTransformer implements ClassFileTransformer {
	
    @Override
    public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) {
    	// 这里我们限制下,只针对目标包下进行耗时统计
        if (!className.startsWith("com/kfit")) {
            return classfileBuffer;
        }
 
        CtClass cl = null;
        try {
            ClassPool classPool = ClassPool.getDefault();
            cl = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));
            
            if(cl.isInterface()) {//接口的情况下,就不用处理了。
	    		//报异常:javassist.CannotCompileException: no method body
	        	//这个接口,类似public interface PermissionService {};
	        	return classfileBuffer;
	        }
            
            for (CtMethod method : cl.getDeclaredMethods()) {
                // 所有方法,统计耗时;请注意,需要通过`addLocalVariable`来声明局部变量
                method.addLocalVariable("start", CtClass.longType);
                method.insertBefore("start = System.currentTimeMillis();");
                String methodName = method.getLongName();
                method.insertAfter("System.out.println(\"" + methodName + " cost: \" + (System" + ".currentTimeMillis() - start));");
            }
 
            byte[] transformed = cl.toBytecode();
            return transformed;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return classfileBuffer;
    }
}

2.3 修改agent

         对于agent稍微修改下:

package com.kfit;

import java.lang.instrument.Instrumentation;
import com.kfit.test4.TimeConsumingTransformer;

public class MyAgent2 {
	/**
     * jvm 参数形式启动,运行此方法
     *
     * manifest需要配置属性Premain-Class
     * @param agentArgs
     * @param inst
     */
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("premain");
        addTimeConsumingTransformer(inst);
    }
 
    /**
     * 动态 attach 方式启动,运行此方法
     *
     * manifest需要配置属性Agent-Class
     *
     * @param agentArgs
     * @param inst
     */
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("agentmain");
        addTimeConsumingTransformer(inst);
    }
 
    /**
     * 统计方法耗时
     * @param inst
     */
    private static void addTimeConsumingTransformer(Instrumentation inst) {
    	System.out.println("addTimeConsumingTransformer");
        inst.addTransformer(new TimeConsumingTransformer(), true);
    }
}

2.4 配置plugin

         在pom.xml文件配置plugin:


				org.apache.maven.plugins
				maven-jar-plugin
				2.2
				
					
						
							${project.name}
							${project.version}
							com.kfit.MyAgent2
							true
							true
							javassist-3.27.0-GA.jar
						
					
					true
				
			

 

2.5 打包

         使用clean package进行打包出来,为了不和之前冲突,取名为:

agentdemo-0.0.2-SNAPSHOT.jar

 

2.6 测试

         在Meimei所在的工程配置vm options:

-javaagent:/data/tmp/agentdemo-0.0.2-SNAPSHOT.jar=angel

         启动运行结果如下:

premain

addTimeConsumingTransformer

shopping:出发去和美眉一起逛街购物!

shopping:和美眉一起回家!

com.kfit.test.MeiMei.shopping() cost: 1005

com.kfit.test.MeiMei.sum(double,double) cost: 0

花了多少钱:5000.0

com.kfit.test.MeiMei.main(java.lang.String[]) cost: 1007

         看看这打印信息是不是很酷,我们都不需要修改源代码,可以说零侵入,就可以实现所有方法的时间的耗时统计,连main方法都打印出来时间了。

 

三、基于javaagent实现的框架

3.1全链路监控工具-Pinpoint

Pinpoint是一款全链路分析工具,提供了无侵入式的调用链监控、方法执行详情查看、应用状态信息监控等功能。基于GoogleDapper论文进行的实现,与另一款开源的全链路分析工具Zipkin类似,但相比Zipkin提供了无侵入式、代码维度的监控等更多的特性。 Pinpoint支持的功能比较丰富,感兴趣的可以自己去了解下。

 

3.2 Pinpoint实现原理

Pinpoint通过字节码增强技术(有的叫动态探针技术)来实现无侵入式的调用链采集。其核心实现原来还是基于JVM的javaagent机制来实现。

Pinpoint在启动时通过设置vm options:

-javaagent:$AGENT_PATH/pinpoint-bootstrap-$VERSION.jar

         来指定pinpoint agent加载路径,在启动的时候agent将在加载应用class文件之前做拦截并修改字节码,在class方法调用的前后加上链路采集逻辑,从而实现链路采集功能。

         javaAgent的底层机制主要依赖JVMTI ,JVMTI全称JVM Tool Interface,是JVM暴露出来的一些供用户扩展的接口集合。JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者扩展自己的逻辑。但JVMTI都是一些接口合集,需要有接口的实现,这就用到了java的instrument,可以理解instrument是JVMTI的一种实现,为JVM提供外挂支持。

 

悟纤小结

师傅:对于javaagent就介绍到这里,徒儿你把这两天的知识和大家总结下。

小结:

(1)javaagent:主要作用是在class 被加载之前对其拦截,以插入我们的监听字节码。

(2)javassist:修改字节码的工具类库。

(3)对于agent的使用方式就是在启动时候配置vm options: -javaagent: /agent.jar

(4)字节码插桩原理:在ClassLoader装载之前拦截修改class中的内容。

 

我就是我,是颜色不一样的烟火。
我就是我,是与众不同的小苹果。

 

学院中有Spring Boot相关的课程:

à悟空学院:https://t.cn/Rg3fKJD

SpringBoot视频:http://t.cn/A6ZagYTi

Spring Cloud视频:http://t.cn/A6ZagxSR

SpringBoot Shiro视频:http://t.cn/A6Zag7IV

SpringBoot交流平台:https://t.cn/R3QDhU0

SpringData和JPA视频:http://t.cn/A6Zad1OH

SpringSecurity5.0视频:http://t.cn/A6ZadMBe

Sharding-JDBC分库分表实战:http://t.cn/A6ZarrqS

分布式事务解决方案「手写代码」:http://t.cn/A6ZaBnIr

你可能感兴趣的:(从零开始学Spring,Boot,spring,boot)