欢迎访问 生活随笔!

生活随笔

当前位置: 首页 > 编程资源 > 编程问答 >内容正文

编程问答

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

发布时间:2024/1/23 编程问答 41 豆豆
生活随笔 收集整理的这篇文章主要介绍了 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的主要功能有哪些?

  • 可以在加载java文件之前做拦截把字节码做修改
  • 获取所有已经被加载过的类
  • 获取所有已经被初始化过了的类(执行过了clinit方法,是上面的一个子集)
  • 获取某个对象的大小
  • 将某个jar加入到bootstrapclasspath里作为高优先级被bootstrapClassloader加载
  • 将某个jar加入到classpath里供AppClassloard去加载
  • 设置某些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><transformerimplementation="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 {@Overridepublic 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,效果是一样的!

    总结

    以上是生活随笔为你收集整理的JVM插桩之三:javaagent介绍及javassist介绍的全部内容,希望文章能够帮你解决所遇到的问题。

    如果觉得生活随笔网站内容还不错,欢迎将生活随笔推荐给好友。