当前位置:   article > 正文

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

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方法的时候做规则匹配

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

看业务类定义:

  1. package com.dxz.chama.service;
  2. import java.util.LinkedList;
  3. import java.util.List;
  4. /**
  5. * 模拟数据插入服务
  6. *
  7. */
  8. public class InsertService {
  9. public void insert2(int num) {
  10. List<Integer> list = new LinkedList<>();
  11. for (int i = 0; i < num; i++) {
  12. list.add(i);
  13. }
  14. }
  15. public void insert1(int num) {
  16. List<Integer> list = new LinkedList<>();
  17. for (int i = 0; i < num; i++) {
  18. list.add(i);
  19. }
  20. }
  21. public void insert3(int num) {
  22. List<Integer> list = new LinkedList<>();
  23. for (int i = 0; i < num; i++) {
  24. list.add(i);
  25. }
  26. }
  27. }

删除服务:

  1. package com.dxz.chama.service;
  2. import java.util.List;
  3. public class DeleteService {
  4. public void delete(List<Integer>list){
  5. for (int i=0;i<list.size();i++){
  6. list.remove(i);
  7. }
  8. }
  9. }

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

定义agent的入口

  1. package com.dxz.chama.javaagent;
  2. import java.lang.instrument.Instrumentation;
  3. /**
  4. * agent的入口类
  5. */
  6. public class TimeMonitorAgent {
  7. // peremain 这个方法名称是固定写法 不能写错或修改
  8. public static void premain(String agentArgs, Instrumentation inst) {
  9. System.out.println("execute insert method interceptor....");
  10. System.out.println(agentArgs);
  11. // 添加自定义类转换器
  12. inst.addTransformer(new TimeMonitorTransformer(agentArgs));
  13. }
  14. }

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

  1. package com.dxz.chama.javaagent;
  2. import java.lang.instrument.ClassFileTransformer;
  3. import java.lang.instrument.IllegalClassFormatException;
  4. import java.lang.reflect.Modifier;
  5. import java.security.ProtectionDomain;
  6. import java.util.Objects;
  7. import javassist.ClassPool;
  8. import javassist.CtClass;
  9. import javassist.CtMethod;
  10. import javassist.CtNewMethod;
  11. /**
  12. * 类方法的字节码替换
  13. */
  14. public class TimeMonitorTransformer implements ClassFileTransformer {
  15. private static final String START_TIME = "\nlong startTime = System.currentTimeMillis();\n";
  16. private static final String END_TIME = "\nlong endTime = System.currentTimeMillis();\n";
  17. private static final String METHOD_RUTURN_VALUE_VAR = "__time_monitor_result";
  18. private static final String EMPTY = "";
  19. private String classNameKeyword;
  20. public TimeMonitorTransformer(String classNameKeyword){
  21. this.classNameKeyword = classNameKeyword;
  22. }
  23. /**
  24. *
  25. * @param classLoader 默认类加载器
  26. * @param className 类名的关键字 因为还会进行模糊匹配
  27. * @param classBeingRedefined
  28. * @param protectionDomain
  29. * @param classfileBuffer
  30. * @return
  31. * @throws IllegalClassFormatException
  32. */
  33. public byte[] transform(ClassLoader classLoader, String className, Class<?> classBeingRedefined,
  34. ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
  35. className = className.replace("/", ".");
  36. CtClass ctClass = null;
  37. try {
  38. //使用全称,用于取得字节码类
  39. ctClass = ClassPool.getDefault().get(className);
  40. //匹配类的机制是基于类的关键字 这个是客户端传过来的参数 满足就会获取所有的方法 不满足跳过
  41. if(Objects.equals(classNameKeyword, EMPTY)||(!Objects.equals(classNameKeyword, EMPTY)&&className.indexOf(classNameKeyword)!=-1)){
  42. //所有方法
  43. CtMethod[] ctMethods = ctClass.getDeclaredMethods();
  44. //遍历每一个方法
  45. for(CtMethod ctMethod:ctMethods){
  46. //修改方法的字节码
  47. transformMethod(ctMethod, ctClass);
  48. }
  49. }
  50. //重新返回修改后的类
  51. return ctClass.toBytecode();
  52. } catch (Exception e) {
  53. e.printStackTrace();
  54. }
  55. return null;
  56. }
  57. /**
  58. * 为每一个拦截到的方法 执行一个方法的耗时操作
  59. * @param ctMethod
  60. * @param ctClass
  61. * @throws Exception
  62. */
  63. private void transformMethod(CtMethod ctMethod, CtClass ctClass) throws Exception {
  64. // 抽象的方法是不能修改的,或者方法前面加了final关键字
  65. if ((ctMethod.getModifiers() & Modifier.ABSTRACT) > 0) {
  66. return;
  67. }
  68. //获取原始方法名称
  69. String methodName = ctMethod.getName();
  70. String monitorStr = "\nSystem.out.println(\"method " + ctMethod.getLongName() + " cost:\" + (endTime - startTime) + \"ms.\");";
  71. //实例化新的方法名称
  72. String newMethodName = methodName + "$impl";
  73. //设置新的方法名称
  74. ctMethod.setName(newMethodName);
  75. //创建新的方法,复制原来的方法,名字为原来的名字
  76. CtMethod newMethod = CtNewMethod.copy(ctMethod, methodName, ctClass, null);
  77. StringBuilder bodyStr = new StringBuilder();
  78. //拼接新的方法内容
  79. bodyStr.append("{");
  80. //返回类型
  81. CtClass returnType = ctMethod.getReturnType();
  82. //是否需要返回
  83. boolean hasReturnValue = (CtClass.voidType != returnType);
  84. if (hasReturnValue) {
  85. String returnClass = returnType.getName();
  86. bodyStr.append("\n").append(returnClass + " " + METHOD_RETURN_VALUE_VAR + ";");
  87. }
  88. bodyStr.append(START_TIME);
  89. if (hasReturnType) {
  90. bodyStr.append("\n").append(METHOD_RETURN_VALUE_VAR + " = ($r)" + newMethodName + "($$);");
  91. } else {
  92. bodyStr.append("\n").append(newMethodName + "($$);");
  93. }
  94. bodyStr.append(END_TIME);
  95. bodyStr.append(monitorStr);
  96. if (hasReturnValue) {
  97. bodyStr.append("\n").append("return " + METHOD_RETURN_VALUE_VAR + " ;");
  98. }
  99. bodyStr.append("}");
  100. //替换新方法
  101. newMethod.setBody(bodyStr.toString());
  102. //增加新方法
  103. ctClass.addMethod(newMethod);
  104. }
  105. }

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

  1. <project xmlns="http://maven.apache.org/POM/4.0.0"
  2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <groupId>com.dxz</groupId>
  6. <artifactId>chama</artifactId>
  7. <version>0.0.1-SNAPSHOT</version>
  8. <packaging>jar</packaging>
  9. <name>chama</name>
  10. <url>http://maven.apache.org</url>
  11. <properties>
  12. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  13. </properties>
  14. <dependencies>
  15. <dependency>
  16. <groupId>javassist</groupId>
  17. <artifactId>javassist</artifactId>
  18. <version>3.12.1.GA</version>
  19. </dependency>
  20. <!-- https://mvnrepository.com/artifact/cglib/cglib -->
  21. <dependency>
  22. <groupId>cglib</groupId>
  23. <artifactId>cglib</artifactId>
  24. <version>3.2.5</version>
  25. </dependency>
  26. <!-- https://mvnrepository.com/artifact/oro/oro -->
  27. <dependency>
  28. <groupId>oro</groupId>
  29. <artifactId>oro</artifactId>
  30. <version>2.0.8</version>
  31. </dependency>
  32. <dependency>
  33. <groupId>junit</groupId>
  34. <artifactId>junit</artifactId>
  35. <version>3.8.1</version>
  36. <scope>test</scope>
  37. </dependency>
  38. </dependencies>
  39. <build>
  40. <plugins>
  41. <plugin>
  42. <groupId>org.apache.maven.plugins</groupId>
  43. <artifactId>maven-compiler-plugin</artifactId>
  44. <configuration>
  45. <source>1.8</source>
  46. <target>1.8</target>
  47. <encoding>utf-8</encoding>
  48. </configuration>
  49. </plugin>
  50. <plugin>
  51. <groupId>org.apache.maven.plugins</groupId>
  52. <artifactId>maven-shade-plugin</artifactId>
  53. <version>3.0.0</version>
  54. <executions>
  55. <execution>
  56. <phase>package</phase>
  57. <goals>
  58. <goal>shade</goal>
  59. </goals>
  60. <configuration>
  61. <transformers>
  62. <transformer
  63. implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
  64. <manifestEntries>
  65. <Premain-Class>com.dxz.chama.javaagent.TimeMonitorAgent</Premain-Class>
  66. </manifestEntries>
  67. </transformer>
  68. </transformers>
  69. </configuration>
  70. </execution>
  71. </executions>
  72. </plugin>
  73. </plugins>
  74. </build>
  75. </project>

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

mvn -clean package

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

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

部署方式:

1 基于IDE开发环境运行

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

  1. package com.dxz.chama.service;
  2. import java.util.LinkedList;
  3. import java.util.List;
  4. public class ServiceTest {
  5. public static void main(String[] args) {
  6. // 插入服务
  7. InsertService insertService = new InsertService();
  8. // 删除服务
  9. DeleteService deleteService = new DeleteService();
  10. System.out.println("....begnin insert....");
  11. insertService.insert1(1003440);
  12. insertService.insert2(2000000);
  13. insertService.insert3(30003203);
  14. System.out.println(".....end insert.....");
  15. List<Integer> list = new LinkedList<>();
  16. for (int i = 0; i < 29988440; i++) {
  17. list.add(i);
  18. }
  19. System.out.println(".....begin delete......");
  20. deleteService.delete(list);
  21. System.out.println("......end delete........");
  22. }
  23. }

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

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

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

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

定义入口agent:

  1. package com.dxz.chama.javaagent.patter;
  2. import java.lang.instrument.Instrumentation;
  3. public class TimeMonitorPatterAgent {
  4. public static void premain(String agentArgs, Instrumentation inst) {
  5. inst.addTransformer(new PatternTransformer());
  6. }
  7. }

定义transformer:

  1. package com.dxz.chama.javaagent.patter;
  2. import javassist.CtClass;
  3. import org.apache.oro.text.regex.PatternCompiler;
  4. import org.apache.oro.text.regex.PatternMatcher;
  5. import org.apache.oro.text.regex.Perl5Compiler;
  6. import org.apache.oro.text.regex.Perl5Matcher;
  7. import java.lang.instrument.ClassFileTransformer;
  8. import java.lang.instrument.IllegalClassFormatException;
  9. import java.security.ProtectionDomain;
  10. public class PatternTransformer implements ClassFileTransformer {
  11. @Override
  12. public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
  13. ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
  14. PatternMatcher matcher = new Perl5Matcher();
  15. PatternCompiler compiler = new Perl5Compiler();
  16. // 指定的业务类
  17. String interceptorClass = "com.dxz.chama.service.InsertService";
  18. // 指定的方法
  19. String interceptorMethod = "insert1";
  20. try {
  21. if (matcher.matches(className, compiler.compile(interceptorClass))) {
  22. ByteCode byteCode = new ByteCode(0;
  23. CtClass ctClass = byteCode.modifyByteCode(interceptorClass, interceptorMethod);
  24. return ctClass.toBytecode(0;
  25. }
  26. } catch (Exception e) {
  27. e.printStackTrace();
  28. }
  29. return null;
  30. }
  31. }

修改字节码的实现:

  1. package com.dxz.chama.javaagent.patter;
  2. import javassist.ClassPool;
  3. import javassist.CtClass;
  4. import javassist.CtMethod;
  5. import javassist.CtNewMethod;
  6. public class ByteCode {
  7. public CtClass modifyByteCode(String className, String method) throws Exception {
  8. ClassPool classPool = ClassPool.getDefault();
  9. CtClass ctClass = classPool.get(className);
  10. CtMethod oldMethod = ctClass.getDeclaredMethod(method);
  11. String oldMethodName = oldMethod.getName(0;
  12. String newName = oldMethodName + "$impl";
  13. oldMethod.setName(newName);
  14. CtMethod newMethod = CtNewMethod.copy(oldMethod, oldMethodName, ctClass, null);
  15. StringBuffer sb = newe StringBuffer();
  16. sb.append("{");
  17. sb.append("\nSystem.out.println(\"start to modify bytecode\"); \n");
  18. sb.append(newName + "($$);\n");
  19. sb.append("System.out.println(\"call method" + oldMethodName + "took\"+(System.currentTimeMillis()-start))");
  20. sb.append("}");
  21. newMethod.setBody(sb.toString());
  22. ctClass.addMethod(newMethod);
  23. return ctClass;
  24. }
  25. }

OK,

修改下pom中的

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

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

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

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/450791
推荐阅读
相关标签
  

闽ICP备14008679号