JVM插桩之三:javaagent介绍及javassist介绍

本文介绍一下,当下比较基础但是使用场景却很多的一种技术,稍微偏底层点,就是字节码插桩技术了...,如果之前大家熟悉了asm,cglib以及javassit等技术,那么下面说的就很简单了...,因为下面要说的功能就是基于javassit实现的,接下来先从javaagent的原理说起,最后会结合一个完整的实例演示实际中如何使用。

1、什么是javassist?

Javassist是一个开源的分析、编辑和创建Java字节码的类库。其主要的特点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成

2、Javassist 作用?

a.运行时监控插桩埋点

b.AOP动态代理实现(性能上比Cglib生成的要慢)

c.获取访问类结构信息:如获取参数名称信息

3、Javassist使用流程

4、 如何对WEB项目对象进行字节码插桩

1.统一获取HttpRequest请求参数插桩示例

2.获取HttpRequest参数遇到ClassNotFound的问题

3.Tomcat ClassLoader介绍,及javaagent jar包加载机制

4.通过class加载沉机制实现在javaagent引用jar包

javaagent的主要功能有哪些?

  1. 可以在加载java文件之前做拦截把字节码做修改
  2. 获取所有已经被加载过的类
  3. 获取所有已经被初始化过了的类(执行过了clinit方法,是上面的一个子集)
  4. 获取某个对象的大小
  5. 将某个jar加入到bootstrapclasspath里作为高优先级被bootstrapClassloader加载
  6. 将某个jar加入到classpath里供AppClassloard去加载
  7. 设置某些native方法的前缀,主要在查找native方法的时候做规则匹配

定义一个业务类,类里面定义几个方法,然后在执行这个方法的时候,会动态实现方法的耗时统计。

看业务类定义:

package com.dxz.chama.service;

import java.util.LinkedList;
import java.util.List;

/**
 * 模拟数据插入服务
 *
 */
public class InsertService {
    
    public void insert2(int num) {
        List<Integer> list = new LinkedList<>();
        for (int i = 0; i < num; i++) {
            list.add(i);
        }
    }

    public void insert1(int num) {
        List<Integer> list = new LinkedList<>();
        for (int i = 0; i < num; i++) {
            list.add(i);
        }
    }

    public void insert3(int num) {
        List<Integer> list = new LinkedList<>();
        for (int i = 0; i < num; i++) {
            list.add(i);
        }
    }
}

删除服务:

package com.dxz.chama.service;

import java.util.List;

public class DeleteService {
    public void delete(List<Integer>list){
        for (int i=0;i<list.size();i++){
            list.remove(i);
        }
    }
}

ok,接下来就是要编写javaagent的相关实现:

定义agent的入口

package com.dxz.chama.javaagent;

import java.lang.instrument.Instrumentation;

/**
 * agent的入口类
 */
public class TimeMonitorAgent {
    // peremain 这个方法名称是固定写法 不能写错或修改
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("execute insert method interceptor....");
        System.out.println(agentArgs);
        // 添加自定义类转换器
        inst.addTransformer(new TimeMonitorTransformer(agentArgs));
    }
}

接下来看最重要的Transformer的实现:

package com.dxz.chama.javaagent;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.reflect.Modifier;
import java.security.ProtectionDomain;
import java.util.Objects;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;

/**
 * 类方法的字节码替换
 */
public class TimeMonitorTransformer implements ClassFileTransformer {

   private static final String START_TIME = "\nlong startTime = System.currentTimeMillis();\n";
   private static final String END_TIME = "\nlong endTime = System.currentTimeMillis();\n";
   private static final String METHOD_RUTURN_VALUE_VAR = "__time_monitor_result";
   private static final String EMPTY = "";
   
   private String classNameKeyword;
   
   public TimeMonitorTransformer(String classNameKeyword){
      this.classNameKeyword = classNameKeyword;
   }

   /**
    *
    * @param classLoader 默认类加载器
    * @param className  类名的关键字 因为还会进行模糊匹配
    * @param classBeingRedefined
    * @param protectionDomain
    * @param classfileBuffer
    * @return
    * @throws IllegalClassFormatException
     */
   public byte[] transform(ClassLoader classLoader, String className, Class<?> classBeingRedefined,
         ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
      className = className.replace("/", ".");
      CtClass ctClass = null;
      try {
         //使用全称,用于取得字节码类
         ctClass = ClassPool.getDefault().get(className);
         //匹配类的机制是基于类的关键字 这个是客户端传过来的参数 满足就会获取所有的方法 不满足跳过
         if(Objects.equals(classNameKeyword, EMPTY)||(!Objects.equals(classNameKeyword, EMPTY)&&className.indexOf(classNameKeyword)!=-1)){
            //所有方法
            CtMethod[] ctMethods = ctClass.getDeclaredMethods();
            //遍历每一个方法
            for(CtMethod ctMethod:ctMethods){
               //修改方法的字节码
               transformMethod(ctMethod, ctClass);   
            }
         }
         //重新返回修改后的类
         return ctClass.toBytecode();
      } catch (Exception e) {
         e.printStackTrace();
      }
      
      return null;
   }

   /**
    * 为每一个拦截到的方法 执行一个方法的耗时操作
    * @param ctMethod
    * @param ctClass
    * @throws Exception
     */
    private void transformMethod(CtMethod ctMethod, CtClass ctClass) throws Exception {
        // 抽象的方法是不能修改的,或者方法前面加了final关键字
        if ((ctMethod.getModifiers() & Modifier.ABSTRACT) > 0) {
            return;
        }
        //获取原始方法名称
        String methodName = ctMethod.getName();
        String monitorStr = "\nSystem.out.println(\"method " + ctMethod.getLongName() + " cost:\" + (endTime - startTime) + \"ms.\");";
        //实例化新的方法名称
        String newMethodName = methodName + "$impl";
        //设置新的方法名称
        ctMethod.setName(newMethodName);
        //创建新的方法,复制原来的方法,名字为原来的名字
        CtMethod newMethod = CtNewMethod.copy(ctMethod, methodName, ctClass, null);
        
        StringBuilder bodyStr = new StringBuilder();
        //拼接新的方法内容
        bodyStr.append("{");

        //返回类型
        CtClass returnType = ctMethod.getReturnType();

        //是否需要返回
        boolean hasReturnValue = (CtClass.voidType != returnType);

        if (hasReturnValue) {
            String returnClass = returnType.getName();
            bodyStr.append("\n").append(returnClass + " " + METHOD_RETURN_VALUE_VAR + ";");
        }
        
        bodyStr.append(START_TIME);
        if (hasReturnType) {
            bodyStr.append("\n").append(METHOD_RETURN_VALUE_VAR + " = ($r)" + newMethodName + "($$);");
        } else {
            bodyStr.append("\n").append(newMethodName + "($$);");
        }

        bodyStr.append(END_TIME);
        bodyStr.append(monitorStr);

        if (hasReturnValue) {
            bodyStr.append("\n").append("return " + METHOD_RETURN_VALUE_VAR + " ;");
        }
         
        bodyStr.append("}");
        //替换新方法
        newMethod.setBody(bodyStr.toString());
        //增加新方法
        ctClass.addMethod(newMethod);
    }
}

其实也很简单就两个类就实现了要实现的功能,那么如何使用呢?需要把上面的代码打成jar包才能执行,建议大家使用maven打包,下面是pom.xml的配置文件

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.dxz</groupId>
    <artifactId>chama</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>chama</name>
    <url>http://maven.apache.org</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.12.1.GA</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/cglib/cglib -->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>3.2.5</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/oro/oro -->
        <dependency>
            <groupId>oro</groupId>
            <artifactId>oro</artifactId>
            <version>2.0.8</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>3.8.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>utf-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.0.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer
                                    implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <manifestEntries>
                                        <Premain-Class>com.dxz.chama.javaagent.TimeMonitorAgent</Premain-Class>
                                    </manifestEntries>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

强调一下,红色标准的非常关键,因为如果要想jar能够运行,必须要把运行清单打包到jar中,且一定要让jar的主类是Permain-Class,否则无法运行,运行清单的目录是这样的.

mvn -clean package

如果打包正确的话,里面的内容应该如下所示:

OK至此整体代码和打包就完成了,那么接下来再讲解如何使用

部署方式:

1 基于IDE开发环境运行

首先,编写一个service的测试类如下:

package com.dxz.chama.service;

import java.util.LinkedList;
import java.util.List;

public class ServiceTest {
    public static void main(String[] args) {
        // 插入服务
        InsertService insertService = new InsertService();
        // 删除服务
        DeleteService deleteService = new DeleteService();
        System.out.println("....begnin insert....");
        insertService.insert1(1003440);
        insertService.insert2(2000000);
        insertService.insert3(30003203);

        System.out.println(".....end insert.....");
        List<Integer> list = new LinkedList<>();
        for (int i = 0; i < 29988440; i++) {
            list.add(i);
        }
        System.out.println(".....begin delete......");
        deleteService.delete(list);
        System.out.println("......end delete........");

    }
}

选择编辑配置:如下截图所示

service是指定要拦截类的关键字,如果这里的参数是InsertService,那么DeleteService相关的方法就无法拦截了。同理也是一样的。

chama-0.0.1-SNAPSHOT.jar这个就是刚刚编写那个javaagent类的代码打成的jar包,ok 让我们看一下最终的效果如何:

实际应用场景中,可以把这些结果写入到log然后发送到es中,就可以做可视化数据分析了...还是蛮强大的,接下来对上面的业务进行扩展,因为上面默认是拦截类里面的所有方法,如果业务需求是拦截类的特定的方法该怎么实现呢?其实很简单就是通过正则匹配,下面给出核心代码:

定义入口agent:

package com.dxz.chama.javaagent.patter;
import java.lang.instrument.Instrumentation;

public class TimeMonitorPatterAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new PatternTransformer());
    }
}

定义transformer:

package com.dxz.chama.javaagent.patter;

import javassist.CtClass;
import org.apache.oro.text.regex.PatternCompiler;
import org.apache.oro.text.regex.PatternMatcher;
import org.apache.oro.text.regex.Perl5Compiler;
import org.apache.oro.text.regex.Perl5Matcher;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

public class PatternTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
            ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        PatternMatcher matcher = new Perl5Matcher();
        PatternCompiler compiler = new Perl5Compiler();
        // 指定的业务类
        String interceptorClass = "com.dxz.chama.service.InsertService";
        // 指定的方法
        String interceptorMethod = "insert1";
        try {
            if (matcher.matches(className, compiler.compile(interceptorClass))) {
                ByteCode byteCode = new ByteCode(0;
                CtClass ctClass = byteCode.modifyByteCode(interceptorClass, interceptorMethod);
                return ctClass.toBytecode(0;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

修改字节码的实现:

package com.dxz.chama.javaagent.patter;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;

public class ByteCode {
    public CtClass modifyByteCode(String className, String method) throws Exception {
        ClassPool classPool = ClassPool.getDefault();
        CtClass ctClass = classPool.get(className);
        CtMethod oldMethod = ctClass.getDeclaredMethod(method);
        String oldMethodName = oldMethod.getName(0;
        String newName = oldMethodName + "$impl";
        oldMethod.setName(newName);

        CtMethod newMethod = CtNewMethod.copy(oldMethod, oldMethodName, ctClass, null);
        StringBuffer sb = newe StringBuffer();
        sb.append("{");
        sb.append("\nSystem.out.println(\"start to modify bytecode\"); \n");
        sb.append(newName + "($$);\n");
        sb.append("System.out.println(\"call method" + oldMethodName + "took\"+(System.currentTimeMillis()-start))");
        sb.append("}");
        newMethod.setBody(sb.toString());
        ctClass.addMethod(newMethod);
        return ctClass;
    }
}

OK,

修改下pom中的

<manifestEntries>
    <Premain-Class>com.dxz.chama.javaagent.patter.TimeMonitorPatterAgent</Premain-Class>
</manifestEntries>

这个时候再重新打包,然后修改上面的运行配置之后再看效果,只能拦截到insert1方法

最后 再说一下如何使用jar运行,其实很简单如下:把各个项目都打成jar,比如把上面的service打成service.jar,然后使用java命令运行:

java -javaagent:d://chama-0.0.1-SNAPSHOT.jar=Service -jar service.jar,效果是一样的!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值