【微服务】springboot 整合javassist详解

news2024/11/17 12:39:08

一、前言

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

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

Javassist提供两个级别的API:源码级别和字节码级别。

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

官网地址:Javassist官方教程

二、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 对象操作

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

2、获取类操作

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

3、 创建新类操作

// 复制一个类,创建一个新类
CtClass ctClass2 = pool.getAndRename("com.congge.DemoService", "com.congge.DemoCopyService");
// 通过类名,创建一个新类
CtClass ctClass3 = pool.makeClass("com.congge.NewDemoService");
// 通过文件流,创建一个新类,注意文件必须是编译后的 class 文件,不是源代码文件。
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;

获取类属性

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

类型判断

// 判断数组类型
ctClass.isArray();
// 判断原生类型
ctClass.isPrimitive();
// 判断接口类型
ctClass.isInterface();
// 判断枚举类型
ctClass.isEnum();
// 判断注解类型
ctClass.isAnn

添加类属性

// 添加接口
ctClass.addInterface(...);
// 添加构造器
ctClass.addConstructor(...);
// 添加字段
ctClass.addField(...);
// 添加方法
ctClass.addMethod(...);

编译类

// 编译成字节码文件,使用当前线程上下文类加载器加载类,如果类已存在或者编译失败将抛出异常
Class clazz = ctClass.toClass();
// 编辑成字节码文件,返回 byte 数组
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) —— 反射调用字节码生成类的方法;

获取方法属性

CtClass ctClass5 = pool.get(TestService.class.getName());
CtMethod ctMethod = ctClass5.getDeclaredMethod("selectOrder");
// 方法名
String methodName = ctMethod.getName();
// 返回类型
CtClass returnType = ctMethod.getReturnType();
// 方法参数,通过此种方式得到方法参数列表 格式:com.congge.UserService.selectUser(java.lang.String,java.util.List,com.entity.User)
ctMethod.getLongName();
// 方法签名 格式:(Ljava/lang/String;Ljava/util/List)Ljava/lang/Integer;
ctMethod.getSignature();

// 获取方法参数名称,可以通过这种方式得到方法真实参数名称
List<String> argKeys = new ArrayList<>();
MethodInfo methodInfo = ctMethod.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
int len = ctMethod.getParameterTypes().length;
// 非静态的成员函数的第一个参数是this
int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1;
for (int i = pos; i < len; i++) {
    argKeys.add(attr.variableName(i));
}

方法操作

// 在方法体前插入代码块
ctMethod.insertBefore("");
// 在方法体后插入代码块
ctMethod.insertAfter("");
// 在某行 字节码 后插入代码块
ctMethod.insertAt(10, "");
// 添加参数
ctMethod.addParameter(CtClass);
// 设置方法名
ctMethod.setName("newName");
// 设置方法体
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操作综合使用案例

导入依赖

        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.27.0-GA</version>
        </dependency>

1、使用javassist创建类

public static void main(String[] args) throws Exception {
        ClassPool pool = new ClassPool(true);
        CtClass targetClass = pool.get("com.congge.test.HelloServiceImpl");
        CtMethod method = targetClass.getDeclaredMethod("sayHello");
        // 复制方法生成一个新的代理方法
        CtMethod agentMethod = CtNewMethod.copy(method, method.getName()+"$agent", targetClass, null);
        agentMethod.setModifiers(Modifier.PRIVATE);
        // 添加方法
        targetClass.addMethod(agentMethod);
        // 构建新的方法体,并使用代理方法
        String source = "{"
                + "System.out.println(\"before handle >  ...\" + $type);"
                + method.getName() + "$agent($$);"
                + "System.out.println(\"after handle ...\");"
                + "}"
                ;
        // 设置方法体
        method.setBody(source);
        targetClass.toClass();
        IHello hello = new HelloServiceImpl();
        hello.sayHello("javassist");
    }

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

 

2、创建代理方法

import javassist.*;

public class JavaSisstWord {

    public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException {
        ClassPool pool = new ClassPool(true);
        pool.insertClassPath(new LoaderClassPath(JavaSisstWord.class.getClassLoader()));

        //构建一个新的类
        CtClass targetClass = pool.makeClass("com.congge.hello");
        targetClass.addInterface(pool.get(IHello.class.getName()));

        //将方法添加进去
        CtClass returnType = pool.get(void.class.getName());
        String name = "sayHello";
        CtClass[] parameters = new CtClass[]{pool.get(String.class.getName())};
        CtMethod method = new CtMethod(returnType,name,parameters,targetClass);
        String src = "{System.out.println(\"hello :\" + $1);}";
        method.setBody(src);
        targetClass.addMethod(method);

        //装载class
        Class aClass = targetClass.toClass();
        IHello hello = (IHello) aClass.newInstance();
        hello.sayHello("新的class的参数");
    }

    public interface IHello{
        void sayHello(String name);
    }

}

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

 

三、Javaagent

在上一篇,用较大的篇幅总结了javaagent的使用,java agent使用 ,对于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的一些主要方法如下:

public interface Instrumentation {
    /**
     * 注册一个Transformer,从此之后的类加载都会被 transformer 拦截。
     * ClassFileTransformer 的 transform 方法可以直接对类的字节码进行修改,但是只能修改方法体,不能变更方法签名、增加和删除方法/类的成员属性
     */
    void addTransformer(ClassFileTransformer transformer);
    
    /**
     * 对JVM已经加载的类重新触发类加载,使用上面注册的 ClassFileTransformer 重新对类进行修饰。
     */
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;

    /**
     * 重新定义类,不是使用 transformer 修饰,而是把处理结果(bytecode)直接给JVM。
     * 调用此方法同样只能修改方法体,不能变更方法签名、增加和删除方法/类的成员属性
     */
    void redefineClasses(ClassDefinition... definitions) throws  ClassNotFoundException, UnmodifiableClassException;

    /**
     * 获取一个对象的大小
     */
    long getObjectSize(Object objectToSize);
    
    /**
     * 将一个jar加入到bootstrap classloader 的 classpath 里
     */
    void appendToBootstrapClassLoaderSearch(JarFile jarfile);

    /**
     * 将一个jar加入到 system classloader 的 classpath 里
     */
    void appendToSystemClassLoaderSearch(JarFile jarfile);

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

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 代理程序实现的

/**
 * agentArgs 是 premain 函数得到的程序参数,通过 -javaagent 传入。这个参数是个字符串,如果程序参数有多个,需要程序自行解析这个字符串。
 * inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。
 */
public static void premain(String agentArgs, Instrumentation inst) {
    
}

/**
 * 带有 Instrumentation 参数的 premain 优先级高于不带此参数的 premain。
 * 如果存在带 Instrumentation 参数的 premain,不带此参数的 premain 将被忽略。
 */
public static void premain(String agentArgs) {
    
}


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

public class MyPreMainAgent {

    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("hello javaAgent");
    }

}

 

 JVM 启动后动态加载

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

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

/**
 * agentArgs 是 agentmain 函数得到的程序参数,在 attach 时传入。这个参数是个字符串,如果程序参数有多个,需要程序自行解析这个字符串。
 * inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。
 */
public static void agentmain(String agentArgs, Instrumentation inst) {

}

/**
 * 带有 Instrumentation 参数的 agentmain 优先级高于不带此参数的 agentmain。
 * 如果存在带 Instrumentation 参数的 agentmain,不带此参数的 agentmain 将被忽略。
 */
public static void agentmain(String agentArgs) {

}

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、导入相关依赖

    <dependencies>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.27.0-GA</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.67</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-jar-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <manifest>
                            <addClasspath>true</addClasspath>
                        </manifest>
                        <manifestEntries>
                            <Premain-Class>com.congge.agent.jvm.AgentMain2</Premain-Class>
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                            <Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

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

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @GetMapping("/queryUserInfo")
    public String queryUserInfo(String userId){
        return "hello :" + userId;
    }

}

3、编写agent类

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class AgentMain2 {

    private static final Set<String> classNameSet = new HashSet<>();

    static {
        classNameSet.add("com.congge.controller.UserController");
    }

    public static void premain(String agentArgs, Instrumentation instrumentation) {

        final ClassPool pool = new ClassPool();
        pool.appendSystemPath();

        //基于工具,在运行的时候修改class字节码,即动态插装
        instrumentation.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

                String currentClassName = className.replaceAll("/", ".");
                if (!classNameSet.contains(currentClassName)) { // 提升classNameSet中含有的类
                    return null;
                }

                if(classNameSet.contains(currentClassName)){
                    // 获取类
                    //CtClass ctClass = ClassPool.getDefault().get(currentClassName);
                    CtClass ctClass = null;
                    try {
                        ctClass = pool.getDefault().get(currentClassName);
                    } catch (NotFoundException e) {
                        e.printStackTrace();
                    }
                    String clazzName = ctClass.getName();

                    // 获取方法
                    CtMethod ctMethod = null;
                    try {
                        ctMethod = ctClass.getDeclaredMethod("queryUserInfo");
                    } catch (NotFoundException e) {
                        e.printStackTrace();
                    }
                    String methodName = ctMethod.getName();

                    // 方法信息:methodInfo.getDescriptor();
                    MethodInfo methodInfo = ctMethod.getMethodInfo();

                    // 方法:入参信息
                    CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
                    LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
                    CtClass[] parameterTypes = new CtClass[0];
                    try {
                        parameterTypes = ctMethod.getParameterTypes();
                    } catch (NotFoundException e) {
                        e.printStackTrace();
                    }


                    boolean isStatic = (methodInfo.getAccessFlags() & AccessFlag.STATIC) != 0;  // 判断是否为静态方法
                    int parameterSize = isStatic ? attr.tableLength() : attr.tableLength() - 1; // 静态类型取值
                    List<String> parameterNameList = new ArrayList<>(parameterSize);            // 入参名称
                    List<String> parameterTypeList = new ArrayList<>(parameterSize);            // 入参类型
                    StringBuilder parameters = new StringBuilder();                             // 参数组装;$1、$2...,$$可以获取全部,但是不能放到数组初始化

                    for (int i = 0; i < parameterSize; i++) {
                        parameterNameList.add(attr.variableName(i + (isStatic ? 0 : 1))); // 静态类型去掉第一个this参数
                        parameterTypeList.add(parameterTypes[i].getName());
                        if (i + 1 == parameterSize) {
                            parameters.append("$").append(i + 1);
                        } else {
                            parameters.append("$").append(i + 1).append(",");
                        }
                    }

                    // 方法:出参信息
                    CtClass returnType = null;
                    try {
                        returnType = ctMethod.getReturnType();
                    } catch (NotFoundException e) {
                        e.printStackTrace();
                    }
                    String returnTypeName = returnType.getName();

                    // 方法:生成方法唯一标识ID
                    int idx = Monitor.generateMethodId(clazzName, methodName, parameterNameList, parameterTypeList, returnTypeName);

                    // 定义属性
                    try {
                        ctMethod.addLocalVariable("startNanos", CtClass.longType);
                    } catch (CannotCompileException e) {
                        e.printStackTrace();
                    }
                    try {
                        ctMethod.addLocalVariable("parameterValues", ClassPool.getDefault().get(Object[].class.getName()));
                    } catch (CannotCompileException e) {
                        e.printStackTrace();
                    } catch (NotFoundException e) {
                        e.printStackTrace();
                    }

                    // 方法前加强
                    try {
                        ctMethod.insertBefore("{ startNanos = System.nanoTime(); parameterValues = new Object[]{" + parameters.toString() + "}; }");
                    } catch (CannotCompileException e) {
                        e.printStackTrace();
                    }

                    // 方法后加强
                    try {
                        ctMethod.insertAfter("{ com.congge.agent.jvm.Monitor.point(" + idx + ", startNanos, parameterValues, $_);}", false); // 如果返回类型非对象类型,$_ 需要进行类型转换
                    } catch (CannotCompileException e) {
                        e.printStackTrace();
                    }

                    // 方法;添加TryCatch
                    try {
                        ctMethod.addCatch("{ com.congge.agent.jvm.Monitor.point(" + idx + ", $e); throw $e; }", ClassPool.getDefault().get("java.lang.Exception"));   // 添加异常捕获
                    } catch (CannotCompileException e) {
                        e.printStackTrace();
                    } catch (NotFoundException e) {
                        e.printStackTrace();
                    }

                    try {
                        return ctClass.toBytecode();
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (CannotCompileException e) {
                        e.printStackTrace();
                    }
                }
                return null;
            }
        });

    }

}

该类的主要实现思路,就是重写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这一个类,在实际开发中,可以通过更灵活的方式去做,比如写到配置文件读取,通过自定义注解,或者扫描某个包路径等等;

代码优化改进

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

import javassist.*;
import javassist.bytecode.AccessFlag;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.LocalVariableAttribute;
import javassist.bytecode.MethodInfo;

import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.*;

public class AgentMain3 {

    private static final Set<String> classNameSet = new HashSet<>();

    static {
        classNameSet.add("com.congge.controller.UserController");
    }

    public static void premain(String agentArgs, Instrumentation instrumentation) {

        final ClassPool pool = new ClassPool();
        pool.appendSystemPath();

        //基于工具,在运行的时候修改class字节码,即动态插装
        instrumentation.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

                String currentClassName = className.replaceAll("/", ".");
                if (!classNameSet.contains(currentClassName)) { // 提升classNameSet中含有的类
                    return null;
                }

                if(classNameSet.contains(currentClassName)){
                    // 获取类
                    //CtClass ctClass = ClassPool.getDefault().get(currentClassName);
                    CtClass ctClass = null;
                    try {
                        ctClass = pool.getDefault().get(currentClassName);
                        try {
                            Object[] annotations = ctClass.getAnnotations();
                            for (Object obj : annotations) {
                                if (!obj.toString().startsWith("@org.springframework.web.bind.annotation.RestController")) {
                                    continue;
                                }
                            }
                        } catch (ClassNotFoundException e) {
                            e.printStackTrace();
                        }
                    } catch (NotFoundException e) {
                        e.printStackTrace();
                    }
                    String clazzName = ctClass.getName();
                    // 获取方法
                    //CtMethod ctMethod = null;
                    try {
                        CtMethod[] declaredMethods = ctClass.getDeclaredMethods();
                        if(Objects.nonNull(declaredMethods) && declaredMethods.length >0){
                            for(CtMethod ctMethod1 : declaredMethods){
                                CtMethod ctMethod = ctClass.getDeclaredMethod(ctMethod1.getName());
                                doHandleMethod(clazzName, ctMethod);
                            }
                        }
                    } catch (NotFoundException e) {
                        e.printStackTrace();
                    }
                    try {
                        return ctClass.toBytecode();
                    } catch (IOException e) {
                        e.printStackTrace();
                    } catch (CannotCompileException e) {
                        e.printStackTrace();
                    }
                }
                return null;
            }
        });

    }

    private static void doHandleMethod(String clazzName, CtMethod ctMethod) {
        String methodName = ctMethod.getName();
        // 方法信息:methodInfo.getDescriptor();
        MethodInfo methodInfo = ctMethod.getMethodInfo();

        // 方法:入参信息
        CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
        LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
        CtClass[] parameterTypes = new CtClass[0];
        try {
            parameterTypes = ctMethod.getParameterTypes();
        } catch (NotFoundException e) {
            e.printStackTrace();
        }

        boolean isStatic = (methodInfo.getAccessFlags() & AccessFlag.STATIC) != 0;  // 判断是否为静态方法
        int parameterSize = isStatic ? attr.tableLength() : attr.tableLength() - 1; // 静态类型取值
        List<String> parameterNameList = new ArrayList<>(parameterSize);            // 入参名称
        List<String> parameterTypeList = new ArrayList<>(parameterSize);            // 入参类型
        StringBuilder parameters = new StringBuilder();                             // 参数组装;$1、$2...,$$可以获取全部,但是不能放到数组初始化

        for (int i = 0; i < parameterSize; i++) {
            parameterNameList.add(attr.variableName(i + (isStatic ? 0 : 1))); // 静态类型去掉第一个this参数
            parameterTypeList.add(parameterTypes[i].getName());
            if (i + 1 == parameterSize) {
                parameters.append("$").append(i + 1);
            } else {
                parameters.append("$").append(i + 1).append(",");
            }
        }

        // 方法:出参信息
        CtClass returnType = null;
        try {
            returnType = ctMethod.getReturnType();
        } catch (NotFoundException e) {
            e.printStackTrace();
        }
        String returnTypeName = returnType.getName();

        // 方法:生成方法唯一标识ID
        int idx = Monitor.generateMethodId(clazzName, methodName, parameterNameList, parameterTypeList, returnTypeName);

        // 定义属性
        try {
            ctMethod.addLocalVariable("startNanos", CtClass.longType);
        } catch (CannotCompileException e) {
            e.printStackTrace();
        }
        try {
            ctMethod.addLocalVariable("parameterValues", ClassPool.getDefault().get(Object[].class.getName()));
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (NotFoundException e) {
            e.printStackTrace();
        }

        // 方法前加强
        try {
            ctMethod.insertBefore("{ startNanos = System.nanoTime(); parameterValues = new Object[]{" + parameters.toString() + "}; }");
        } catch (CannotCompileException e) {
            e.printStackTrace();
        }

        // 方法后加强
        try {
            ctMethod.insertAfter("{ com.congge.agent.jvm.Monitor.point(" + idx + ", startNanos, parameterValues, $_);}", false); // 如果返回类型非对象类型,$_ 需要进行类型转换
        } catch (CannotCompileException e) {
            e.printStackTrace();
        }

        // 方法;添加TryCatch
        try {
            ctMethod.addCatch("{ com.congge.agent.jvm.Monitor.point(" + idx + ", $e); throw $e; }", ClassPool.getDefault().get("java.lang.Exception"));   // 添加异常捕获
        } catch (CannotCompileException e) {
            e.printStackTrace();
        } catch (NotFoundException e) {
            e.printStackTrace();
        }
    }

}

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

@RestController
public class UserController {

    @GetMapping("/queryUserInfo")
    public String queryUserInfo(String userId){
        return "hello :" + userId;
    }

    @GetMapping("/queryUserInfo2")
    public String queryUserInfo2(String userName){
        return "hello :" + userName;
    }

}

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

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

 

 

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/78140.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

linux redhat 8 创建逻辑卷

LVM与直接使用物理存储相比,有以下优点: 1. 灵活的容量. 当使用逻辑卷时,文件系统可以扩展到多个磁盘上,你可以聚合多个磁盘或磁盘分区成单一的逻辑卷. 2. 方便的设备命名 逻辑卷可以按你觉得方便的方式来起任何名称. 3.磁盘条块化. 你可以生成一个逻辑盘,它的数据可以被…

记录一次Mac本地启动nacos遇到的问题

nacos 官网&#xff1a;https://nacos.io/zh-cn/docs/quick-start.html 我这里下载的是2.0.3稳定的版本 本地启动&#xff1a;sh startup.sh -m standalone 问题1&#xff1a;Caused by: java.lang.IllegalStateException: No DataSource set 这里是数据源连接有问题&#xff…

Linux网络原理及编程(6)——第十六节 TCP可靠性保证的原理

目录 1、确认应答机制 2、超时重传机制 3、滑动窗口 4、流量控制 5、拥塞控制 6、延迟应答 &#xff08;各位好&#xff0c;博主新建了个公众号《自学编程村》&#xff0c;拉到底部即可看到&#xff0c;有情趣可以关注看看哈哈&#xff0c;关注后还可以加博主wx呦~~~&am…

Apache Flink 水印的工作机制详解与源码阅读

一、时间长河谁能解 在人类生存的地球上&#xff0c;存在着一种很神秘的东西&#xff1a;时间&#xff0c;它看不见摸不着&#xff0c;但速度恒定&#xff0c;单调递增且永无止境的往前推进&#xff0c;人类的历史被淹没在茫茫的时间长河中。同时在地球附近&#xff0c;一个星…

【自动化测试】如何平衡手工和自动化测试

作为一名测试人员&#xff0c;如何平衡手工和自动化测试&#xff0c;是一道绕不过去的课题。不可否认&#xff0c;自动化测试具有提高效率&#xff0c;加快回归速度并因此有助于及时交付项目的好处。但是&#xff0c;在考虑自动化之前&#xff0c;我们应该评估一些要点&#xf…

2023最新SSM计算机毕业设计选题大全(附源码+LW)之java毕业生就业管理系统243xa

首先选择计算机题目的时候先看定什么主题&#xff0c;一般的话都选择当年最热门的话题进行组题&#xff0c;就比如说&#xff0c;今年的热门话题有奥运会&#xff0c;全运会&#xff0c;残运会&#xff0c;或者疫情相关的&#xff0c;这些都是热门话题&#xff0c;所以你就可以…

vscode 关闭/忽略/ignore 单个/指定 git 仓库/repository 提示

文章目录1. 问题2. 解决方法2.1 只追踪 打开文件 所在的仓库2.2 忽略指定的仓库3. 参考1. 问题 vscode 当打开的项目中有多个 git 仓库时&#xff0c;默认会显示所有仓库的 status。 有些已经不再使用的仓库可能有多处更改&#xff0c;但我们并不想去处理它。 如果直接关闭 vs…

gcc 4.8.5 的string问题

gcc 4.8.5及一下版本 的string 因为内部内存管理采用了COW的方式&#xff0c;导致了string在多线程的状态下容易崩溃。崩的很没有规律&#xff0c;表现就是string在赋值的时候就莫名其妙的崩了&#xff0c;查看堆栈&#xff0c;你也看不出啥。崩溃的很随机。 为了避免崩溃&…

文本纠错--文本分割N-gram--Macbert模型的调用以及对返回结果的处理

文本根据词典进行纠错 输入一段可能带有错误信息的文字&#xff0c; 通过词典来检测其中可能错误的词。 例如&#xff1a;有句子如下&#xff1a;中央人民政府驻澳门特别行政区联络办公室1日在机关大楼设灵堂    有词典如下&#xff1a;中国人民&#xff0c;中央人民&#x…

漏斗分析 - AARRR模型案例分析

漏斗分析是一套流程式的数据分析方法&#xff0c;能够科学地反映各阶段用户转化情况。漏斗分析模型已经广泛应用于用户行为分析类产品&#xff0c;且功能十分强大&#xff1a;它可以评估总体或各个环节的转化情况、促销活动效果&#xff1b;也可以与其他数据分析模型结合进行深…

【三维目标检测】SSN(二)

SSN数据和源码配置调试过程请参考上一篇博文&#xff1a;【三维目标检测】SSN&#xff08;一&#xff09;_Coding的叶子的博客-CSDN博客。本文主要详细介绍SSN网络结构及其运行中间状态。 1 模型总体过程 SSN主要结构如下图所示&#xff0c;其核心在于提出了shape-aware heads…

认识分布式锁、使用分布式锁 Redission、实现秒杀案例

分布式锁 基本原理 分布式锁&#xff1a;满足分布式系统或集群模式下多进程可见并且互斥的锁。 分布式锁的核心思想就是让大家都使用同一把锁&#xff0c;只要大家使用的是同一把锁&#xff0c;那么我们就能锁住线程&#xff0c;不让线程进行&#xff0c;让程序串行执行&…

[附源码]计算机毕业设计基于Vue的社区拼购商城Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

node.js-fs模块学习

目录 1.使用fs.readFile方法读取文件 2.使用fs.writeFile方法写入文件 3.fs小案例-整理成绩并写入到新文件中 4.fs模块-路径动态拼接的问题 1.使用fs.readFile方法读取文件 //导入fs模块 const fs require(fs)//调用fs读取文件 //参数1&#xff1a;读取文件的存放路径 //…

spring——Spring Bean属性注入——短命名空间注入——p 命名空间注入(setter注入)...

短命名空间注入 我们在通过构造函数或 setter 方法进行属性注入时&#xff0c;通常是在 <bean> 元素中嵌套 <property> 和 <constructor-arg> 元素来实现的。这种方式虽然结构清晰&#xff0c;但书写较繁琐。Spring 框架提供了 2 种短命名空间&#xff0c;可…

java基于Springboot的论坛管理系统-计算机毕业设计

项目介绍 在社会快速发展的影响下&#xff0c;论坛管理系统继续发展&#xff0c;使论坛管理系统的管理和运营比过去十年更加信息化。依照这一现实为基础&#xff0c;设计一个快捷而又方便的网上论坛管理系统是一项十分重要并且有价值的事情。对于传统的论坛管理系统控制模型来…

【大数据入门核心技术-Azkaban】(一)Azkaban介绍

目录 一、Azkaban 是什么 二、Azkaban 特点 三、Azkaban 与 Oozie 对比 一、Azkaban 是什么 Azkaban 是由 Linkedin 公司推出的一个批量工作流任务调度器&#xff0c;Azkaban 使用 job 文件建立任务之间的依赖关系&#xff0c;并提供 Web 界面供用户管理和调度工作流 官方网…

最新、最全面的Java面试题及答案(212道)

文章目录一. Java 基础1. JDK 和 JRE 有什么区别&#xff1f;2. 和 equals 的区别是什么&#xff1f;3. 两个对象的 hashCode() 相同&#xff0c;则equals() 也一定为true&#xff0c;对吗&#xff1f;4. final 在 Java 中有什么作用&#xff1f;5. Java 中的 Math. round(-1.…

服务器编译spark3.3.1源码支持CDH6.3.2

1、一定要注意编译环境的配置 mvn:3.6.3 scala:2.12.17 JDK:1.8 spark:3.3.1 服务器内存至少需要 8G 重点2、下载连接 wget https://dlcdn.apache.org/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.zip wget https://downloads.lightbend.com/scala/2.12.17/scala-2…

大量域名数据下载地址收集整理

说明地址中文网站排行榜——网站排名大全http://www.alexa.cn/siterankAlexa排名(70万的数据)top-1m.csv.zipCisco Umbrellahttp://s3-us-west-1.amazonaws.com/umbrella-static/index.htmlMajestic Millionhttps://majestic.com/reports/majestic-millionTranco-List.euhttps:…