定制化开发【微服务】springboot 整合javassist详解

一、前言

Javassist 定制化开发是一个开源,用于分析、定制化开发编辑和创建Java定制化开发字节码的类库,定制化开发由东京工业大学数学和定制化开发计算机科学系的 Shigeru Chiba (千叶滋)所创建。定制化开发目前已加入了开放源代码 定制化开发应用服务器项目,通过使用Javassist定制化开发对字节码操作为JBoss实现动态"AOP"框架。

通过使用Javassist可以使Java定制化开发程序在运行时定义一个新的类,并且在JVM定制化开发加载类文件时修改它

Javassist定制化开发提供两个级别的API:定制化开发源码级别和字节码级别。

定制化开发如果使用源码级的API,定制化开发开发人员可以在不知道Java定制化开发字节码的情况下编辑Java类文件,定制化开发就像我们编写Java定制化开发源代码一样方便。定制化开发如果使用字节码级别的API,定制化开发那么需要详细了解Java定制化开发字节码和类文件格式,定制化开发因为字节码级别的API定制化开发允许我们对类文件进行任意修改。

官网地址

二、Javassist 定制化开发中几个重要的类

在使用javassist定制化开发进行编码之前,有必要对javassist定制化开发理论知识做一个全面的定制化开发了解和学习;

Javassist 定制化开发中最为重要的是 ClassPool,CtClass ,CtMethod 以及 CtField 这几个类;

  • ClassPool:基于Hashtable 实现的CtClass 对象容器,定制化开发其中键是类名称,定制化开发值是表示该类的 CtClass ​​对象;
  • CtClass:CtClass 表示类,一个 CtClass (编译时类)定制化开发对象可以处理一个 class 文件,这些 CtClass 定制化开发对象可以从 ClassPool 获得;
  • CtMethods:定制化开发表示类中的方法;
  • CtFields :定制化开发表示类中的字段;

ClassPool

CtClass定制化开发对象的容器,常用的API如下:

  1. getDefault () —— 定制化开发返回默认的ClassPool ,单例模式,定制化开发一般通过该方法创建ClassPool;
  2. appendClassPath(ClassPath cp),  insertClassPath(ClassPath cp)  —— 将一个ClassPath定制化开发加到定制化开发类搜索路径末尾位置,定制化开发或插入到起始位置。定制化开发通过该方法写入额外的类搜索路径,以解决多个类加载器环境中找不到类问题;
  3. importPackage(String packageName) —— 导入包;
  4. makeClass(String classname) —— 创建一个空类,里面没有变量或方法,后面通过CtClass函数进行添加;
  5. get(String classname)、getCtClass(String classname)  ——  根据类路径名获取该类的CtClass对象,用于后续编辑;

1、获取 ClassPool 对象操作

  1. // 获取 ClassPool 对象,使用系统默认类路径
  2. ClassPool pool = new ClassPool(true);
  3. // 效果与 new ClassPool(true) 一致
  4. ClassPool pool1 = ClassPool.getDefault();

2、获取类操作

  1. // 通过类名获取 CtClass,未找到会抛出异常
  2. CtClass ctClass = pool.get("com.congge.service.DemoService");
  3. // 通过类名获取 CtClass,未找到返回 null,不会抛出异常
  4. CtClass ctClass1 = pool.getOrNull("com.congge.service.DemoService");

3、 创建新类操作

  1. // 复制一个类,创建一个新类
  2. CtClass ctClass2 = pool.getAndRename("com.congge.DemoService", "com.congge.DemoCopyService");
  3. // 通过类名,创建一个新类
  4. CtClass ctClass3 = pool.makeClass("com.congge.NewDemoService");
  5. // 通过文件流,创建一个新类,注意文件必须是编译后的 class 文件,不是源代码文件。
  6. CtClass ctClass4 = pool.makeClass(new FileInputStream(new File("./customize/DemoBeforeHandler.class")));

CtClass

通过 CtClass 对象,开发人员可以得到很多关于类的信息,就可以对类进行修改等操作,常用的API如下:

  • debugDump;String类型,如果生成。class文件,保存在这个目录下;
  • setName(String name):给类重命名;
  • setSuperclass(CtClass clazz):设置父类;
  • addField(CtField f, Initializer init):添加字段(属性),初始值见CtField;
  • addMethod(CtMethod m):添加方法(函数);
  • toBytecode(): 返回修改后的字节码。需要注意的是一旦调用该方法,则无法继续修改CtClass
  • toClass(): 将修改后的CtClass加载至当前线程的上下文类加载器中,CtClass的toClass方法是通过调用本方法实现。需要注意的是一旦调用该方法,则无法继续修改已经被加载的CtClass
  • writeFile(String directoryName):根据CtClass生成 .class 文件;
  • defrost():解冻类,用于使用了toclass()、toBytecode、writeFile(),类已经被JVM加载,Javassist冻结CtClass后;
  • detach():避免内存溢出,从ClassPool中移除一些不需要的CtClass;

获取类属性

  1. // 类名
  2. String simpleName = ctClass.getSimpleName();
  3. // 类全名
  4. String name = ctClass.getName();
  5. // 包名
  6. String packageName = ctClass.getPackageName();
  7. // 接口
  8. CtClass[] interfaces = ctClass.getInterfaces();
  9. // 继承类
  10. CtClass superclass = ctClass.getSuperclass();
  11. // 获取字节码文件,可以通过 ClassFile 对象进行字节码级操作
  12. ClassFile classFile = ctClass.getClassFile();
  13. // 获取带参数的方法,第二个参数为参数列表数组,类型为 CtClass
  14. CtMethod ctMethod = ctClass.getDeclaredMethod("selectOrder", new CtClass[] {pool.get(String.class.getName()), pool.get(String.class.getName())});
  15. // 获取字段
  16. CtField ctField = ctClass.getField("salary");

类型判断

  1. // 判断数组类型
  2. ctClass.isArray();
  3. // 判断原生类型
  4. ctClass.isPrimitive();
  5. // 判断接口类型
  6. ctClass.isInterface();
  7. // 判断枚举类型
  8. ctClass.isEnum();
  9. // 判断注解类型
  10. ctClass.isAnn

添加类属性

  1. // 添加接口
  2. ctClass.addInterface(...);
  3. // 添加构造器
  4. ctClass.addConstructor(...);
  5. // 添加字段
  6. ctClass.addField(...);
  7. // 添加方法
  8. ctClass.addMethod(...);

编译类

  1. // 编译成字节码文件,使用当前线程上下文类加载器加载类,如果类已存在或者编译失败将抛出异常
  2. Class clazz = ctClass.toClass();
  3. // 编辑成字节码文件,返回 byte 数组
  4. byte[] bytes = ctClass.toBytecode();

CtMethod

方法相关相关,常用的API如下:

  • insertBefore(String src) —— 在方法的起始位置插入代码;
  • insertAfter(String src) —— 在方法的所有 return 语句前插入代码以确保语句能够被执行,除非遇到exception;
  • insertAt(int lineNum, String src): —— 在指定的位置插入代码;
  • addCatch(String src, CtClass exceptionType) —— 将方法内语句作为try的代码块,插入catch代码块src;
  • setBody(String src) —— 将方法的内容设置为要写入的代码,当方法被 abstract修饰时,该修饰符被移除;
  • setModifiers(int mod) —— 设置访问级别,一般使用Modifier调用常量;
  • invoke(Object obj, Object... args) —— 反射调用字节码生成类的方法;

获取方法属性

  1. CtClass ctClass5 = pool.get(TestService.class.getName());
  2. CtMethod ctMethod = ctClass5.getDeclaredMethod("selectOrder");
  3. // 方法名
  4. String methodName = ctMethod.getName();
  5. // 返回类型
  6. CtClass returnType = ctMethod.getReturnType();
  7. // 方法参数,通过此种方式得到方法参数列表 格式:com.congge.UserService.selectUser(java.lang.String,java.util.List,com.entity.User)
  8. ctMethod.getLongName();
  9. // 方法签名 格式:(Ljava/lang/String;Ljava/util/List)Ljava/lang/Integer;
  10. ctMethod.getSignature();
  11. // 获取方法参数名称,可以通过这种方式得到方法真实参数名称
  12. List<String> argKeys = new ArrayList<>();
  13. MethodInfo methodInfo = ctMethod.getMethodInfo();
  14. CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
  15. LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
  16. int len = ctMethod.getParameterTypes().length;
  17. // 非静态的成员函数的第一个参数是this
  18. int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1;
  19. for (int i = pos; i < len; i++) {
  20. argKeys.add(attr.variableName(i));
  21. }

方法操作

  1. // 在方法体前插入代码块
  2. ctMethod.insertBefore("");
  3. // 在方法体后插入代码块
  4. ctMethod.insertAfter("");
  5. // 在某行 字节码 后插入代码块
  6. ctMethod.insertAt(10, "");
  7. // 添加参数
  8. ctMethod.addParameter(CtClass);
  9. // 设置方法名
  10. ctMethod.setName("newName");
  11. // 设置方法体
  12. ctMethod.setBody("");

对于setBody $0代表this $1、$2、...代表方法的第几个参数,$符号含义总结如下:

符号含义
$0, $1, $2, ...  this,第几个参数
$args参数列表. $args的类型是Object[].
$$所有实参.例如, m($$) 等价于 m($1,$2,...)
$cflow(...)cflow变量
$r结果类型. 用于表达式转换.
$w包装类型. 用于表达式转换.
$_结果值
$sigjava.lang.Class列表,代表正式入参类型
$typejava.lang.Class对象,代表正式入参值.
$classjava.lang.Class对象,代表传入的代码段.

CtField

字段相关,常用的API如下:

  • CtField(CtClass type, String name, CtClass declaring)  —— 构造函数,添加字段类型,名称,所属的类;
  • CtField.Initializer constant() —— CtClass使用addField时初始值的设置;
  • setModifiers(int mod) —— 设置访问级别,一般使用Modifier调用常量;

Javassist API操作综合使用案例

导入依赖

  1. <dependency>
  2. <groupId>org.javassist</groupId>
  3. <artifactId>javassist</artifactId>
  4. <version>3.27.0-GA</version>
  5. </dependency>

1、使用javassist创建类

  1. public static void main(String[] args) throws Exception {
  2. ClassPool pool = new ClassPool(true);
  3. CtClass targetClass = pool.get("com.congge.test.HelloServiceImpl");
  4. CtMethod method = targetClass.getDeclaredMethod("sayHello");
  5. // 复制方法生成一个新的代理方法
  6. CtMethod agentMethod = CtNewMethod.copy(method, method.getName()+"$agent", targetClass, null);
  7. agentMethod.setModifiers(Modifier.PRIVATE);
  8. // 添加方法
  9. targetClass.addMethod(agentMethod);
  10. // 构建新的方法体,并使用代理方法
  11. String source = "{"
  12. + "System.out.println(\"before handle > ...\" + $type);"
  13. + method.getName() + "$agent($$);"
  14. + "System.out.println(\"after handle ...\");"
  15. + "}"
  16. ;
  17. // 设置方法体
  18. method.setBody(source);
  19. targetClass.toClass();
  20. IHello hello = new HelloServiceImpl();
  21. hello.sayHello("javassist");
  22. }

运行上面的代码,观察输出结果,通过该案例就动态创建出了一个接口实现类

 

2、创建代理方法

  1. import javassist.*;
  2. public class JavaSisstWord {
  3. public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {
  4. ClassPool pool = new ClassPool(true);
  5. pool.insertClassPath(new LoaderClassPath(JavaSisstWord.class.getClassLoader()));
  6. //构建一个新的类
  7. CtClass targetClass = pool.makeClass("com.congge.hello");
  8. targetClass.addInterface(pool.get(IHello.class.getName()));
  9. //将方法添加进去
  10. CtClass returnType = pool.get(void.class.getName());
  11. String name = "sayHello";
  12. CtClass[] parameters = new CtClass[]{pool.get(String.class.getName())};
  13. CtMethod method = new CtMethod(returnType,name,parameters,targetClass);
  14. String src = "{System.out.println(\"hello :\" + $1);}";
  15. method.setBody(src);
  16. targetClass.addMethod(method);
  17. //装载class
  18. Class aClass = targetClass.toClass();
  19. IHello hello = (IHello) aClass.newInstance();
  20. hello.sayHello("新的class的参数");
  21. }
  22. public interface IHello{
  23. void sayHello(String name);
  24. }
  25. }

可以结合下面这张图总结一下javassist的运行流程

 

三、Javaagent

在上一篇,用较大的篇幅总结了javaagent的使用, ,对于Java 程序员来说,Java Intrumentation、Java agent 这些技术可能平时接触的很少。事实上,在我们日常开发中接触到的各种工具中,有很多都是基于javaagent原理实现的,如(JRebel, spring-loaded)、IDE debug、各种线上诊断工具(btrace,Arthas,skywalking)等。

java agent实现技术也很多,比如本篇接下去要讲的javassist,asm等,都是可以实现的,关于java agent,先介绍几个重要的底层接口类;

Instrumentation

使用 java.lang.instrument.Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。

有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。

Instrumentation 的最大作用,就是类定义动态改变和操作

Instrumentation的一些主要方法如下:

  1. public interface Instrumentation {
  2. /**
  3. * 注册一个Transformer,从此之后的类加载都会被 transformer 拦截。
  4. * ClassFileTransformer 的 transform 方法可以直接对类的字节码进行修改,但是只能修改方法体,不能变更方法签名、增加和删除方法/类的成员属性
  5. */
  6. void addTransformer(ClassFileTransformer transformer);
  7. /**
  8. * 对JVM已经加载的类重新触发类加载,使用上面注册的 ClassFileTransformer 重新对类进行修饰。
  9. */
  10. void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
  11. /**
  12. * 重新定义类,不是使用 transformer 修饰,而是把处理结果(bytecode)直接给JVM。
  13. * 调用此方法同样只能修改方法体,不能变更方法签名、增加和删除方法/类的成员属性
  14. */
  15. void redefineClasses(ClassDefinition... definitions) throws ClassNotFoundException, UnmodifiableClassException;
  16. /**
  17. * 获取一个对象的大小
  18. */
  19. long getObjectSize(Object objectToSize);
  20. /**
  21. * 将一个jar加入到bootstrap classloader 的 classpath 里
  22. */
  23. void appendToBootstrapClassLoaderSearch(JarFile jarfile);
  24. /**
  25. * 将一个jar加入到 system classloader 的 classpath 里
  26. */
  27. void appendToSystemClassLoaderSearch(JarFile jarfile);
  28. /**
  29. * 获取当前被JVM加载的所有类对象
  30. */
  31. Class[] getAllLoadedClasses();
  32. }

Javaagent

  • Java agent 是一种特殊的Java程序(Jar文件),它是 Instrumentation 的客户端具体实现;
  • 与普通 Java 程序通过main方法启动不同,agent 并不是一个可以单独启动的程序,而必须依附在一个Java应用程序(JVM)上,与它运行在同一个进程中,通过 Instrumentation API 与虚拟机交互;
  • Java agent 与 Instrumentation 密不可分,二者也需要在一起使用。因为JVM 会把 Instrumentation 的实例会作为参数注入到 Java agent 的启动方法中。因此如果想使用 Instrumentation 功能,拿到 Instrumentation 实例,我们必须通过Java agent;

Java agent 有两个启动时机,一个是在程序启动时通过 -javaagent 参数启动代理程序,另一个是在程序运行期间通过 Java Tool API 中的 attach api 动态启动代理程序;

JVM启动时静态加载

对于JVM启动时加载的 agent,Instrumentation 会通过 premain 方法传入代理程序,premain 方法会在程序 main 方法执行之前被调用。

此时大部分Java类都没有被加载(“大部分”是因为,agent类本身和它依赖的类还是无法避免的会先加载的),是一个对类加载埋点做手脚(addTransformer)的好机会。但这种方式有很大的局限性,Instrumentation 仅限于 main 函数执行前,此时有很多类还没有被加载,如果想为其注入 Instrumentation 就无法办到。

这种方式的应用:例如在 IDEA 启动 debug 模式时,就是以 -javaagent 的形式启动 debug 代理程序实现的

  1. /**
  2. * agentArgs 是 premain 函数得到的程序参数,通过 -javaagent 传入。这个参数是个字符串,如果程序参数有多个,需要程序自行解析这个字符串。
  3. * inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。
  4. */
  5. public static void premain(String agentArgs, Instrumentation inst) {
  6. }
  7. /**
  8. * 带有 Instrumentation 参数的 premain 优先级高于不带此参数的 premain。
  9. * 如果存在带 Instrumentation 参数的 premain,不带此参数的 premain 将被忽略。
  10. */
  11. public static void premain(String agentArgs) {
  12. }


如下面这段代码,按照上一篇文章,将MyPreMainAgent 配置并打包后,其他类启动参数配置了这个jar就会先于方法输出这段结果;

  1. public class MyPreMainAgent {
  2. public static void premain(String agentArgs, Instrumentation inst) {
  3. System.out.println("hello javaAgent");
  4. }
  5. }

 

 JVM 启动后动态加载

对于VM启动后动态加载的 agent,Instrumentation 会通过 agentmain 方法传入代理程序,agentmain 在 main 函数开始运行后才被调用;

这种方式,比如在使用 Arthas 进行诊断线上问题时,通过 attach api,来动态加载代理程序到目标VM;

  1. /**
  2. * agentArgs 是 agentmain 函数得到的程序参数,在 attach 时传入。这个参数是个字符串,如果程序参数有多个,需要程序自行解析这个字符串。
  3. * inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。
  4. */
  5. public static void agentmain(String agentArgs, Instrumentation inst) {
  6. }
  7. /**
  8. * 带有 Instrumentation 参数的 agentmain 优先级高于不带此参数的 agentmain。
  9. * 如果存在带 Instrumentation 参数的 agentmain,不带此参数的 agentmain 将被忽略。
  10. */
  11. public static void agentmain(String agentArgs) {
  12. }

MANIFEST.MF

编写好的代理类想要运行,在打 jar 包前,还需在 MANIFEST.MF 中指定代理程序入口(当然,也可以在maven的pom文件中进行插件化形式的配置,效果类似);

大多数 JAR 文件会包含一个 META-INF 目录,它用于存储包和扩展的配置数据,如安全性和版本信息。其中会有一个 MANIFEST.MF 文件,该文件包含了该 Jar 包的版本、创建人和类搜索路径等信息,如果是可执行Jar 包,会包含Main-Class属性,表明 Main 方法入口;

例如下面是通过 mvn clean package 命令打包后的 Jar 包中的 MANIFEST.MF 文件,从中可以看出 jar 的版本、创建者、SpringBoot 版本、程序入口、类搜索路径等信息。
 

 

 

其中涉及到与agent相关的参数

  • Premain-Class:JVM 启动时指定了代理,此属性指定代理类,即包含 premain 方法的类;
  • Agent-Class:JVM动态加载代理,此属性指定代理类,即包含 agentmain 方法的类;
  • Boot-Class-Path:设置引导类加载器搜索的路径列表,列表中的路径由一个或多个空格分开;
  • Can-Redefine-Classes:布尔值(true 或 false)。是否能重定义此代理所需的类;
  • Can-Retransform-Classes:布尔值(true 或 false)。是否能重转换此代理所需的类;
  • Can-Set-Native-Method-Prefix:布尔值(true 或 false)。是否能设置此代理所需的本机方法前缀;

四、基于javassit实现对coontroller层的监控

通常在实际的业务开发中,我们可能会碰到类似下面这样的需求

  • 拦截指定包下的所有业务类,进行方法参数合规性校验;
  • 对特定的接口请求进行限流;
  • 对特定的方法进行参数的日志审计;
  • ...

遇到这样的需求,很多同学第一反应大多会想到AOP,没毛病,使用aop来解决这个问题是个不错的思路,但还是那句话,有了javaagent之后,可以尽可能的让开发人员少改动现有的代码,接下来,考虑下如果在业务中要实现对某个controller进行参数,返回值的监控,该如何做呢?接下来看完整的实现步骤;

1、导入相关依赖

  1. <dependencies>
  2. <dependency>
  3. <groupId>junit</groupId>
  4. <artifactId>junit</artifactId>
  5. <version>4.12</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.springframework.boot</groupId>
  9. <artifactId>spring-boot-starter-web</artifactId>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.javassist</groupId>
  13. <artifactId>javassist</artifactId>
  14. <version>3.27.0-GA</version>
  15. </dependency>
  16. <dependency>
  17. <groupId>com.alibaba</groupId>
  18. <artifactId>fastjson</artifactId>
  19. <version>1.2.67</version>
  20. </dependency>
  21. </dependencies>
  22. <build>
  23. <plugins>
  24. <plugin>
  25. <groupId>org.apache.maven.plugins</groupId>
  26. <artifactId>maven-jar-plugin</artifactId>
  27. <version>3.1.0</version>
  28. <configuration>
  29. <archive>
  30. <manifest>
  31. <addClasspath>true</addClasspath>
  32. </manifest>
  33. <manifestEntries>
  34. <Premain-Class>com.congge.agent.jvm.AgentMain2</Premain-Class>
  35. <Can-Redefine-Classes>true</Can-Redefine-Classes>
  36. <Can-Retransform-Classes>true</Can-Retransform-Classes>
  37. <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
  38. </manifestEntries>
  39. </archive>
  40. </configuration>
  41. </plugin>
  42. </plugins>
  43. </build>

2、提供一个测试用的接口类

  1. import org.springframework.web.bind.annotation.GetMapping;
  2. import org.springframework.web.bind.annotation.RestController;
  3. @RestController
  4. public class UserController {
  5. @GetMapping("/queryUserInfo")
  6. public String queryUserInfo(String userId){
  7. return "hello :" + userId;
  8. }
  9. }

3、编写agent类

  1. import java.io.IOException;
  2. import java.lang.instrument.ClassFileTransformer;
  3. import java.lang.instrument.IllegalClassFormatException;
  4. import java.lang.instrument.Instrumentation;
  5. import java.security.ProtectionDomain;
  6. import java.util.ArrayList;
  7. import java.util.HashSet;
  8. import java.util.List;
  9. import java.util.Set;
  10. public class AgentMain2 {
  11. private static final Set<String> classNameSet = new HashSet<>();
  12. static {
  13. classNameSet.add("com.congge.controller.UserController");
  14. }
  15. public static void premain(String agentArgs, Instrumentation instrumentation) {
  16. final ClassPool pool = new ClassPool();
  17. pool.appendSystemPath();
  18. //基于工具,在运行的时候修改class字节码,即动态插装
  19. instrumentation.addTransformer(new ClassFileTransformer() {
  20. @Override
  21. public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
  22. String currentClassName = className.replaceAll("/", ".");
  23. if (!classNameSet.contains(currentClassName)) { // 提升classNameSet中含有的类
  24. return null;
  25. }
  26. if(classNameSet.contains(currentClassName)){
  27. // 获取类
  28. //CtClass ctClass = ClassPool.getDefault().get(currentClassName);
  29. CtClass ctClass = null;
  30. try {
  31. ctClass = pool.getDefault().get(currentClassName);
  32. } catch (NotFoundException e) {
  33. e.printStackTrace();
  34. }
  35. String clazzName = ctClass.getName();
  36. // 获取方法
  37. CtMethod ctMethod = null;
  38. try {
  39. ctMethod = ctClass.getDeclaredMethod("queryUserInfo");
  40. } catch (NotFoundException e) {
  41. e.printStackTrace();
  42. }
  43. String methodName = ctMethod.getName();
  44. // 方法信息:methodInfo.getDescriptor();
  45. MethodInfo methodInfo = ctMethod.getMethodInfo();
  46. // 方法:入参信息
  47. CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
  48. LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
  49. CtClass[] parameterTypes = new CtClass[0];
  50. try {
  51. parameterTypes = ctMethod.getParameterTypes();
  52. } catch (NotFoundException e) {
  53. e.printStackTrace();
  54. }
  55. boolean isStatic = (methodInfo.getAccessFlags() & AccessFlag.STATIC) != 0; // 判断是否为静态方法
  56. int parameterSize = isStatic ? attr.tableLength() : attr.tableLength() - 1; // 静态类型取值
  57. List<String> parameterNameList = new ArrayList<>(parameterSize); // 入参名称
  58. List<String> parameterTypeList = new ArrayList<>(parameterSize); // 入参类型
  59. StringBuilder parameters = new StringBuilder(); // 参数组装;$1、$2...,$$可以获取全部,但是不能放到数组初始化
  60. for (int i = 0; i < parameterSize; i++) {
  61. parameterNameList.add(attr.variableName(i + (isStatic ? 0 : 1))); // 静态类型去掉第一个this参数
  62. parameterTypeList.add(parameterTypes[i].getName());
  63. if (i + 1 == parameterSize) {
  64. parameters.append("$").append(i + 1);
  65. } else {
  66. parameters.append("$").append(i + 1).append(",");
  67. }
  68. }
  69. // 方法:出参信息
  70. CtClass returnType = null;
  71. try {
  72. returnType = ctMethod.getReturnType();
  73. } catch (NotFoundException e) {
  74. e.printStackTrace();
  75. }
  76. String returnTypeName = returnType.getName();
  77. // 方法:生成方法唯一标识ID
  78. int idx = Monitor.generateMethodId(clazzName, methodName, parameterNameList, parameterTypeList, returnTypeName);
  79. // 定义属性
  80. try {
  81. ctMethod.addLocalVariable("startNanos", CtClass.longType);
  82. } catch (CannotCompileException e) {
  83. e.printStackTrace();
  84. }
  85. try {
  86. ctMethod.addLocalVariable("parameterValues", ClassPool.getDefault().get(Object[].class.getName()));
  87. } catch (CannotCompileException e) {
  88. e.printStackTrace();
  89. } catch (NotFoundException e) {
  90. e.printStackTrace();
  91. }
  92. // 方法前加强
  93. try {
  94. ctMethod.insertBefore("{ startNanos = System.nanoTime(); parameterValues = new Object[]{" + parameters.toString() + "}; }");
  95. } catch (CannotCompileException e) {
  96. e.printStackTrace();
  97. }
  98. // 方法后加强
  99. try {
  100. ctMethod.insertAfter("{ com.congge.agent.jvm.Monitor.point(" + idx + ", startNanos, parameterValues, $_);}", false); // 如果返回类型非对象类型,$_ 需要进行类型转换
  101. } catch (CannotCompileException e) {
  102. e.printStackTrace();
  103. }
  104. // 方法;添加TryCatch
  105. try {
  106. ctMethod.addCatch("{ com.congge.agent.jvm.Monitor.point(" + idx + ", $e); throw $e; }", ClassPool.getDefault().get("java.lang.Exception")); // 添加异常捕获
  107. } catch (CannotCompileException e) {
  108. e.printStackTrace();
  109. } catch (NotFoundException e) {
  110. e.printStackTrace();
  111. }
  112. try {
  113. return ctClass.toBytecode();
  114. } catch (IOException e) {
  115. e.printStackTrace();
  116. } catch (CannotCompileException e) {
  117. e.printStackTrace();
  118. }
  119. }
  120. return null;
  121. }
  122. });
  123. }
  124. }

该类的主要实现思路,就是重写premain方法,并覆盖其中的instrumentation的实现,在instrumentation的实现中,充分利用javassist提供的相关API,拦截并获取目标UserController的方法的参数,以及执行结果;

4、将上面的agent所在的类配置到pom下并打包

 

5、启动springboot工程并在VM中配置如下参数

-javaagent:E:\code-self\spi\java-agent\target\java-agent-1.0-SNAPSHOT.jar=com.congge.agent.User

 

 

6、测试结果

启动完成后,浏览器访问下接口,并观察控制台输出结果;

 

 

通过控制台结果输出,在agent中需要监控拦截的信息就可以拿到了,那么拿到这些信息之后,理论上来说,就可以做更多的事情了,比如,上报异常参数,执行结果等等;

本段代码中,逻辑是写在一起的,而且只监控了UserController这一个类,在实际开发中,可以通过更灵活的方式去做,比如写到配置文件读取,通过自定义注解,或者扫描某个包路径等等;

代码优化改进

按照上面的思路,为了让这段代码更具通用性,我们可以直针对特定注解的类进行监控,同时对这样的目标类下的所有方法进行拦截,改进后的代码如下:

  1. import javassist.*;
  2. import javassist.bytecode.AccessFlag;
  3. import javassist.bytecode.CodeAttribute;
  4. import javassist.bytecode.LocalVariableAttribute;
  5. import javassist.bytecode.MethodInfo;
  6. import java.io.IOException;
  7. import java.lang.instrument.ClassFileTransformer;
  8. import java.lang.instrument.IllegalClassFormatException;
  9. import java.lang.instrument.Instrumentation;
  10. import java.security.ProtectionDomain;
  11. import java.util.*;
  12. public class AgentMain3 {
  13. private static final Set<String> classNameSet = new HashSet<>();
  14. static {
  15. classNameSet.add("com.congge.controller.UserController");
  16. }
  17. public static void premain(String agentArgs, Instrumentation instrumentation) {
  18. final ClassPool pool = new ClassPool();
  19. pool.appendSystemPath();
  20. //基于工具,在运行的时候修改class字节码,即动态插装
  21. instrumentation.addTransformer(new ClassFileTransformer() {
  22. @Override
  23. public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
  24. String currentClassName = className.replaceAll("/", ".");
  25. if (!classNameSet.contains(currentClassName)) { // 提升classNameSet中含有的类
  26. return null;
  27. }
  28. if(classNameSet.contains(currentClassName)){
  29. // 获取类
  30. //CtClass ctClass = ClassPool.getDefault().get(currentClassName);
  31. CtClass ctClass = null;
  32. try {
  33. ctClass = pool.getDefault().get(currentClassName);
  34. try {
  35. Object[] annotations = ctClass.getAnnotations();
  36. for (Object obj : annotations) {
  37. if (!obj.toString().startsWith("@org.springframework.web.bind.annotation.RestController")) {
  38. continue;
  39. }
  40. }
  41. } catch (ClassNotFoundException e) {
  42. e.printStackTrace();
  43. }
  44. } catch (NotFoundException e) {
  45. e.printStackTrace();
  46. }
  47. String clazzName = ctClass.getName();
  48. // 获取方法
  49. //CtMethod ctMethod = null;
  50. try {
  51. CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
  52. if(Objects.nonNull(declaredMethods) && declaredMethods.length >0){
  53. for(CtMethod ctMethod1 : declaredMethods){
  54. CtMethod ctMethod = ctClass.getDeclaredMethod(ctMethod1.getName());
  55. doHandleMethod(clazzName, ctMethod);
  56. }
  57. }
  58. } catch (NotFoundException e) {
  59. e.printStackTrace();
  60. }
  61. try {
  62. return ctClass.toBytecode();
  63. } catch (IOException e) {
  64. e.printStackTrace();
  65. } catch (CannotCompileException e) {
  66. e.printStackTrace();
  67. }
  68. }
  69. return null;
  70. }
  71. });
  72. }
  73. private static void doHandleMethod(String clazzName, CtMethod ctMethod) {
  74. String methodName = ctMethod.getName();
  75. // 方法信息:methodInfo.getDescriptor();
  76. MethodInfo methodInfo = ctMethod.getMethodInfo();
  77. // 方法:入参信息
  78. CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
  79. LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
  80. CtClass[] parameterTypes = new CtClass[0];
  81. try {
  82. parameterTypes = ctMethod.getParameterTypes();
  83. } catch (NotFoundException e) {
  84. e.printStackTrace();
  85. }
  86. boolean isStatic = (methodInfo.getAccessFlags() & AccessFlag.STATIC) != 0; // 判断是否为静态方法
  87. int parameterSize = isStatic ? attr.tableLength() : attr.tableLength() - 1; // 静态类型取值
  88. List<String> parameterNameList = new ArrayList<>(parameterSize); // 入参名称
  89. List<String> parameterTypeList = new ArrayList<>(parameterSize); // 入参类型
  90. StringBuilder parameters = new StringBuilder(); // 参数组装;$1、$2...,$$可以获取全部,但是不能放到数组初始化
  91. for (int i = 0; i < parameterSize; i++) {
  92. parameterNameList.add(attr.variableName(i + (isStatic ? 0 : 1))); // 静态类型去掉第一个this参数
  93. parameterTypeList.add(parameterTypes[i].getName());
  94. if (i + 1 == parameterSize) {
  95. parameters.append("$").append(i + 1);
  96. } else {
  97. parameters.append("$").append(i + 1).append(",");
  98. }
  99. }
  100. // 方法:出参信息
  101. CtClass returnType = null;
  102. try {
  103. returnType = ctMethod.getReturnType();
  104. } catch (NotFoundException e) {
  105. e.printStackTrace();
  106. }
  107. String returnTypeName = returnType.getName();
  108. // 方法:生成方法唯一标识ID
  109. int idx = Monitor.generateMethodId(clazzName, methodName, parameterNameList, parameterTypeList, returnTypeName);
  110. // 定义属性
  111. try {
  112. ctMethod.addLocalVariable("startNanos", CtClass.longType);
  113. } catch (CannotCompileException e) {
  114. e.printStackTrace();
  115. }
  116. try {
  117. ctMethod.addLocalVariable("parameterValues", ClassPool.getDefault().get(Object[].class.getName()));
  118. } catch (CannotCompileException e) {
  119. e.printStackTrace();
  120. } catch (NotFoundException e) {
  121. e.printStackTrace();
  122. }
  123. // 方法前加强
  124. try {
  125. ctMethod.insertBefore("{ startNanos = System.nanoTime(); parameterValues = new Object[]{" + parameters.toString() + "}; }");
  126. } catch (CannotCompileException e) {
  127. e.printStackTrace();
  128. }
  129. // 方法后加强
  130. try {
  131. ctMethod.insertAfter("{ com.congge.agent.jvm.Monitor.point(" + idx + ", startNanos, parameterValues, $_);}", false); // 如果返回类型非对象类型,$_ 需要进行类型转换
  132. } catch (CannotCompileException e) {
  133. e.printStackTrace();
  134. }
  135. // 方法;添加TryCatch
  136. try {
  137. ctMethod.addCatch("{ com.congge.agent.jvm.Monitor.point(" + idx + ", $e); throw $e; }", ClassPool.getDefault().get("java.lang.Exception")); // 添加异常捕获
  138. } catch (CannotCompileException e) {
  139. e.printStackTrace();
  140. } catch (NotFoundException e) {
  141. e.printStackTrace();
  142. }
  143. }
  144. }

为了模拟出效果,我们在UserController中再增加一个方法

  1. @RestController
  2. public class UserController {
  3. @GetMapping("/queryUserInfo")
  4. public String queryUserInfo(String userId){
  5. return "hello :" + userId;
  6. }
  7. @GetMapping("/queryUserInfo2")
  8. public String queryUserInfo2(String userName){
  9. return "hello :" + userName;
  10. }
  11. }

按照上面的步骤再次完成配置之后,再次启动工程进行测试,依次访问下面的接口,,观察控制台输出效果:

  1. http://localhost:8087/queryUserInfo?userId=222
  2. http://localhost:8087/queryUserInfo2?userName=javassist

 

 

如果需要监控更多的业务类,或者特定注解的类,也可以尝试类似的思路,比如我们要监控业务实现层的方法等,均可借鉴。

网站建设定制开发 软件系统开发定制 定制软件开发 软件开发定制 定制app开发 app开发定制 app开发定制公司 电商商城定制开发 定制小程序开发 定制开发小程序 客户管理系统开发定制 定制网站 定制开发 crm开发定制 开发公司 小程序开发定制 定制软件 收款定制开发 企业网站定制开发 定制化开发 android系统定制开发 定制小程序开发费用 定制设计 专注app软件定制开发 软件开发定制定制 知名网站建设定制 软件定制开发供应商 应用系统定制开发 软件系统定制开发 企业管理系统定制开发 系统定制开发