简单的skywalking探针加载原理学习

简单的skywalking探针加载原理学习

本课程为B站bytebuddy课程 的学习笔记。

记录本人的操作记录方便后续复习学习使用,本人具体代码地址为: https://gitee.com/xiaodali/git-agent-demo

1. 创建目标工程

server工程比较简单,只是简单的一个controller可以进行数据库查询,首先我们新建一个git-agent-demo项目,再在下面创建一个子项目agent-server作为测试server服务,具体的搭建过程如下,git-agent-demo父类工程项目pom文件如下:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<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>cn.git.agent</groupId>
    <artifactId>git-agent-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>
    <modules>
        <module>agent-server</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.9.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR8</spring-cloud.version>
        <mybatis.plus.version>3.3.0</mybatis.plus.version>
        <mysql.version>5.1.47</mysql.version>
        <alibaba.version>2.2.5.RELEASE</alibaba.version>
        <seata.version>1.4.2</seata.version>
        <lombok.version>1.18.6</lombok.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${alibaba.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- springCloud -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- mysql驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>${mybatis.plus.version}</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>${lombok.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.4</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--单元测试-->
    </dependencies>

</project>

子项目agent-server引入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>
    <parent>
        <groupId>cn.git.agent</groupId>
        <artifactId>git-agent-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>agent-server</artifactId>
    <packaging>jar</packaging>

    <name>agent-server</name>
    <url>http://maven.apache.org</url>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- compiler -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>${lombok.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

配置application.yaml内容如下:

yaml 复制代码
server:
  port: 8080
spring:
  application:
    name: git-agent-demo
  datasource:
    url: jdbc:mysql://192.168.138.129:3306/test?useUnicode=true&characterEncoding=utf-8&useSSL=false  # 数据库连接 URL
    username: root  # 数据库用户名
    password: 101022  # 数据库密码
    driver-class-name: com.mysql.jdbc.Driver
mybatis-plus:
  configuration:
    # sql日志打印
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      insert-strategy: not_null
      update-strategy: not_null
      id-type: auto

初始化sql内容如下:

sql 复制代码
DROP TABLE IF EXISTS `user_info`;
CREATE TABLE `user_info`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  `user_name` varchar(50) NOT NULL COMMENT '用户名称',
  `pwd` varchar(50) NOT NULL COMMENT '密码',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `user_name`(`user_name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

select * from user_info

insert into user_info (user_name, pwd) values('user1', '123456');
insert into user_info (user_name, pwd) values('user2', '123456');
insert into user_info (user_name, pwd) values('user3', '123456');
insert into user_info (user_name, pwd) values('user4', '123456');
insert into user_info (user_name, pwd) values('user5', '123456');
insert into user_info (user_name, pwd) values('user6', '123456');

服务层层面创建一个controller作为入口:

java 复制代码
package cn.git.agent.controller;

import cn.git.agent.entity.UserInfo;
import cn.git.agent.service.UserInfoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * @description: 用户信息controller
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-20
 */
@RestController
@RequestMapping("/userInfo")
public class UserInfoController {

    @Autowired
    private UserInfoService userInfoService;

    @GetMapping("/list/{userName}")
    public List<UserInfo> selectUserInfoList(@PathVariable(value = "userName") String userName) {
        return userInfoService.selectUserInfoList(userName);
    }

}

具体的service以及对应impl如下:

  • UserInfoService

    java 复制代码
    package cn.git.agent.service;
    
    import cn.git.agent.entity.UserInfo;
    
    import java.util.List;
    
    /**
     * @description: 用户信息service
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-20
     */
    public interface UserInfoService {
    
        List<UserInfo> selectUserInfoList(String userName);
    
    }
  • UserInfoServiceImpl

    java 复制代码
    package cn.git.agent.service.impl;
    
    import cn.git.agent.entity.UserInfo;
    import cn.git.agent.mapper.UserInfoMapper;
    import cn.git.agent.service.UserInfoService;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    import java.util.List;
    
    /**
     * @description: 用户信息service实现
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-20
     */
    @Service
    public class UserInfoServiceImpl implements UserInfoService {
    
        @Autowired
        private UserInfoMapper userInfoMapper;
    
        @Override
        public List<UserInfo> selectUserInfoList(String userName) {
            QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
            queryWrapper.lambda().like(UserInfo::getUserName, userName);
            List<UserInfo> userInfoList = userInfoMapper.selectList(queryWrapper);
            return userInfoList;
        }
    }

服务启动类如下:

java 复制代码
package cn.git.agent;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * @description: 服务端启动类
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-20
 */
@EnableFeignClients
@SpringBootApplication
@MapperScan("cn.git.agent.mapper")
public class AgentServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(AgentServerApplication.class, args);
    }
}

启动服务,访问接口 http://localhost:8080/userInfo/list/user 获得展示信息如下,至此准备服务完毕

2. Skywalking 概述

SkyWalking主要可以划分为4部分:

  • Probes: agent项目(sniffer)探针
  • Platform backend:oap skywalking server服务端分析平台
  • Storage:oap使用的存储
  • UI: 界面

大致流程即:

  1. agent(sniff)探针在服务端程序中执行SkyWalking的插件的拦截器逻辑,完成监测数据获取
  2. agent(sniff)探针通过gRPC(默认)等形式将获取的监测数据传输到OAP系统
  3. OAP系统将数据进行处理/整合后,存储到Storage中(ES, MySQL, H2等)
  4. 用户可通过UI界面快捷查询数据,了解请求调用链路,调用耗时等信息

3. SkyWalking Agent

深入理解 Skywalking Agent - 简书 (jianshu.com) <= 推荐先阅读,很详细

  • 基础介绍

    SkyWalking Agent,是SkyWalking中的组件之一(skywalking-agent.jar),在Java中通过Java Agent实现。

  • 使用简述

    1. 通过java -javaagent:skywalking-agent.jar包的绝对路径 -jar 目标服务jar包绝对路径指令,在目标服务main方法执行前,会先执行skywalking-agent.jarorg.apache.skywalking.apm.agent.SkyWalkingAgent#premain方法
    2. premian方法执行时,加载与skywalking-agent.jar同层级文件目录的./activations./plusgins目录内的所有插件jar包,根据jar文件内的skywalking-plugin.def配置文件作相关解析工作
    3. 解析工作完成后,将所有插件实现的拦截器逻辑,通过JVM工具类Instrumentation提供的redefine/retransform能力,修改目标类的.class内容,将拦截器内指定的增强逻辑附加到被拦截的类的原方法实现中
    4. 目标服务的main方法执行,此时被拦截的多个类内部方法逻辑已经被增强,比如某个方法执行前后额外通过gRPC将方法耗时记录并发送到OAP。简单理解的话,类似Spring中常用的AOP切面技术,这里相当于字节码层面完成切面增强
  • 实现思考

    Byte Buddy可以指定一个拦截器对指定拦截的类中指定的方法进行增强。SkyWalking Java Agent使用ByteBuddy实现,从0到1实现时,避不开以下几个问题需要考虑:

    1. 每个插件都需要有各自的拦截器逻辑,如何让Byte Buddy使用我们指定的多个拦截器
    2. 多个拦截器怎么区分各自需要拦截的类和方法
    3. 如何在premain中使用Byte Buddy时加载多个插件的拦截器逻辑

3.1 实现springMVC插件拦截

我们之前的bytebuddy-core核心学习过程中已经实现了springMVC方法的拦截,追要是拦截 @RestController以及@Controller注解的类,然后拦截带有Mapping尾缀注解的方法 ,实现拦截springMVC入口方法。这里新增一个springmvc-standlone-plugin模块,这里就简单回顾一下:

  • 引入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>
        <parent>
            <groupId>cn.git.agent</groupId>
            <artifactId>git-agent-demo</artifactId>
            <version>1.0-SNAPSHOT</version>
            <relativePath>../../pom.xml</relativePath>
        </parent>
    
        <artifactId>springmvc-standlone-plugin</artifactId>
        <packaging>jar</packaging>
    
        <name>springmvc-standlone-plugin</name>
        <url>http://maven.apache.org</url>
    
        <properties>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
    
        <dependencies>
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>3.8.1</version>
                <scope>test</scope>
            </dependency>
            <!-- Byte Buddy -->
            <dependency>
                <groupId>net.bytebuddy</groupId>
                <artifactId>byte-buddy</artifactId>
                <version>1.14.10</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.6</version>
            </dependency>
        </dependencies>
    
        <build>
            <plugins>
                <plugin>
                    <!-- 用于打包插件 -->
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-assembly-plugin</artifactId>
                    <version>3.1.0</version>
                    <configuration>
                        <archive>
                            <manifestEntries>
                                <!-- MANIFEST.MF 配置项,指定premain方法所在类 -->
                                <Premain-Class>cn.git.agent.SpringMVCAgent</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>
                        <descriptorRefs>
                            <descriptorRef>jar-with-dependencies</descriptorRef>
                        </descriptorRefs>
                    </configuration>
                    <executions>
                        <execution>
                            <id>make-assembly</id>
                            <!-- 什么阶段会触发此插件 -->
                            <phase>package</phase>
                            <goals>
                                <!-- 只运行一次 -->
                                <goal>single</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </project>
  • SpringMVCAgent探针入口

    java 复制代码
    package cn.git.agent;
    
    import net.bytebuddy.agent.builder.AgentBuilder;
    import net.bytebuddy.matcher.ElementMatchers;
    
    import java.lang.instrument.Instrumentation;
    
    /**
     * @description: byteBuddy探针,实现springmvc 拦截器
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-19
     */
    public class SpringMVCAgent {
    
        /**
         * 控制器注解名称
         */
        public static final String REST_CONTROLLER_NAME = "org.springframework.web.bind.annotation.RestController";
        public static final String CONTROLLER_NAME = "org.springframework.stereotype.Controller";
    
    
        /**
         * premain方法,main方法执行之前进行调用,插桩代码入口
         * @param args 标识外部传递参数
         * @param instrumentation 插桩对象
         */
        public static void premain(String args, Instrumentation instrumentation) {
            // 创建AgentBuilder对象
            AgentBuilder builder = new AgentBuilder.Default()
                    // 忽略拦截的包
                    .ignore(ElementMatchers.nameStartsWith("net.bytebuddy")
                            .or(ElementMatchers.nameStartsWith("org.apache"))
                    )
                    // 拦截标注以什么注解的类
                    .type(ElementMatchers.isAnnotatedWith(
                            ElementMatchers.named(CONTROLLER_NAME)
                                    .or(ElementMatchers.named(REST_CONTROLLER_NAME)))
                    )
                    // 前面的type()方法匹配到的类,进行拦截
                    .transform(new ByteBuddyTransform())
                    .with(new ByteBuddyListener());
    
            // 安装
            builder.installOn(instrumentation);
        }
    }
  • transformer方法

    java 复制代码
    package cn.git.agent;
    
    import net.bytebuddy.agent.builder.AgentBuilder;
    import net.bytebuddy.description.type.TypeDescription;
    import net.bytebuddy.dynamic.DynamicType;
    import net.bytebuddy.implementation.MethodDelegation;
    import net.bytebuddy.utility.JavaModule;
    
    import java.security.ProtectionDomain;
    
    import static net.bytebuddy.matcher.ElementMatchers.*;
    
    /**
     * @description: bytebuddy transform,当被拦截的type第一次要被加载的时候,会进入到此方法
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-19
     */
    public class ByteBuddyTransform implements AgentBuilder.Transformer {
    
        /**
         * 拦截的注解开头结尾
         */
        private static final String MAPPING_PACKAGE_PREFIX = "org.springframework.web.bind.annotation";
        private static final String MAPPING_PACKAGE_SUFFIX = "Mapping";
    
        /**
         * Allows for a transformation of a {@link DynamicType.Builder}.
         *
         * @param builder
         * @param typeDescription  要被加载的类的信息
         * @param classLoader      The class loader of the instrumented class. Might be {@code null} to represent the bootstrap class loader.
         * @param module           The class's module or {@code null} if the current VM does not support modules.
         * @param protectionDomain The protection domain of the transformed type.
         * @return A transformed version of the supplied {@code builder}.
         */
        @Override
        public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
                                                TypeDescription typeDescription,
                                                ClassLoader classLoader,
                                                JavaModule module,
                                                ProtectionDomain protectionDomain) {
            // 获取实际的名字
            String actualName = typeDescription.getActualName();
            System.out.println("actualName: " + actualName);
    
            // 确保匹配的是具体的类,而不是接口
            if (typeDescription.isInterface()) {
                System.out.println("接口不拦截");
                return builder;
            }
    
            // 实例化 SpringMvcInterceptor
            SpringMvcInterceptor interceptor = new SpringMvcInterceptor();
    
            // 拦截所有被注解标记的方法
            DynamicType.Builder.MethodDefinition.ReceiverTypeDefinition<?> intercept = builder.method(
                            not(isStatic())
                                    .and(isAnnotatedWith(nameStartsWith(MAPPING_PACKAGE_PREFIX).and(nameEndsWith(MAPPING_PACKAGE_SUFFIX)))
                            )
                    )
                    .intercept(MethodDelegation.to(interceptor));
            // 不能返回builder,因为bytebuddy里面的库里面的类基本都是不可变的,修改之后需要返回一个新的builder,避免修改丢失
            return intercept;
        }
    }
  • 切面interceptor类,具体逻辑实现

    java 复制代码
    package cn.git.agent;
    
    import net.bytebuddy.implementation.bind.annotation.*;
    
    import java.lang.reflect.Method;
    import java.util.Arrays;
    import java.util.concurrent.Callable;
    
    public class SpringMvcInterceptor {
    
        /**
         * 被标注 RuntimeType 注解的方法就是拦截方法,此时返回的值与返回参数可以与被拦截的方法不一致
         * byteBuddy会在运行期间给被拦截的方法参数进行赋值
         * @return
         */
        @RuntimeType
        public Object intercept(
                @This Object targetObject,
                @Origin Method targetMethod,
                @AllArguments Object[] targetMethodArgs,
                @SuperCall Callable<?> superCall) {
            Long start = System.currentTimeMillis();
            System.out.println("targetObject : " + targetObject);
            System.out.println("targetMethodName : " + targetMethod.getName());
            System.out.println("targetMethodArgs : " + Arrays.toString(targetMethodArgs));
            Object call;
            try {
                call = superCall.call();
            } catch (Exception e) {
                e.printStackTrace();
                throw new RuntimeException(e);
            } finally {
                Long end = System.currentTimeMillis();
                System.out.println(targetMethod.getName() + "  耗时:" + (end - start) + "ms");
            }
            return call;
        }
    }
  • 监听器类

    java 复制代码
    package cn.git.agent;
    
    import net.bytebuddy.agent.builder.AgentBuilder;
    import net.bytebuddy.description.type.TypeDescription;
    import net.bytebuddy.dynamic.DynamicType;
    import net.bytebuddy.utility.JavaModule;
    
    /**
     * @description: 监听器
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-19
     */
    public class ByteBuddyListener implements AgentBuilder.Listener {
    
        /**
         * 当一个类型被发现时调用,就会回调此方法
         *
         * @param typeName    The binary name of the instrumented type.
         * @param classLoader The class loader which is loading this type or {@code null} if loaded by the boots loader.
         * @param module      The instrumented type's module or {@code null} if the current VM does not support modules.
         * @param loaded      {@code true} if the type is already loaded.
         */
        @Override
        public void onDiscovery(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {
            if (typeName.contains("TestController")) {
                System.out.println("onDiscovery: " + typeName);
            }
        }
    
        /**
         * 对某一个类型进行转换时调用,就会回调此方法
         *
         * @param typeDescription The type that is being transformed.
         * @param classLoader     The class loader which is loading this type or {@code null} if loaded by the boots loader.
         * @param module          The transformed type's module or {@code null} if the current VM does not support modules.
         * @param loaded          {@code true} if the type is already loaded.
         * @param dynamicType     The dynamic type that was created.
         */
        @Override
        public void onTransformation(TypeDescription typeDescription,
                                     ClassLoader classLoader,
                                     JavaModule module,
                                     boolean loaded,
                                     DynamicType dynamicType) {
            System.out.println("onTransformation: " + typeDescription.getActualName());
        }
    
        /**
         * 当某一个类被加载并且被忽略时(包括ignore配置或不匹配)调用,就会回调此方法
         *
         * @param typeDescription The type being ignored for transformation.
         * @param classLoader     The class loader which is loading this type or {@code null} if loaded by the boots loader.
         * @param module          The ignored type's module or {@code null} if the current VM does not support modules.
         * @param loaded          {@code true} if the type is already loaded.
         */
        @Override
        public void onIgnored(TypeDescription typeDescription,
                              ClassLoader classLoader,
                              JavaModule module,
                              boolean loaded) {
    //        log.info("onIgnored: {}", typeDescription.getActualName());
    //        System.out.println("onIgnored: " + typeDescription.getActualName());
        }
    
        /**
         * 当transform过程中发生异常时,会回调此方法
         *
         * @param typeName    The binary name of the instrumented type.
         * @param classLoader The class loader which is loading this type or {@code null} if loaded by the boots loader.
         * @param module      The instrumented type's module or {@code null} if the current VM does not support modules.
         * @param loaded      {@code true} if the type is already loaded.
         * @param throwable   The occurred error.
         */
        @Override
        public void onError(String typeName,
                            ClassLoader classLoader,
                            JavaModule module,
                            boolean loaded,
                            Throwable throwable) {
            System.out.println("onError: " + typeName);
            throwable.printStackTrace();
        }
    
        /**
         * 当某一个类被处理完,不管是transform还是忽略时,都会回调此方法
         *
         * @param typeName    The binary name of the instrumented type.
         * @param classLoader The class loader which is loading this type or {@code null} if loaded by the boots loader.
         * @param module      The instrumented type's module or {@code null} if the current VM does not support modules.
         * @param loaded      {@code true} if the type is already loaded.
         */
        @Override
        public void onComplete(String typeName, ClassLoader classLoader, JavaModule module, boolean loaded) {
    //        log.info("onComplete: {}", typeName);
    //        System.out.println("onComplete: " + typeName);
        }
    }

启动服务,然后访问接口 http://localhost:8080/userInfo/list/user 发现接口已经被增强了


3.2 实现mysql监听插件

同样的我们新增一个mysql的插件模块mysql-standlone-plugin,对应的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>
    <parent>
        <groupId>cn.git.agent</groupId>
        <artifactId>git-agent-demo</artifactId>
        <version>1.0-SNAPSHOT</version>
        <relativePath>../../pom.xml</relativePath>
    </parent>

    <artifactId>mysql-standlone-plugin</artifactId>
    <packaging>jar</packaging>

    <name>mysql-standlone-plugin</name>
    <url>http://maven.apache.org</url>

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

    <dependencies>
        <!-- Byte Buddy -->
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>1.14.10</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.6</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <!-- 用于打包插件 -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <!-- MANIFEST.MF 配置项,指定premain方法所在类 -->
                            <Premain-Class>cn.git.agent.MySqlAgent</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>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <!-- 什么阶段会触发此插件 -->
                        <phase>package</phase>
                        <goals>
                            <!-- 只运行一次 -->
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

我对应的mysql的版本信息为5.x.x,所以我增强的方法就暂定了 com.mysql.jdbc.PreparedStatement 此类,并且增强类种的"execute", "executeQuery", "executeUpdate" 三个方法,探针入口类如下:

java 复制代码
package cn.git.agent;

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.matcher.ElementMatchers;

import java.lang.instrument.Instrumentation;

import static net.bytebuddy.matcher.ElementMatchers.named;

/**
 * @description: mysql的拦截插件
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-23
 */
public class MySqlAgent {

    /**
     * 拦截方法名称
     */
    public static final String PREPARED_STATEMENT_NAME = "com.mysql.jdbc.PreparedStatement";

    /**
     * premain方法,main方法执行之前进行调用,插桩代码入口
     * @param args 标识外部传递参数
     * @param instrumentation 插桩对象
     */
    public static void premain(String args, Instrumentation instrumentation) {
        // 创建AgentBuilder对象
        AgentBuilder builder = new AgentBuilder.Default()
                // 忽略拦截的包
                .ignore(ElementMatchers.nameStartsWith("net.bytebuddy")
                        .or(ElementMatchers.nameStartsWith("org.apache"))
                )
                // 拦截标注以什么注解的类
                .type(named(PREPARED_STATEMENT_NAME))
                // 前面的type()方法匹配到的类,进行拦截
                .transform(new MySqlTransform())
                .with(new MySqlListener());

        // 安装
        builder.installOn(instrumentation);
    }
}

对应的MySqlTransform实现类如下:

java 复制代码
package cn.git.agent;

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;

import java.security.ProtectionDomain;

import static net.bytebuddy.matcher.ElementMatchers.*;

/**
 * @description: bytebuddy transform,当被拦截的type第一次要被加载的时候,会进入到此方法
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-19
 */
public class MySqlTransform implements AgentBuilder.Transformer {

    /**
     * 拦截的方法
     */
    private static final String INSPECT_EXECUTE_METHOD = "execute";
    private static final String INSPECT_EXECUTE_QUERY = "executeQuery";
    private static final String INSPECT_EXECUTE_UPDATE = "executeUpdate";
    private static final String INSPECT_EXECUTE_LARGE_UPDATE = "executeLargeUpdate";


    /**
     * Allows for a transformation of a {@link DynamicType.Builder}.
     *
     * @param builder
     * @param typeDescription  要被加载的类的信息
     * @param classLoader      The class loader of the instrumented class. Might be {@code null} to represent the bootstrap class loader.
     * @param module           The class's module or {@code null} if the current VM does not support modules.
     * @param protectionDomain The protection domain of the transformed type.
     * @return A transformed version of the supplied {@code builder}.
     */
    @Override
    public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
                                            TypeDescription typeDescription,
                                            ClassLoader classLoader,
                                            JavaModule module,
                                            ProtectionDomain protectionDomain) {
        // 获取实际的名字
        String actualName = typeDescription.getActualName();
        System.out.println("actualName: " + actualName);

        // 确保匹配的是具体的类,而不是接口
        /**
         *         if (typeDescription.isInterface()) {
         *             System.out.println("接口不拦截");
         *             return builder;
         *         }
         */

        // 实例化 SpringMvcInterceptor
        MySqlInterceptor interceptor = new MySqlInterceptor();

        // 拦截所有被注解标记的方法
        DynamicType.Builder.MethodDefinition.ReceiverTypeDefinition<?> intercept = builder.method(
                        ElementMatchers.namedOneOf(INSPECT_EXECUTE_METHOD,
                                INSPECT_EXECUTE_QUERY,
                                INSPECT_EXECUTE_UPDATE,
                                INSPECT_EXECUTE_LARGE_UPDATE)
                )
                .intercept(MethodDelegation.to(interceptor));
        // 不能返回builder,因为bytebuddy里面的库里面的类基本都是不可变的,修改之后需要返回一个新的builder,避免修改丢失
        return intercept;
    }
}

我们对应的MySqlInterceptors实现如下:

java 复制代码
package cn.git.agent;

import net.bytebuddy.implementation.bind.annotation.*;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.concurrent.Callable;

/**
 * @description: mysql增强拦截器
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-23
 */
public class MySqlInterceptor {

    /**
     * 被标注 RuntimeType 注解的方法就是拦截方法,此时返回的值与返回参数可以与被拦截的方法不一致
     * byteBuddy会在运行期间给被拦截的方法参数进行赋值
     * @return
     */
    @RuntimeType
    public Object intercept(
            @This Object targetObject,
            @Origin Method targetMethod,
            @AllArguments Object[] targetMethodArgs,
            @SuperCall Callable<?> superCall) {
        Long start = System.currentTimeMillis();

        try {
            String executeSql = getSql(targetObject);
            System.out.println("执行sql信息为 : " + executeSql);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        Object call;
        try {
            call = superCall.call();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } finally {
            Long end = System.currentTimeMillis();
            System.out.println(targetMethod.getName() + "  耗时:" + (end - start) + "ms");
        }
        return call;
    }

    /**
     * 获取sql语句
     * @param preparedStatement
     * @return
     * @throws Exception
     */
    private static String getSql(Object preparedStatement) throws Exception {
        Field sqlField = findField(preparedStatement.getClass(), "originalSql");
        if (sqlField == null) {
            throw new NoSuchFieldException("originalSql");
        }
        sqlField.setAccessible(true);
        return (String) sqlField.get(preparedStatement);
    }


    /**
     * 获取字段
     * @param clazz
     * @param fieldName
     * @return
     */
    private static Field findField(Class<?> clazz, String fieldName) {
        while (clazz != null) {
            try {
                System.out.println("子类没有对应字段,获取父类信息" + clazz.getName());
                return clazz.getDeclaredField(fieldName);
            } catch (NoSuchFieldException e) {
                clazz = clazz.getSuperclass();
            }
        }
        return null;
    }
}

此处我们需要本地调试,就不使用复制外部包方式了,直接配置VM-option参数,可以使用断点进行调试探针:

sh 复制代码
-javaagent:D:\idea_workspace\bytebuddy-demo\git-agent-demo\standlone-plugins\mysql-standlone-plugin\target\mysql-standlone-plugin-1.0-SNAPSHOT-jar-with-dependencies.jar

启动服务,调用对应的查询接口 http://localhost:8080/userInfo/list/user,我们可以看到方法已经被增强,可以获取到执行的sql信息。

4. 动态插拔插件实现

4.1 实现要点分析

  1. 如何实现单一java agent入口类

    仅在apm-agent模块提供一个premain方法入口,其他插件未来都在apm-plugins内实现

  2. 如何整合多个插件需要拦截的类范围

    AgentBuilder#type方法中,通过.or(Xxx)链接多个插件需要拦截的类范围

  3. 如何整合多个插件需要增强的方法范围

    DynamicType.Builder#method方法中,通过.or(Xxx)链接多个插件需要增强的方法范围

  4. 如何整个多个插件各自增强的方法对应的拦截器

    DynamicType.Builder.MethodDefinition.ImplementationDefinition#intercept可以通过andThen指定多个拦截器

  5. 如何让被拦截的类中增强的方法走正确的拦截器逻辑?

    拦截器InterceptorA对应类拦截范围ClassA下的增强方法范围MethodA,所以需要有一种机制维护三者之间的关系

    需要记录的映射:

    • 拦截类范围 => 增强的方法范围
    • 拦截类范围 => 拦截器

    (如果没有维护这些映射关系,那么多个拦截器互相干扰,比如意外增强了其他类中的同名方法)

4.2 项目结构

我们主备仿照skywalking进行编写,其agent集合apm-sinffer模块向下包含插件apm-plugins模块,探针入口apm-agent模块,以及核心apm-core模块,其中plugins模块向下还包含多个子插件模块,具体结构如下图所示:

4.3 插件的抽象

我们插件抽象具体实现主要是围绕动态获取拦截的类,动态获取拦截的方法以及拦截类拦截方法需要有一定关联,我们在新增三个模块中进行具体实现,整体逻辑结构图如下:

4.3.1 核心模块

插件的抽象工作基本都是核心模块完成,具体实现部分参考如下代码

4.3.1.1 插件顶级父类
java 复制代码
package cn.git.agent;

import cn.git.agent.interceptor.ConstructMethodInterceptPoint;
import cn.git.agent.interceptor.InstanceMethodInterceptPoint;
import cn.git.agent.interceptor.StaticMethodInterceptPoint;
import cn.git.agent.match.ClassMatch;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;

/**
 * @description: 所有插件的顶级父类
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-24
 */
public abstract class AbstractClassEnhancePluginDefine {

    /**
     * 获取需要拦截的类
     *
     * @return
     */
    protected abstract ClassMatch enhanceClass();

    /**
     * 获取需要拦截的方法,实例拦截方法
     *
     * @return
     */
    protected abstract InstanceMethodInterceptPoint[] getInstanceMethodsInterceptPoints();

    /**
     * 获取构造方法的拦截点
     *
     * @return
     */
    protected abstract ConstructMethodInterceptPoint[] getConstructMethodInterceptPoint();

    /**
     * 获取静态方法拦截点
     *
     * @return
     */
    protected abstract StaticMethodInterceptPoint[] getStaticMethodInterceptPoint();

    /**
     * 增强类的主入口
     *
     * @param typeDescription
     * @param builder
     * @param classLoader
     * @return
     */
    public DynamicType.Builder<?> define(TypeDescription typeDescription,
                                         DynamicType.Builder<?> builder,
                                         ClassLoader classLoader) {
        // 增强插件类的名字 eg: cn.git.agent.Mysql5Instrumentation
        String pluginsDefineClassName = this.getClass().getName();

        // 要增强的类名称 eg: com.mysql.jdbc.PreparedStatement
        String typeName = typeDescription.getTypeName();
        System.out.println("准备使用" + pluginsDefineClassName + "增强" + typeName + "开始");
        DynamicType.Builder<?> newBuilder = this.enhance(typeDescription, builder, classLoader);
        System.out.println("使用" + pluginsDefineClassName + "增强" + typeName + "成功");
        return newBuilder;
    }

    private DynamicType.Builder<?> enhance(TypeDescription typeDescription,
                                           DynamicType.Builder<?> builder,
                                           ClassLoader classLoader) {
        // 增强静态方法
        builder = this.enhanceClass(typeDescription, builder, classLoader);
        // 增强实例方法
        builder = this.enhanceInstance(typeDescription, builder, classLoader);

        return builder;
    }

    /**
     * 增强实例方法
     * @param typeDescription
     * @param builder
     * @param classLoader
     * @return
     */
    protected abstract DynamicType.Builder<?> enhanceInstance(TypeDescription typeDescription,
                                                   DynamicType.Builder<?> builder,
                                                   ClassLoader classLoader);

    /**
     * 增强静态方法
     * @param typeDescription
     * @param builder
     * @param classLoader
     * @return
     */
    protected abstract DynamicType.Builder<?> enhanceClass(TypeDescription typeDescription,
                                                DynamicType.Builder<?> builder,
                                                ClassLoader classLoader);
}
4.3.1.2 设置方法拦截点

方法拦截点分为构造器 ConstructMethodInterceptPoint,实例InstanceMethodInterceptPoint以及StaticMethodInterceptPoint静态方法拦截点,具体实现如下:

  • InstanceMethodInterceptPoint

    java 复制代码
    package cn.git.agent.interceptor;
    
    import net.bytebuddy.description.method.MethodDescription;
    import net.bytebuddy.matcher.ElementMatcher;
    
    /**
     * @description: 需要拦截的方法点,实例方法拦截点
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-24
     */
    public interface InstanceMethodInterceptPoint {
    
        /**
         * 获取需要拦截的方法,作为method()方法的参数
         *
         * @return
         */
        ElementMatcher<MethodDescription> getMethodsMatcher();
    
        /**
         * 获取需要拦截的方法的拦截器
         *
         * @return
         */
        String getMethodInterceptor();
    }
  • ConstructMethodInterceptPoint

    java 复制代码
    package cn.git.agent.interceptor;
    
    import net.bytebuddy.description.method.MethodDescription;
    import net.bytebuddy.matcher.ElementMatcher;
    
    /**
     * @description: 构造方法拦截点
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-24
     */
    public interface ConstructMethodInterceptPoint {
    
        /**
         * 获取需要拦截的方法,作为method()方法的参数
         *
         * @return
         */
        ElementMatcher<MethodDescription> getConstructMethodsMatcher();
    
        /**
         * 获取需要拦截的方法的拦截器
         *
         * @return
         */
        String getMethodInterceptor();
    }
  • StaticMethodInterceptPoint

    java 复制代码
    package cn.git.agent.interceptor;
    
    import net.bytebuddy.description.method.MethodDescription;
    import net.bytebuddy.matcher.ElementMatcher;
    
    /**
     * @description: 获取静态方法拦截点
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-24
     */
    public interface StaticMethodInterceptPoint {
    
        /**
         * 获取需要拦截的方法,作为method()方法的参数
         *
         * @return
         */
        ElementMatcher<MethodDescription> getStaticMethodsMatcher();
    
        /**
         * 获取需要拦截的方法的拦截器
         *
         * @return
         */
        String getMethodInterceptor();
    }
4.3.1.3 拦截类匹配ClassMatch

拦截类匹配器分为NameMatch以及IndirectMatch,他们分别为名称匹配以及其他匹配,顶级父类接口都是ClassMatch,具体实现如下

  • IndirectMatch

    java 复制代码
    package cn.git.agent.match;
    
    import net.bytebuddy.description.type.TypeDescription;
    import net.bytebuddy.matcher.ElementMatcher;
    
    /**
     * @description: IndirectMatch,所有非nameMatch情况,都需要实现IndirectMatch
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-24
     */
    public interface IndirectMatch extends ClassMatch {
    
        /**
         * 构建匹配规则
         * eg: named(CLASS_NAME1).or(named(CLASS_NAME2))
         *
         * @return
         */
        ElementMatcher.Junction<? super TypeDescription> buildJunction();
    
        /**
         * 用于当前匹配器
         * 用于判断 typeDescription 是否满足 IndirectMatch 的实现
         *
         * @param typeDescription
         * @return
         */
        boolean isMatch(TypeDescription typeDescription);
    
    }
  • NameMatch

    java 复制代码
    package cn.git.agent.match;
    
    import net.bytebuddy.description.type.TypeDescription;
    import net.bytebuddy.matcher.ElementMatcher;
    import net.bytebuddy.matcher.ElementMatchers;
    
    /**
     * @description: 类名称匹配实现类
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-24
     */
    public class NameMatch implements ClassMatch {
    
        /**
         * 全类名称
         */
        private String className;
    
        public String getClassName() {
            return className;
        }
    
        /**
         * 构造函数
         * @param className
         */
        public NameMatch(String className) {
            this.className = className;
        }
    
        /**
         * 构建匹配器
         *
         * @return
         */
        public ElementMatcher.Junction<? super TypeDescription> buildJunction() {
            return ElementMatchers.named(className);
        }
    
        /**
         * 构建匹配器
         *
         * @param className
         * @return
         */
        public static NameMatch byName(String className) {
            return new NameMatch(className);
        }
    
    }

    暂时还有几个IndirectMatch的具体实现类,参考如下,多个注解拦截

    java 复制代码
    package cn.git.agent.match;
    
    import net.bytebuddy.description.annotation.AnnotationDescription;
    import net.bytebuddy.description.annotation.AnnotationList;
    import net.bytebuddy.description.type.TypeDescription;
    import net.bytebuddy.matcher.ElementMatcher;
    
    import java.lang.annotation.Annotation;
    import java.util.ArrayList;
    import java.util.Arrays;
    import java.util.List;
    
    import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith;
    import static net.bytebuddy.matcher.ElementMatchers.named;
    
    /**
     * @description: 多个注解拦截
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-24
     */
    public class MultiAnnotationMatch implements IndirectMatch {
    
        /**
         * 注解名称列表
         */
        private List<String> annotationNameList;
    
        /**
         * 构造函数
         *
         * @param annotationNames 注解名称列表
         */
        private MultiAnnotationMatch(String... annotationNames) {
            if (annotationNames == null || annotationNames.length == 0) {
                throw new IllegalArgumentException("classNames can not be null or empty");
            }
            this.annotationNameList = Arrays.asList(annotationNames);
        }
    
        /**
         * 类名构建匹配规则
         * eg: ElementMatchers.isAnnotatedWith(ElementMatchers.named(CONTROLLER_NAME)
         *     .and(ElementMatchers.named(REST_CONTROLLER_NAME))...
         *
         * @return
         */
        @Override
        public ElementMatcher.Junction<? super TypeDescription> buildJunction() {
            ElementMatcher.Junction<? super TypeDescription> junction = null;
            for (String annotationName : annotationNameList) {
                if (junction == null) {
                    junction = isAnnotatedWith(named(annotationName));
                } else {
                    junction = junction.and(isAnnotatedWith(named(annotationName)));
                }
            }
    
            return junction;
        }
    
        /**
         * 用于当前匹配器匹
         * 用于判断 typeDescription 是否满足 IndirectMatch 的实现
         *
         * @param typeDescription
         * @return
         */
        @Override
        public boolean isMatch(TypeDescription typeDescription) {
            /**
             * 比如annotationNameList = [xxx.xxx.Aannotation, xxx.xxx.Bannotation]
             * typeDescription上的注解是 @Aannotation(xxx) and @BAnnotation(yyy) true 否则 false
             */
            List<String> annotationList = new ArrayList<>(annotationNameList);
    
            // 获取typeDescription上的注解名称
            AnnotationList declaredAnnotations = typeDescription.getDeclaredAnnotations();
            for (AnnotationDescription annotationDescription : declaredAnnotations) {
                // 获取注解的全类名
                String actualName = annotationDescription.getAnnotationType().getActualName();
                annotationList.remove(actualName);
            }
            return annotationList.isEmpty();
        }
    
        public static IndirectMatch byMultiAnnotationMatch(String... annotationNames) {
            return new MultiAnnotationMatch(annotationNames);
        }
    }

    多个类名相等情况匹配

    java 复制代码
    package cn.git.agent.match;
    
    import net.bytebuddy.description.type.TypeDescription;
    import net.bytebuddy.matcher.ElementMatcher;
    
    import java.util.Arrays;
    import java.util.List;
    
    import static net.bytebuddy.matcher.ElementMatchers.named;
    
    /**
    * @description: 多个类名相等情况匹配
    * @program: bank-credit-sy
    * @author: lixuchun
    * @create: 2024-12-24
    */
    public class MultiClassNameMatch implements IndirectMatch {
    
       /**
        * 需要匹配的类名列表
        */
       private List<String> needMatchClassNameList;
    
       /**
        * 构造方法
        *
        * @param classNames
        */
       private MultiClassNameMatch(String[] classNames) {
           if (classNames == null || classNames.length == 0) {
               throw new IllegalArgumentException("classNames can not be null or empty");
           }
           this.needMatchClassNameList = Arrays.asList(classNames);
       }
    
       /**
        * 构建匹配规则
        * eg: named(CLASS_NAME1).or(named(CLASS_NAME2))
        *
        * @return
        */
       @Override
       public ElementMatcher.Junction<? super TypeDescription> buildJunction() {
           ElementMatcher.Junction<? super TypeDescription> junction = null;
           for (String className : needMatchClassNameList) {
               if (junction == null) {
                   junction = named(className);
               } else {
                   junction = junction.or(named(className));
               }
           }
           return junction;
       }
    
       /**
        * 用于当前匹配器
        * 用于判断 typeDescription 是否满足 IndirectMatch 的实现
        *
        * @param typeDescription
        * @return
        */
       @Override
       public boolean isMatch(TypeDescription typeDescription) {
           /**
            * 比如 needMatchClassNameList里面是 com.mysql.jdbc.PreparedStatement
            * 而 typeDescription 是 com.mysql.jdbc.PreparedStatement 返回true,否则返回false
            */
           return needMatchClassNameList.contains(typeDescription.getTypeName());
       }
    
       /**
        * 静态方法构建匹配规则
        *
        * @param classNames 匹配类名称
        * @return
        */
       public static IndirectMatch byMultiClassMatch(String... classNames) {
           return new MultiClassNameMatch(classNames);
       }
    }

4.3.2 插件模块

插件模块主要就是继承顶级插件父类,实现对应的类匹配以及方法匹配等,具体实现如下:

4.3.2.1 Mysql5Instrumentation
java 复制代码
package cn.git.agent;

import cn.git.agent.interceptor.ConstructMethodInterceptPoint;
import cn.git.agent.interceptor.InstanceMethodInterceptPoint;
import cn.git.agent.interceptor.StaticMethodInterceptPoint;
import cn.git.agent.match.ClassMatch;
import cn.git.agent.match.MultiClassNameMatch;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;

/**
 * @description: 定义mysql5的拦截插件
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-24
 */
public class Mysql5Instrumentation extends AbstractClassEnhancePluginDefine {

    /**
     * 拦截的方法
     */
    private static final String INSPECT_EXECUTE_METHOD = "execute";
    private static final String INSPECT_EXECUTE_QUERY = "executeQuery";
    private static final String INSPECT_EXECUTE_UPDATE = "executeUpdate";
    private static final String INSPECT_EXECUTE_LARGE_UPDATE = "executeLargeUpdate";

    /**
     * 拦截器类全路径名称
     */
    private static final String INTERCEPTOR_CLASS_NAME = "cn.git.agent.interceptor.Mysql5Interceptor";

    /**
     * 拦截类全路径名称
     */
    public static final String PREPARED_STATEMENT_NAME = "com.mysql.jdbc.PreparedStatement";

    /**
     * 获取需要拦截的类
     *
     * @return
     */
    @Override
    protected ClassMatch enhanceClass() {
        return MultiClassNameMatch.byMultiClassMatch(PREPARED_STATEMENT_NAME);
    }

    /**
     * 获取需要拦截的方法,实例拦截方法
     *
     * @return
     */
    @Override
    protected InstanceMethodInterceptPoint[] getInstanceMethodsInterceptPoints() {
        return new InstanceMethodInterceptPoint[] {
                new InstanceMethodInterceptPoint() {
                    /**
                     * 获取需要拦截的方法,作为method()方法的参数
                     *
                     * @return
                     */
                    @Override
                    public ElementMatcher<MethodDescription> getMethodsMatcher() {
                        return ElementMatchers.namedOneOf(INSPECT_EXECUTE_METHOD,
                                INSPECT_EXECUTE_QUERY,
                                INSPECT_EXECUTE_UPDATE,
                                INSPECT_EXECUTE_LARGE_UPDATE);
                    }

                    /**
                     * 获取需要拦截的方法的拦截器
                     *
                     * @return
                     */
                    @Override
                    public String getMethodInterceptor() {
                        return INTERCEPTOR_CLASS_NAME;
                    }
                }
        };
    }

    /**
     * 获取构造方法的拦截点
     *
     * @return
     */
    @Override
    protected ConstructMethodInterceptPoint[] getConstructMethodInterceptPoint() {
        return new ConstructMethodInterceptPoint[0];
    }

    /**
     * 获取静态方法拦截点
     *
     * @return
     */
    @Override
    protected StaticMethodInterceptPoint[] getStaticMethodInterceptPoint() {
        return new StaticMethodInterceptPoint[0];
    }
}
4.3.2.2 springMVC具体实现

mvc比较特殊,需要校验@Controller以及@RestController两个注解标注的类,所以 需要新增两个Instrumentation,并且还有一个公共的SpringMVCCommonInstrumentation,实现公共部分。

java 复制代码
package cn.git.agent;

import cn.git.agent.interceptor.ConstructMethodInterceptPoint;
import cn.git.agent.interceptor.InstanceMethodInterceptPoint;
import cn.git.agent.interceptor.StaticMethodInterceptPoint;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.matcher.ElementMatcher;

import static net.bytebuddy.matcher.ElementMatchers.*;

/**
 * @description: 定义springMVC的拦截插件
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-24
 */
public abstract class SpringMVCCommonInstrumentation extends AbstractClassEnhancePluginDefine {

    /**
     * 拦截的注解开头结尾
     */
    private static final String MAPPING_PACKAGE_PREFIX = "org.springframework.web.bind.annotation";
    private static final String MAPPING_PACKAGE_SUFFIX = "Mapping";

    /**
     * 拦截器类
     */
    public static final String SPRINGMVC_INTERCEPTOR_CLASS = "cn.git.agent.interceptor.SpringMVCInterceptor";

    /**
     * 获取需要拦截的方法,实例拦截方法
     *
     * @return
     */
    @Override
    protected InstanceMethodInterceptPoint[] getInstanceMethodsInterceptPoints() {
        return new InstanceMethodInterceptPoint[] {
                new InstanceMethodInterceptPoint() {
                    @Override
                    public ElementMatcher<MethodDescription> getMethodsMatcher() {
                        return not(isStatic())
                                .and(
                                        isAnnotatedWith(nameStartsWith(MAPPING_PACKAGE_PREFIX)
                                                .and(nameEndsWith(MAPPING_PACKAGE_SUFFIX)))
                                );
                    }

                    @Override
                    public String getMethodInterceptor() {
                        return SPRINGMVC_INTERCEPTOR_CLASS;
                    }
                }
        };
    }

    /**
     * 获取构造方法的拦截点
     *
     * @return
     */
    @Override
    protected ConstructMethodInterceptPoint[] getConstructMethodInterceptPoint() {
        return new ConstructMethodInterceptPoint[0];
    }

    /**
     * 获取静态方法拦截点
     *
     * @return
     */
    @Override
    protected StaticMethodInterceptPoint[] getStaticMethodInterceptPoint() {
        return new StaticMethodInterceptPoint[0];
    }
}

@Controller注解的实现

java 复制代码
package cn.git.agent;

import cn.git.agent.match.ClassMatch;
import cn.git.agent.match.MultiAnnotationMatch;

/**
 * @description: 定义springMVC的Controller拦截插件
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-24
 */
public class SpringMVCControllerInstrumentation extends SpringMVCCommonInstrumentation {

    /**
     * 控制器注解名称
     */
    public static final String CONTROLLER_NAME = "org.springframework.stereotype.Controller";

    /**
     * 拦截器类
     */
    public static final String SPRINGMVC_INTERCEPTOR_CLASS = "cn.git.agent.interceptor.SpringMVCInterceptor";

    /**
     * 获取需要拦截的类
     *
     * @return
     */
    @Override
    protected ClassMatch enhanceClass() {
        return MultiAnnotationMatch.byMultiAnnotationMatch(CONTROLLER_NAME);
    }
}

@RestController的实现

java 复制代码
package cn.git.agent;

import cn.git.agent.match.ClassMatch;
import cn.git.agent.match.MultiAnnotationMatch;

/**
 * @description: 定义springMVC的RestController拦截插件
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-24
 */
public class SpringMVCRestControllerInstrumentation extends SpringMVCCommonInstrumentation {

    /**
     * 控制器注解名称
     */
    public static final String REST_CONTROLLER_NAME = "org.springframework.web.bind.annotation.RestController";

    /**
     * 拦截器类
     */
    public static final String SPRINGMVC_INTERCEPTOR_CLASS = "cn.git.agent.interceptor.SpringMVCInterceptor";

    /**
     * 获取需要拦截的类
     *
     * @return
     */
    @Override
    protected ClassMatch enhanceClass() {
        return MultiAnnotationMatch.byMultiAnnotationMatch(REST_CONTROLLER_NAME);
    }
}

4.4 打包插件

4.4.1 插件需求

我们希望项目实现插件打包于skywalking相似,可以生成一个dest目录,放置我们的agent-jar包,并且可以生成一个plugins目录,以便我们之后的插件都可以放置到此目录,将我们的agent也做成一个可以动态插拔的效果。

4.4.2 具体实现

此时我们需要引入一个maven插件maven-antrun-plugin,其配置于apm-agent模块,具体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>
    <parent>
        <groupId>cn.git.agent</groupId>
        <artifactId>apm-sniffer</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <artifactId>amp-agent</artifactId>
    <packaging>jar</packaging>

    <name>amp-agent</name>
    <url>http://maven.apache.org</url>

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

    <dependencies>
        <dependency>
            <groupId>cn.git.agent</groupId>
            <artifactId>apm-agent-core</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <!-- 用于打包插件 -->
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <archive>
                        <manifestEntries>
                            <!-- MANIFEST.MF 配置项,指定premain方法所在类 -->
                            <Premain-Class>cn.git.agent.CustomSkyWalkingAgent</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>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id>
                        <!-- 什么阶段会触发此插件 -->
                        <phase>package</phase>
                        <goals>
                            <!-- 只运行一次 -->
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <!-- 创建目录,拷贝jar包到指定目录 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-antrun-plugin</artifactId>
                <version>1.8</version>
                <executions>
                    <execution>
                        <!-- 在clean阶段删除dist目录 -->
                        <id>clean</id>
                        <phase>clean</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                        <configuration>
                            <target>
                                <delete dir="${project.basedir}/../../dist"/>
                            </target>
                        </configuration>
                    </execution>
                    <execution>
                        <!-- 在package阶段创建dist目录,创建/dist/plugins目录,拷贝agent-jar到/dist目录下 -->
                        <id>package</id>
                        <phase>package</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                        <configuration>
                            <target>
                                <mkdir dir="${project.basedir}/../../dist"/>
                                <copy file="${project.build.directory}/amp-agent-1.0-SNAPSHOT-jar-with-dependencies.jar" tofile="${project.basedir}/../../dist/apm-agent-1.0.SNAPSHOT-jar-with-dependencies.jar" overwrite="true">
                                </copy>
                                <mkdir dir="${project.basedir}/../../dist/plugins"/>
                            </target>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

4.4.3 打包效果

插件更新完毕后,我们install,观察到dist目录以及plugins目录已经生成,并且jar包也复制到此目录下了

4.5 获取类型匹配器

我们需要类类型匹配器,其中包含名称以及非名称两种匹配器进行自动联合匹配,在岗进入探针的时候,调用find方法,发现所有的需要加载的类信息,具体的代码实现如下所示:

java 复制代码
package cn.git.agent;

import cn.git.agent.match.ClassMatch;
import cn.git.agent.match.IndirectMatch;
import cn.git.agent.match.NameMatch;
import net.bytebuddy.description.NamedElement;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;

import java.util.*;

/**
 * @description: 插件查找器
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-25
 */
public class PluginFinder {

    /**
     * 用于存储ClassMatch为NameMatcher的插件
     * key: 全类名 ,eg: cn.git.agent.service.impl.UserInfoServiceImpl
     * value: 处理此类的多个插件列表
     */
    private final Map<String, List<AbstractClassEnhancePluginDefine>> nameMatchDefine = new HashMap<>();

    /**
     * 用于存储ClassMatch为IndirectMatch间接匹配器的插件
     */
    private final List<AbstractClassEnhancePluginDefine> signatureMatchDefine = new ArrayList<>();

    /**
     * 构造函数
     *
     * @param pluginDefineList 所有加载的插件
     */
    public PluginFinder(List<AbstractClassEnhancePluginDefine> pluginDefineList) {
        for (AbstractClassEnhancePluginDefine pluginDefine : pluginDefineList) {
            // 获取类匹配器
            ClassMatch classMatch = pluginDefine.enhanceClass();
            if (classMatch == null) {
                continue;
            }
            // 判断是名称匹配
            if (classMatch instanceof NameMatch) {
                NameMatch nameMatch = (NameMatch) classMatch;
                // computeIfAbsent 方法会检查 nameMatchDefine 中是否存在键为 nameMatch.getClassName() 的条目。
                // 如果不存在,则使用提供的 lambda 表达式创建一个新的 LinkedList 并将其放入 Map 中,然后返回这个新的 LinkedList
                // 如果该键已经存在,则直接返回与该键关联的现有列表
                List<AbstractClassEnhancePluginDefine> enhancePluginDefineList = nameMatchDefine.computeIfAbsent(nameMatch.getClassName(), a -> new LinkedList<>());
                // 添加插件到名称匹配列表
                enhancePluginDefineList.add(pluginDefine);
            } else {
                // 间接匹配
                signatureMatchDefine.add(pluginDefine);
            }
        }
    }

    /**
     * 构建匹配器,返回已加载最终拼接的类匹配器条件
     *
     * @return plugin1_junction.or(plugin2_junction.or(plugin3_junction)).....
     */
    public ElementMatcher<? super TypeDescription> buildTypeMatch() {
        /**
         * 注释提到"当某个类第一次被加载时,都会回调该方法",这意味着每当 JVM 加载一个新的类时,ByteBuddy 会调用这个 matches 方法来判断是否对该类进行增强或其他操作。
         * 例如,如果 nameMatchDefine 中包含 cn.git.TestService,那么当 cn.git.TestService 类被加载时,matches 方法会返回 true,从而触发相应的插件逻辑。
         */
        ElementMatcher.Junction<? super TypeDescription> junction = new ElementMatcher.Junction.AbstractBase<NamedElement>() {
            @Override
            public boolean matches(NamedElement target) {
                // 当某个类第一次被加载时,都会回调该方法 eg: cn.git.TestService
                return nameMatchDefine.containsKey(target.getActualName());
            }
        };

        // 只增强类,不增强接口
        junction = junction.and(ElementMatchers.not(ElementMatchers.isInterface()));

        // 添加间接匹配插件
        for (AbstractClassEnhancePluginDefine pluginDefine : signatureMatchDefine) {
            ClassMatch classMatch = pluginDefine.enhanceClass();
            if (classMatch instanceof IndirectMatch) {
                IndirectMatch indirectMatch = (IndirectMatch) classMatch;
                junction = junction.or(indirectMatch.buildJunction());
            }
        }
        return junction;
    }

    /**
     * 通过插件类类型获取插件列表
     *
     * @param typeDescription
     * @return
     */
    public List<AbstractClassEnhancePluginDefine> find(TypeDescription typeDescription) {
        // 返回参数列表
        List<AbstractClassEnhancePluginDefine> matchedPlugins = new LinkedList<AbstractClassEnhancePluginDefine>();
        // 获取全类名称
        String typeName = typeDescription.getTypeName();

        // 获取名称匹配插件
        if (nameMatchDefine.containsKey(typeName)) {
            matchedPlugins.addAll(nameMatchDefine.get(typeName));
        }

        // 获取间接匹配插件SignatureMatchDefine
        for (AbstractClassEnhancePluginDefine pluginDefine : signatureMatchDefine) {
            IndirectMatch match = (IndirectMatch) pluginDefine.enhanceClass();
            if (match.isMatch(typeDescription)) {
                matchedPlugins.add(pluginDefine);
            }
        }

        return matchedPlugins;
    }
}

4.6 transform逻辑

transform主要的作用是具体的进行方法的增强处理,我们此处单独定义一个静态类进行实现,仿照skywalking在探针类中实现,使用pluginFinde.find方法获取就提类,然后使用具体类获取类的增强方法,

4.6.1 agent探针新增transform方法

java 复制代码
package cn.git.agent;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.scaffold.TypeValidation;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;

import java.lang.instrument.Instrumentation;
import java.util.List;

import static net.bytebuddy.matcher.ElementMatchers.nameContains;
import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;

/**
 * @description: 仿照skywalking编写的探针入口
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-23
 */
public class CustomSkyWalkingAgent {

    // 插件查找器
    private static PluginFinder pluginFinder;

    /**
     * premain方法,main方法执行之前进行调用,插桩代码入口
     * @param args 标识外部传递参数
     * @param instrumentation 插桩对象
     */
    public static void premain(String args, Instrumentation instrumentation) {
        // 创建AgentBuilder对象, 用于构建动态类
        // 默认开启类型验证,
        ByteBuddy byteBuddy = new ByteBuddy().with(TypeValidation.of(true));
        AgentBuilder builder = new AgentBuilder.Default(byteBuddy);

        try {
            pluginFinder = new PluginFinder(null);
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }

        // 忽略拦截的包
        builder.ignore(
                nameStartsWith("net.bytebuddy.")
                        .or(nameStartsWith("org.slf4j."))
                        .or(nameStartsWith("org.groovy."))
                        .or(nameContains("javassist"))
                        .or(nameContains(".asm."))
                        .or(nameContains(".reflectasm."))
                        .or(nameStartsWith("sun.reflect"))
                        .or(ElementMatchers.isSynthetic()));

        builder.type(pluginFinder.buildTypeMatch())
                .transform(null)
                        .installOn(instrumentation);

        // 安装
        builder.installOn(instrumentation);
    }

    /**
     * 方法拦截器,拼接对应类拦截后的方法拦截条件拼接
     */
    private static class Transformer implements AgentBuilder.Transformer {
        /**
         * 拦截方法
         * 正常返回   DynamicType.Builder.MethodDefinition.ReceiverTypeDefinition
         *          ReceiverTypeDefinition 继承 ReceiverTypeDefinition<U> extends MethodDefinition
         *          MethodDefinition 继承 MethodDefinition<S> extends Builder
         *          所以返回的 Builder 类型即可
         * @param builder
         * @param typeDescription
         *          可以获取拦截类名 this.getClass().getName() eg: cn.git.agent.service.impl.UserInfoServiceImpl
         *          可以获取增强插件 TypeName eg: com.mysql.jdbc.PreparedStatement
         *
         * @param classLoader
         * @param javaModule
         * @return
         */
        @Override
        public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
                                                TypeDescription typeDescription,
                                                ClassLoader classLoader,
                                                JavaModule javaModule) {
            System.out.println("Transformer方法加载 : typeDescription.getActualName: " + typeDescription.getActualName());
            System.out.println("Transformer方法加载 : typeDescription.getTypeName: " + typeDescription.getTypeName());
            // 获取插件列表
            List<AbstractClassEnhancePluginDefine> pluginDefineList = pluginFinder.find(typeDescription);
            if (pluginDefineList.size() > 0) {
                DynamicType.Builder<?> newBuilder = builder;
                for (AbstractClassEnhancePluginDefine pluginDefine : pluginDefineList) {
                    DynamicType.Builder<?> possiableBuilder = pluginDefine.define(typeDescription, newBuilder, classLoader);
                    if (possiableBuilder != null) {
                        newBuilder = possiableBuilder;
                    }
                }
                return newBuilder;
            }
            return builder;
        }
    }
}

4.6.2 插件顶级父类define实现

我们在顶级父类中,实现了 define 方法, 此方法中获取具体增强类eg: com.mysql.jdbc.PreparedStatement 还有具体的插件类cn.git.agent.Mysql5Instrumentation,然后分别定义抽象的静态方法增强方法以及实例方法增强方法,使用子类进行具体的实现。具体方法实现代码如下:

java 复制代码
/**
 * 增强类的主入口
 *
 * @param typeDescription
 * @param builder
 * @param classLoader
 * @return
 */
public DynamicType.Builder<?> define(TypeDescription typeDescription,
								 DynamicType.Builder<?> builder,
								 ClassLoader classLoader) {
	// 增强插件类的名字 eg: cn.git.agent.Mysql5Instrumentation
	String pluginsDefineClassName = this.getClass().getName();

	// 要增强的类名称 eg: com.mysql.jdbc.PreparedStatement
	String typeName = typeDescription.getTypeName();
	System.out.println("准备使用" + pluginsDefineClassName + "增强" + typeName + "开始");
	DynamicType.Builder<?> newBuilder = this.enhance(typeDescription, builder, classLoader);
	System.out.println("使用" + pluginsDefineClassName + "增强" + typeName + "成功");
	return newBuilder;
}

private DynamicType.Builder<?> enhance(TypeDescription typeDescription,
								   DynamicType.Builder<?> builder,
								   ClassLoader classLoader) {
	// 增强静态方法
	builder = this.enhanceClass(typeDescription, builder, classLoader);
	// 增强实例方法
	builder = this.enhanceInstance(typeDescription, builder, classLoader);

	return builder;
}

/**
 * 增强实例方法
 * @param typeDescription
 * @param builder
 * @param classLoader
 * @return
 */
protected abstract DynamicType.Builder<?> enhanceInstance(TypeDescription typeDescription,
													  DynamicType.Builder<?> builder,
													  ClassLoader classLoader);

/**
 * 增强静态方法
 * @param typeDescription
 * @param builder
 * @param classLoader
 * @return
 */
protected abstract DynamicType.Builder<?> enhanceClass(TypeDescription typeDescription,
												   DynamicType.Builder<?> builder,
												   ClassLoader classLoader);

4.7 静态构造实例方法增强

我们在顶级插件父类新增了两个抽象方法,enhanceInstance,以及 enhanceClass用于实现实际增强方法实现,在此基础上我们新增ClassEnhancePluginDefine类,继承顶级父类,并且实现这两个抽象方法,然后基础插件类便直接实现此类即可,无需实现顶级父类,具体逻辑代码如下所示:

4.7.1 ClassEnhancePluginDefine

java 复制代码
package cn.git.agent.enhance;

import cn.git.agent.AbstractClassEnhancePluginDefine;
import cn.git.agent.interceptor.ConstructMethodInterceptPoint;
import cn.git.agent.interceptor.InstanceMethodInterceptPoint;
import cn.git.agent.interceptor.StaticMethodInterceptPoint;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.SuperMethodCall;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;

/** 
 * @description: 所有插件都继承AbstractClassEnhancePluginDefine
 * 此类完成了enhanceInstance,以及 enhanceStaticClass方法,完成增强(transform指定method以及interceptor)
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-25
 */
public abstract class ClassEnhancePluginDefine extends AbstractClassEnhancePluginDefine {

    /**
     * 增强实例与构造方法
     * builder.method(
     * 	ElementMatchers.namedOneOf(INSPECT_EXECUTE_METHOD,
     * 			INSPECT_EXECUTE_QUERY,
     * 			INSPECT_EXECUTE_UPDATE,
     * 			INSPECT_EXECUTE_LARGE_UPDATE)
     * )
     * .intercept(MethodDelegation.to(interceptor));
     *
     * @param typeDescription
     * @param builder
     * @param classLoader
     * @return
     */
    @Override
    protected DynamicType.Builder<?> enhanceInstance(TypeDescription typeDescription,
                                                     DynamicType.Builder<?> builder,
                                                     ClassLoader classLoader) {
        // 获取实例以及构造方法拦截点
        InstanceMethodInterceptPoint[] instanceMethodsInterceptPoints = this.getInstanceMethodsInterceptPoints();
        ConstructMethodInterceptPoint[] constructMethodInterceptPoints = this.getConstructMethodInterceptPoint();

        // 是否存在构造器拦截点
        boolean existsConstructorInterceptPoint = false;
        if (constructMethodInterceptPoints != null && constructMethodInterceptPoints.length > 0) {
            existsConstructorInterceptPoint = true;
        }

        // 是否存在实例拦截点
        boolean existsInstanceInterceptPoint = false;
        if (instanceMethodsInterceptPoints != null && instanceMethodsInterceptPoints.length > 0) {
            existsInstanceInterceptPoint = true;
        }

        // 如果不存在拦截点,则直接返回
        if (!existsConstructorInterceptPoint && !existsInstanceInterceptPoint) {
            return builder;
        }


        // 拼装方法拦截点
        String typeName = typeDescription.getTypeName();

        // 开始增强实例方法
        if (existsInstanceInterceptPoint) {
            for (InstanceMethodInterceptPoint point : instanceMethodsInterceptPoints) {
                // 静态方法匹配器
                ElementMatcher<MethodDescription> methodsMatcher = point.getMethodsMatcher();
                // 拦截器名称
                String interceptorClassName = point.getMethodInterceptor();
                if (interceptorClassName == null || interceptorClassName.trim().isEmpty()) {
                    throw new RuntimeException("要增强的实例方法类[{" + typeName + "}]未指定拦截器");
                }

                builder = builder.method(ElementMatchers.not(ElementMatchers.isStatic())
                                .and(methodsMatcher))
                        .intercept(MethodDelegation.withDefaultConfiguration()
                                .to(new InstanceMethodsInterceptor(interceptorClassName, classLoader)));
            }
        }

        if (existsConstructorInterceptPoint) {
            for (ConstructMethodInterceptPoint constructPoint : constructMethodInterceptPoints) {
                // 静态方法匹配器
                ElementMatcher<MethodDescription> methodsMatcher = constructPoint.getConstructMethodsMatcher();
                // 拦截器名称
                String interceptorClassName = constructPoint.getMethodInterceptor();
                if (interceptorClassName == null || interceptorClassName.trim().isEmpty()) {
                    throw new RuntimeException("要增强的构造方法类[{" + typeName + "}]未指定拦截器");
                }

                // 创建builder信息,指定拦截器信息
                builder = builder.method(methodsMatcher)
                        .intercept(SuperMethodCall.INSTANCE.andThen(
                                MethodDelegation.withDefaultConfiguration()
                                        .to(new ConstructorMethodsInterceptor(interceptorClassName, classLoader))
                        ));
            }
        }

        return null;
    }

    /**
     * 增强静态方法
     * builder.method(
     * 	ElementMatchers.namedOneOf(INSPECT_EXECUTE_METHOD,
     * 			INSPECT_EXECUTE_QUERY,
     * 			INSPECT_EXECUTE_UPDATE,
     * 			INSPECT_EXECUTE_LARGE_UPDATE)
     * )
     * .intercept(MethodDelegation.to(interceptor));
     * @param typeDescription
     * @param builder
     * @param classLoader
     * @return
     */
    @Override
    protected DynamicType.Builder<?> enhanceStaticClass(TypeDescription typeDescription,
                                                        DynamicType.Builder<?> builder,
                                                        ClassLoader classLoader) {

        // 获取我们的静态拦截点
        StaticMethodInterceptPoint[] staticMethodInterceptPoint = this.getStaticMethodInterceptPoint();
        if (staticMethodInterceptPoint == null || staticMethodInterceptPoint.length == 0) {
            return builder;
        }

        // 拼装方法拦截点
        String typeName = typeDescription.getTypeName();
        for (StaticMethodInterceptPoint point : staticMethodInterceptPoint) {
            // 静态方法匹配器
            ElementMatcher<MethodDescription> staticMethodsMatcher = point.getStaticMethodsMatcher();
            // 拦截器名称
            String interceptorClassName = point.getMethodInterceptor();
            if (interceptorClassName == null || interceptorClassName.trim().isEmpty()) {
                throw new RuntimeException("要增强的类[{" + typeName + "}]未指定拦截器");
            }

            builder = builder.method(ElementMatchers.isStatic()
                    .and(staticMethodsMatcher))
                    .intercept(MethodDelegation.withDefaultConfiguration()
                            .to(new StaticMethodsInterceptor(interceptorClassName, classLoader)));
        }
        return builder;
    }
}

4.7.2 MethodsInterceptor

增强静态构造以及实例方法我们主要新增了三个MethodsInterceptor 类作为三个方法byteBuddy拦截器,里面的逻辑就是我们正常的拦截方法,传入class参数,传入params参数等,可以执行父级的call方法,在前后以及异常进行增,三种方法的方法增强类代码如下:

  • 静态方法拦截类

    java 复制代码
    package cn.git.agent.enhance;
    
    import net.bytebuddy.implementation.bind.annotation.*;
    
    import java.lang.reflect.Method;
    import java.util.concurrent.Callable;
    
    /**
     * @description: 静态方法byteBuddy拦截器
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-25
     */
    public class StaticMethodsInterceptor {
    
        /**
         * 静态方法环绕通知器
         */
        private StaticMethodAroundInterceptor aroundInterceptor;
    
        /**
         * 构造方法
         *
         * @param interceptorClassName
         * @param classLoader
         */
        public StaticMethodsInterceptor(String interceptorClassName,
                                        ClassLoader classLoader) {
    
        }
    
        /**
         * 被标注 RuntimeType 注解的方法就是拦截方法,此时返回的值与返回参数可以与被拦截的方法不一致
         * byteBuddy会在运行期间给被拦截的方法参数进行赋值
         * @return
         */
        @RuntimeType
        public Object intercept(
                @Origin Class<?> clazz,
                @Origin Method targetMethod,
                @AllArguments Object[] targetMethodArgs,
                @SuperCall Callable<?> superCall) throws Throwable {
            Object call = null;
    
            try {
                aroundInterceptor.beforeMethod(clazz, targetMethod, targetMethodArgs, targetMethod.getParameterTypes());
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println("class [" + clazz.getName() + "] before exec method [" + targetMethod.getName() + "] interceptor failure [" + e + "]");
            }
    
            try {
                call = superCall.call();
            } catch (Throwable e) {
                try {
                    aroundInterceptor.handleMethodException(clazz,
                            targetMethod,
                            targetMethodArgs,
                            targetMethod.getParameterTypes(),
                            e);
                } catch (Throwable t) {
                    System.out.println("class [" + clazz.getName() + "] handle static [" + targetMethod.getName() + "] interceptor failure [" + e + "]");
                    e.printStackTrace();
                }
                throw e;
            } finally {
                try {
                    aroundInterceptor.afterMethod(clazz,
                            targetMethod,
                            targetMethodArgs,
                            targetMethod.getParameterTypes(),
                            call);
                } catch (Throwable e) {
                    e.printStackTrace();
                    System.out.println("class [" + clazz.getName() + "] after exec method [" + targetMethod.getName() + "] interceptor failure [" + e + "]");
                }
                System.out.println("targetMethodName : " + targetMethod.getName());
            }
            return call;
        }
    }
  • 实例方法拦截类

    java 复制代码
    package cn.git.agent.enhance;
    
    import net.bytebuddy.implementation.bind.annotation.*;
    
    import java.lang.reflect.Method;
    import java.util.concurrent.Callable;
    
    /**
     * @description: 实例方法byteBuddy拦截器
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-25
     */
    public class InstanceMethodsInterceptor {
    
        /**
         * 实例方法环绕通知器
         */
        private InstanceMethodAroundInterceptor aroundInterceptor;
    
        /**
         * 构造方法
         *
         * @param interceptorClassName
         * @param classLoader
         */
        public InstanceMethodsInterceptor(String interceptorClassName,
                                          ClassLoader classLoader) {
    
        }
    
        /**
         * 被标注 RuntimeType 注解的方法就是拦截方法,此时返回的值与返回参数可以与被拦截的方法不一致
         * byteBuddy会在运行期间给被拦截的方法参数进行赋值
         * @return
         */
        @RuntimeType
        public Object intercept(
                @This Object targetObject,
                @Origin Method targetMethod,
                @AllArguments Object[] targetMethodArgs,
                @SuperCall Callable<?> superCall) throws Throwable {
            Object call = null;
    
            // 执行后前置方法
            try {
                aroundInterceptor.beforeMethod(targetObject, targetMethod, targetMethodArgs, targetMethod.getParameterTypes());
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println("class [" + targetObject.getClass().getName() + "] before exec method [" + targetMethod.getName() + "] interceptor failure [" + e + "]");
            }
    
            // 执行原方法
            try {
                call = superCall.call();
            } catch (Throwable e) {
                try {
                    // 执行异常处理方法
                    aroundInterceptor.handleMethodException(targetObject,
                            targetMethod,
                            targetMethodArgs,
                            targetMethod.getParameterTypes(),
                            e);
                } catch (Throwable t) {
                    System.out.println("class [" + targetObject.getClass().getName() + "] handle instance [" + targetMethod.getName() + "] interceptor failure [" + e + "]");
                    e.printStackTrace();
                }
                throw e;
            } finally {
                try {
                    // 执行后置方法
                    aroundInterceptor.afterMethod(targetObject,
                            targetMethod,
                            targetMethodArgs,
                            targetMethod.getParameterTypes(),
                            call);
                } catch (Throwable e) {
                    e.printStackTrace();
                    System.out.println("class [" + targetObject.getClass().getName() + "] after exec method [" + targetMethod.getName() + "] interceptor failure [" + e + "]");
                }
                System.out.println("targetMethodName : " + targetMethod.getName());
            }
            return call;
        }
    }
  • 构造方法拦截类

    java 复制代码
    package cn.git.agent.enhance;
    
    import net.bytebuddy.implementation.bind.annotation.AllArguments;
    import net.bytebuddy.implementation.bind.annotation.RuntimeType;
    import net.bytebuddy.implementation.bind.annotation.This;
    
    /**
     * @description: 构造方法拦截器
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-25
     */
    public class ConstructorMethodsInterceptor {
    
        /**
         * 静态方法环绕通知器
         */
        private ConstructorMethodAroundInterceptor aroundInterceptor;
    
        /**
         * 构造方法
         *
         * @param interceptorClassName
         * @param classLoader
         */
        public ConstructorMethodsInterceptor(String interceptorClassName,
                                             ClassLoader classLoader) {
    
        }
    
        /**
         * 被标注 RuntimeType 注解的方法就是拦截方法,此时返回的值与返回参数可以与被拦截的方法不一致
         * byteBuddy会在运行期间给被拦截的方法参数进行赋值
         * @return
         */
        @RuntimeType
        public Object intercept(@This Object obj,
                                @AllArguments Object[] allArguments) throws Throwable {
            Object call = null;
            try {
                aroundInterceptor.onConstruct(obj, allArguments);
            } catch (Throwable t) {
                t.printStackTrace();
            }
    
            return call;
        }
    }

在此基础上,我们还定义了三个环绕切面类 MethodAroundInterceptor,其中切面类里面有前置方法,后置方法以及异常执行方法,可以在增强方法中进行执行,那么我们具体实现代码如下:

  • 静态方法环绕执行拦截器

    java 复制代码
    package cn.git.agent.enhance;
    
    import java.lang.reflect.Method;
    
    /**
     * @description: 静态方法的interceptor必须实现此接口
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-25
     */
    public interface StaticMethodAroundInterceptor {
    
        /**
         * 前置方法
         * @param clazz
         * @param method
         * @param allArguments
         * @param paramTypes
         */
        void beforeMethod(Class clazz,
                          Method method,
                          Object[] allArguments,
                          Class<?>[] paramTypes);
    
        /**
         * 后置方法,最終通知,不管原方法是否執行都會執行,finally裡面邏輯
         *
         * @param clazz
         * @param method
         * @param allArguments
         * @param paramTypes
         * @param ret
         */
        void afterMethod(Class clazz,
                         Method method,
                         Object[] allArguments,
                         Class<?>[] paramTypes,
                         Object ret);
    
        /**
         * 异常方法
         * @param clazz
         * @param method
         * @param allArguments
         * @param paramTypes
         * @param t
         */
        void handleMethodException(Class clazz,
                                   Method method,
                                   Object[] allArguments,
                                   Class<?>[] paramTypes,
                                   Throwable t);
    }
  • 实例方法环绕拦截器

    java 复制代码
    package cn.git.agent.enhance;
    
    import java.lang.reflect.Method;
    
    /**
     * @description: 静态方法的interceptor必须实现此接口
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-25
     */
    public interface InstanceMethodAroundInterceptor {
    
        /**
         * 前置方法
         * @param objectInstance
         * @param method
         * @param allArguments
         * @param paramTypes
         */
        void beforeMethod(Object objectInstance,
                          Method method,
                          Object[] allArguments,
                          Class<?>[] paramTypes);
    
        /**
         * 后置方法,最終通知,不管原方法是否執行都會執行,finally裡面邏輯
         *
         * @param objectInstance
         * @param method
         * @param allArguments
         * @param paramTypes
         * @param ret
         */
        void afterMethod(Object objectInstance,
                         Method method,
                         Object[] allArguments,
                         Class<?>[] paramTypes,
                         Object ret);
    
        /**
         * 异常方法
         * @param objectInstance
         * @param method
         * @param allArguments
         * @param paramTypes
         * @param t
         */
        void handleMethodException(Object objectInstance,
                                   Method method,
                                   Object[] allArguments,
                                   Class<?>[] paramTypes,
                                   Throwable t);
    }
  • 构造方法实例拦截器

    java 复制代码
    package cn.git.agent.enhance;
    
    /**
     * @description: 构造方法拦截器环绕通知类,构造方法拦截器必须实现此接口
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-25
     */
    public interface ConstructorMethodAroundInterceptor {
    
        /**
         * 拦截构造方法, 在构造方法执行后调用
         *
         * @param obj               拦截到的对象,构造器返回对象
         * @param allArguments      拦截到的所有参数
         */
        void onConstruct(Object obj,
                         Object[] allArguments);
    }

4.7.3 基础插件实现环绕增强方法

我们基础插件需要实现我们的环绕增强方法,我们的主要逻辑就在这实现的方法之中,我们的mysql以及springMVC具体的实现逻辑简单逻辑如下:

  • mysql5环绕增强方法

    java 复制代码
    package cn.git.agent.interceptor;
    
    import cn.git.agent.enhance.InstanceMethodAroundInterceptor;
    
    import java.lang.reflect.Method;
    
    /**
     * @description: mysql5定义拦截器
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-24
     */
    public class Mysql5Interceptor implements InstanceMethodAroundInterceptor {
        /**
         * 前置方法
         *
         * @param objectInstance
         * @param method
         * @param allArguments
         * @param paramTypes
         */
        @Override
        public void beforeMethod(Object objectInstance, Method method, Object[] allArguments, Class<?>[] paramTypes) {
            System.out.println("mysql5 具体方法 beforeMethod 执行开始");
        }
    
        /**
         * 后置方法,最終通知,不管原方法是否執行都會執行,finally裡面邏輯
         *
         * @param objectInstance
         * @param method
         * @param allArguments
         * @param paramTypes
         * @param ret
         */
        @Override
        public void afterMethod(Object objectInstance, Method method, Object[] allArguments, Class<?>[] paramTypes, Object ret) {
            System.out.println("mysql5 具体方法 afterMethod 执行开始");
        }
    
        /**
         * 异常方法
         *
         * @param objectInstance
         * @param method
         * @param allArguments
         * @param paramTypes
         * @param t
         */
        @Override
        public void handleMethodException(Object objectInstance, Method method, Object[] allArguments, Class<?>[] paramTypes, Throwable t) {
            System.out.println("mysql5 具体方法 handleMethodException 执行开始 error : " + t.getMessage());
        }
    }
  • springMVC的环绕增强方法

    java 复制代码
    package cn.git.agent.interceptor;
    
    import cn.git.agent.enhance.InstanceMethodAroundInterceptor;
    
    import java.lang.reflect.Method;
    
    /**
     * @description: spirngMVC拦截器
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-24
     */
    public class SpringMVCInterceptor implements InstanceMethodAroundInterceptor {
        /**
         * 前置方法
         *
         * @param objectInstance
         * @param method
         * @param allArguments
         * @param paramTypes
         */
        @Override
        public void beforeMethod(Object objectInstance, Method method, Object[] allArguments, Class<?>[] paramTypes) {
            System.out.println("SpringMVCInterceptor 具体方法 beforeMethod 执行开始");
        }
    
        /**
         * 后置方法,最終通知,不管原方法是否執行都會執行,finally裡面邏輯
         *
         * @param objectInstance
         * @param method
         * @param allArguments
         * @param paramTypes
         * @param ret
         */
        @Override
        public void afterMethod(Object objectInstance, Method method, Object[] allArguments, Class<?>[] paramTypes, Object ret) {
            System.out.println("SpringMVCInterceptor 具体方法 afterMethod 执行开始");
        }
    
        /**
         * 异常方法
         *
         * @param objectInstance
         * @param method
         * @param allArguments
         * @param paramTypes
         * @param t
         */
        @Override
        public void handleMethodException(Object objectInstance, Method method, Object[] allArguments, Class<?>[] paramTypes, Throwable t) {
            System.out.println("SpringMVCInterceptor 具体方法 handleMethodException 执行开始 error: " + t.getMessage());
        }
    }

4.8 扩展新属性存储额外信息

我们在实例类型以及构造器类型方法调用的时候,before,handle以及after方法调用,我们希望他们可以进行中间值的传递,比如在before设置一个值,在after中进行使用,那么我们需要在增强类中新增一个比较特殊的属性信息,这个属性需要实现get set方法。

并且我们新增的时候每一个类只能新增一次,否则同一属性新增到类中会有问题。首先我们新增一个 EnhancedContext 用于设置增强以及扩展属性值

java 复制代码
package cn.git.agent.enhance;

/** 
 * @description: 处理一个类的上下文状态
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-26
 */
public class EnhancedContext {

    /**
     * 是否被增强
     */
    private boolean isEnhanced = false;

    /**
     * 是否被扩展,新增属性 CONTEXT_ATTR_NAME
     */
    private boolean objectExtended = false;


    public boolean isEnhanced() {
        return isEnhanced;
    }

    public boolean isObjectExtended() {
        return objectExtended;
    }

    public void initializationStageCompleted() {
        this.isEnhanced = true;
    }

    public void objectExtendedCompleted() {
        this.objectExtended = true;
    }
}

我们还需要新增一个接口,接口定义了新增字段,并且还要新增字段的get 以及set方法,我们先新增一个基础接口类

java 复制代码
package cn.git.agent.enhance;

/**
 * @description: 被增强的实例信息,所有需要增强构造或者实例的字节码都需要实现此接口
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-26
 */
public interface EnhancedInstance {

    /**
     * 获取被增强的实例
     * @return
     */
    Object getSkyWalkingDynamicField();

    /**
     * 设置被增强的参数值
     * @param value
     */
    void setSkyWalkingDynamicField(Object value);

}

我们一个类执行处理一次,所以我们在最开始的agent类入口新增一个EnhancedContext,后期transformer在操作一个typeDescription都是对应一个EnhancedContext,这样就可以判定全局唯一执行一次。

java 复制代码
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
										TypeDescription typeDescription,
										ClassLoader classLoader,
										JavaModule javaModule) {
	System.out.println("Transformer方法加载 : typeDescription.getActualName: " + typeDescription.getActualName());
	System.out.println("Transformer方法加载 : typeDescription.getTypeName: " + typeDescription.getTypeName());
	// 获取插件列表
	List<AbstractClassEnhancePluginDefine> pluginDefineList = pluginFinder.find(typeDescription);
	if (pluginDefineList.size() > 0) {
		// 创建动态类
		DynamicType.Builder<?> newBuilder = builder;
		// 创建EnhancedContext对象
		EnhancedContext enhancedContext = new EnhancedContext();
		for (AbstractClassEnhancePluginDefine pluginDefine : pluginDefineList) {
			DynamicType.Builder<?> possiableBuilder = pluginDefine.define(typeDescription, newBuilder, classLoader, enhancedContext);
			if (possiableBuilder != null) {
				newBuilder = possiableBuilder;
			}
		}

		// 打印增强信息
		if (enhancedContext.isEnhanced()) {
			System.out.println("使用" + pluginDefineList.get(0).getClass().getName() + "增强" + typeDescription.getActualName() + "成功");
		}
		return newBuilder;
	}
	return builder;
}

我们修改了pluginDefine.define的方法参数,新增了enhancedContext,并且后面的enhance方法我们也有变动,在define方法中,enhance方法执行完毕我们要设置增强类型设置true已增强状态

java 复制代码
public DynamicType.Builder<?> define(TypeDescription typeDescription,
									 DynamicType.Builder<?> builder,
									 ClassLoader classLoader,
									 EnhancedContext enhancedContext) {
	// 增强插件类的名字 eg: cn.git.agent.Mysql5Instrumentation
	String pluginsDefineClassName = this.getClass().getName();

	// 要增强的类名称 eg: com.mysql.jdbc.PreparedStatement
	String typeName = typeDescription.getTypeName();
	System.out.println("准备使用" + pluginsDefineClassName + "增强" + typeName + "开始");
	DynamicType.Builder<?> newBuilder = this.enhance(typeDescription, builder, classLoader, enhancedContext);

	// 调用初始化完成,增强类型设置true已增强
	enhancedContext.initializationStageCompleted();

	System.out.println("使用" + pluginsDefineClassName + "增强" + typeName + "成功");
	return newBuilder;
}

在我们的enhanceInstance中,我们需要判定类是否已经新增了属性,被增强了,如果没有则新增自定义属性值,新增get set方法

java 复制代码
// 为字节码新增属性,三个参数 名称,类型,修饰符 private | volatile ,并且新增get set方法
// 对于同一个typeDescription,只能定义一个属性,否则会报错,执行一次
/**
 * isAssignableTo作用
 * 如果 typeDescription 描述的是 EnhancedInstance 本身,返回 true。
 * 如果 typeDescription 描述的是 EnhancedInstance 的子类或实现了 EnhancedInstance 接口的类,也返回 true。
 * 否则返回 false。
 */
if (!typeDescription.isAssignableTo(EnhancedInstance.class)) {
	// 没有被扩展过判定
	if (!enhancedContext.isObjectExtended()) {
		builder.defineField(CONTEXT_ATTR_NAME,
				Object.class,
				Opcodes.ACC_PRIVATE | Opcodes.ACC_VOLATILE)
				// 指定实现的接口,get set
				.implement(EnhancedInstance.class)
				.intercept(FieldAccessor.ofField(CONTEXT_ATTR_NAME));

		// 修改扩展状态为已扩展过
		enhancedContext.objectExtendedCompleted();
	}
}

我们修改构造方法以及实例方法增强interceptor,修改targetObject类型为EnhancedInstance,并且修改before,after以及exception方法类型,同样需要修改已经实现的mysql以及mvc对应的 Mysql5Interceptor以及 SpringMVCInterceptor参数类型为 EnhancedInstance,此部分代码就不再赘述了。

4.9 自定义类加载器加载插件

我们需要新增一个类加载器,我们需要加载我们打包后的plugins目录中的jar包文件,而且我们自定义的jar包文件中,resources目录下我们新增了一个自定义的配置描述文件,描述我们每个插件的Instrumentation类全名地址,这样可以使用类全名找到每个插件的定义内容,然后每个插件都可以使用顶层父类接收,这样便可以加载全部插件的基本信息。

首先我们新增一个PluginBootstrap启动加载类,用于读取插件的jar包

java 复制代码
package cn.git.agent;

import cn.git.agent.loader.AgentClassLoader;

import java.net.URL;
import java.util.ArrayList;
import java.util.List;

/** 
 * @description: 插件启动类
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-26
 */
public class PluginBootstrap {

    /**
     * 加载所有插件
     * 因为是自定义路径下的jar包,所以需要手动加载
     *  1.获取到agent-jar的路径
     *  2.使用自定义类加载器加载插件
     *
     * @return
     */
    public List<AbstractClassEnhancePluginDefine> loadPlugins() {
        AgentClassLoader.initDefaultClassLoader();

        PluginResourceResolver resourceResolver = new PluginResourceResolver();
        List<URL> resources = resourceResolver.getResources();

        if (resources == null && resources.size() == 0) {
            System.out.println("no plugins define file git-plugins.def found !");
            return new ArrayList<>();
        }

        for (URL resource : resources) {
            try {
                PluginCfg.INSTANCE.load(resource.openStream());
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println("load plugins define file git-plugins.def failed !");
            }
        }

        // 拿到全类目,通过反射获取到实例对象,使用AbstractClassEnhancePluginDefine进行接收
        List<PluginDefine> pluginClassList = PluginCfg.INSTANCE.getPluginClassList();
        List<AbstractClassEnhancePluginDefine> pluginDefineList = new ArrayList<>();
        for (PluginDefine pluginDefine : pluginClassList) {
            try {
                AbstractClassEnhancePluginDefine plugin = (AbstractClassEnhancePluginDefine) Class.forName(
                        pluginDefine.getDefineClass(),
                        true,
                        AgentClassLoader.getDefaultClassLoader()
                ).newInstance();
                pluginDefineList.add(plugin);
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println("load plugin define class " + pluginDefine.getDefineClass() + " failed !");
            }
        }
        return pluginDefineList;
    }
}

新增一个类加载器的子类,用于新增一个类加载器,加载自己自定义的目录jar文件信息

java 复制代码
package cn.git.agent.loader;

import cn.git.agent.PluginBootstrap;
import cn.git.agent.boot.AgentPackagePath;
import lombok.RequiredArgsConstructor;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;
import java.util.concurrent.locks.ReentrantLock;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * @description: 用于加载插件和插件的拦截器
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-26
 */
public class AgentClassLoader extends ClassLoader {

    /**
     * 默认的类加载器
     * 用于定义 Mysql5Instrumentation等类,除了插件的interceptor
     */
    private static AgentClassLoader DEFAULT_CLASS_LOADER;

    /**
     * 插件的类路径
     */
    private List<File> classPath;
    private List<Jar> allJars;

    /**
     * 加载jar文件的锁,避免多次加载
     */
    private ReentrantLock jarScanLock = new ReentrantLock();

    /**
     * 构造函数
     *
     * @param parent
     */
    public AgentClassLoader(ClassLoader parent) {
        super(parent);
        // agent.jar的目录
        File agentJarDir = AgentPackagePath.getPath();
        classPath = new LinkedList<>();
        classPath.add(new File(agentJarDir, "plugins"));
    }

    public static AgentClassLoader getDefaultClassLoader() {
        return DEFAULT_CLASS_LOADER;
    }

    /**
     * 初始化默认的类加载器
     */
    public static void initDefaultClassLoader() {
        if (DEFAULT_CLASS_LOADER == null) {
            DEFAULT_CLASS_LOADER = new AgentClassLoader(PluginBootstrap.class.getClassLoader());
        }
    }

    /**
     * 重写findClass方法
     *  loadClass -> 自己回调 findClass(自己定义自己的类加载器) -> defineClass
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Jar> allJars = getAllJars();

        // 获取类名
        String path = name.replace(".", "/").concat(".class");

        for (Jar jar : allJars) {
            JarEntry entry = jar.jarFile.getJarEntry(path);
            if (entry == null) {
                continue;
            }

            // 获取jar文件
            try {
                URL url = new URL("jar:file" + jar.sourceFile.getAbsolutePath() + "!/" + path);
                byte[] bytes = IOUtils.toByteArray(url);
                return defineClass(name, bytes, 0, bytes.length);
            } catch (Exception e) {
                e.printStackTrace();
                System.out.println("Can not load jar file:" + jar.sourceFile.getAbsolutePath());
            }
        }
        throw new ClassNotFoundException("can not find " + name);
    }


    /**
     * 重写getResources方法
     *
     * @param name
     * @return
     * @throws IOException
     */
    @Override
    public Enumeration<URL> getResources(String name) throws IOException {
        List<URL> allResources = new LinkedList<>();
        List<Jar> allJars = getAllJars();
        for (Jar jar : allJars) {
            JarEntry jarEntry = jar.jarFile.getJarEntry(name);
            if (jarEntry != null) {
                allResources.add(new URL("jar:file" + jar.sourceFile.getAbsolutePath() + "!/" + name));
            }
        }

        // 迭代
        Iterator<URL> iterator = allResources.iterator();
        return new Enumeration<URL>() {
            @Override
            public boolean hasMoreElements() {
                return iterator.hasNext();
            }
            @Override
            public URL nextElement() {
                return iterator.next();
            }
        };
    }

    /**
     * 重写getResource方法
     *
     * @param name
     * @return
     */
    @Override
    public URL getResource(String name) {
        for (Jar jar : allJars) {
            JarEntry jarEntry = jar.jarFile.getJarEntry(name);
            if (jarEntry != null) {
                try {
                    return new URL("jar:file" + jar.sourceFile.getAbsolutePath() + "!/" + name);
                } catch (Exception e) {
                    System.out.println("Can not load jar file:" + jar.sourceFile.getAbsolutePath());
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    /**
     * 获取所有的jar文件
     * @return
     */
    private List<Jar> getAllJars() {
        if (allJars == null) {
            jarScanLock.lock();
            try {
                // 避免多次加载
                if (allJars == null) {
                    allJars = doGetJar();
                }
            } finally {
                jarScanLock.unlock();
            }
        }
        return allJars;
    }

    /**
     * 获取所有的jar文件
     *
     * @return
     */
    private List<Jar> doGetJar() {
        // 返回参数
        List<Jar> list = new LinkedList<>();
        //eg: D:/apm-sniffer/apm-agent-core/target/classes/plugins
        for (File path : classPath) {
            if (path.exists() && path.isDirectory()) {
                // jarFile名称
                String[] jarFileNames = path.list(new FilenameFilter() {
                    @Override
                    public boolean accept(File dir, String name) {
                        return name.endsWith(".jar");
                    }
                });
                if (ArrayUtils.isEmpty(jarFileNames)) {
                    continue;
                }

                for (String jarFileName : jarFileNames) {
                    File jarSourceFile = new File(path, jarFileName);
                    Jar jar = null;
                    try {
                        jar = new Jar(new JarFile(jarSourceFile), jarSourceFile);
                        list.add(jar);
                        System.out.println("Load jar file:" + jarSourceFile.getAbsolutePath());
                    } catch (IOException e) {
                        e.printStackTrace();
                        System.out.println("Can not load jar file:" + jarSourceFile.getAbsolutePath());
                    }
                }
            }
        }
        return list;
    }

    /** 
     * @description: 插件的类加载器
     * @program: bank-credit-sy
     * @author: lixuchun
     * @create: 2024-12-26
     */
    @RequiredArgsConstructor
    private static class Jar {
        /**
         * jar文件
         */
        private final JarFile jarFile;

        /**
         * jar文件对应的目录
         */
        private final File sourceFile;
    }
}

新增一个PluginResourceResolver,获取插件资源目录(/plugins目录下的所有jar内部的git-plugins.def文件)

java 复制代码
package cn.git.agent;

import cn.git.agent.loader.AgentClassLoader;

import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

/** 
 * @description: 插件资源解析器
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-27
 */
public class PluginResourceResolver {

    /**
     * 获取插件资源目录(/plugins目录下的所有jar内部的git-plugins.def文件)
     *
     * @return
     */
    public List<URL> getResources() {
        // 返回参数
        List<URL> cfgUrlPathList = new ArrayList<>();

        try {
            // 获取插件类加载器,需要重写ClassLoader以及getResources方法
            Enumeration<URL> urls = AgentClassLoader.getDefaultClassLoader().getResources("git-plugins.def");

            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                cfgUrlPathList.add(url);
                System.out.println("获取插件资源 [git-plugins.def] 成功, url: " + url);
            }

            return cfgUrlPathList;
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println("获取插件资源 [git-plugins.def] 失败");
        }

        return null;
    }
}

新增两个接收参数类,PluginDefine,PluginCfg 插件配置类,PluginCfg 将每个插件中的git-plugins.def转义为PluginDefine

java 复制代码
package cn.git.agent;

import org.apache.commons.lang3.StringUtils;

/**
 * @description: 插件定义类,git- plugins.def 文件中的对应关系
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-27
 */
public class PluginDefine {

    /**
     * 插件名称 eg: mysql5,springmvc 。。。。。
     */
    private String name;

    /**
     * The class name of plugin defined.
     */
    private String defineClass;

    private PluginDefine(String name, String defineClass) {
        this.name = name;
        this.defineClass = defineClass;
    }

    public static PluginDefine build(String define) {
        if (StringUtils.isEmpty(define)) {
            throw new RuntimeException(define);
        }

        String[] pluginDefine = define.split("=");
        if (pluginDefine.length != 2) {
            throw new RuntimeException(define);
        }

        String pluginName = pluginDefine[0];
        String defineClass = pluginDefine[1];
        return new PluginDefine(pluginName, defineClass);
    }

    public String getDefineClass() {
        return defineClass;
    }

    public String getName() {
        return name;
    }

}

PluginCfg 主要内容如下:

java 复制代码
package cn.git.agent;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

/** 
 * @description: 插件配置文件,转义git-plugins.def文件到PluginDefine实例
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-27
 */
public enum PluginCfg {
    INSTANCE;

    /**
     * 存放所有插件.def文件中,构造出来的 PluginDefine 实例
     */
    private List<PluginDefine> pluginClassList = new ArrayList<PluginDefine>();

    /**
     * 加载插件配置文件,转换为PluginDefine实例
     *
     * @param input
     * @throws IOException
     */
    void load(InputStream input) throws IOException {
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(input));
            String pluginDefine;
            while ((pluginDefine = reader.readLine()) != null) {
                try {
                    if (pluginDefine.trim().length() == 0 || pluginDefine.startsWith("#")) {
                        continue;
                    }
                    PluginDefine plugin = PluginDefine.build(pluginDefine);
                    pluginClassList.add(plugin);
                } catch (Exception e) {
                    System.out.println("load plugin error: " + pluginDefine);
                }
            }
        } finally {
            input.close();
        }
    }

    /**
     * 获取所有插件
     * @return
     */
    public List<PluginDefine> getPluginClassList() {
        return pluginClassList;
    }
}

4.10 拦截器类加载器

我们定义一个类,进行拦截类加载器,自定义类加载器,要想插件拦截器中能够访问到被拦截的类,需要是同一个类加载器或者子类类加载器

eg : classA被拦截,ClassLoaderA加载,那么拦截器也必须是ClassLoaderA加载,否则无法访问到classA,

我们在enhance 对应Interceptor传入classLoader中,对应typeDescription的classLoader,所以我们需要定义其相同类或者其子类的类加载器才可以,具体实现代码如下:

java 复制代码
package cn.git.agent.loader;

/**
 * @description: 用于加载插件当中的拦截器interceptor
 * @program: bank-credit-sy
 * @author: lixuchun
 * @create: 2024-12-27
 */
public class InterceptorInstanceLoader {

    /**
     * 加载拦截器
     * @param interceptorClassName 拦截器全类目
     * @param targetClassLoader 自定义类加载器,要想插件拦截器中能够访问到被拦截的类,需要是同一个类加载器或者子类类加载器
     *                    eg : classA被拦截,ClassLoaderA加载,那么拦截器也必须是ClassLoaderA加载,否则无法访问到classA
     * @param <T> ConstructorMethodAroundInterceptor, MethodAroundInterceptor,
     * @return
     */
    public static <T> T load(String interceptorClassName, ClassLoader targetClassLoader) throws ClassNotFoundException,
            IllegalAccessException, InstantiationException {
        if (targetClassLoader == null) {
            targetClassLoader = InterceptorInstanceLoader.class.getClassLoader();
        }
        AgentClassLoader agentClassLoader = new AgentClassLoader(targetClassLoader);
        Object o = Class.forName(interceptorClassName, true, agentClassLoader).newInstance();
        return (T) o;
    }
}

对应的三个MethodsInterceptor里面的构造方法也需要修改,此处以InstanceMethodsInterceptor为例,修改代码如下:

java 复制代码
/**
 * 构造方法
 *
 * @param interceptorClassName
 * @param classLoader
 */
public InstanceMethodsInterceptor(String interceptorClassName,
                                  ClassLoader classLoader) {
    try {
        aroundInterceptor = InterceptorInstanceLoader.load(interceptorClassName, classLoader);
    } catch (Exception e) {
        e.printStackTrace();
        System.out.println("获取构造方法拦截器失败 ConstructorMethodsInterceptor");
    }
}

5. 简单验证

我们对项目进行编译,然后将编译成功的插件放入到我们预订的plugins目录下

然后我们设置探针部分,在server启动的时候添加探针参数

sh 复制代码
-javaagent:D:\idea_workspace\bytebuddy-demo\git-agent-demo\dist\apm-agent-1.0.SNAPSHOT-jar-with-dependencies.jar

然后启动服务,发现探针增强部分已经进行了加载,具体内容如下:

我们再次访问前端页面,查看我们的增强信息是否打印 http://localhost:8080/userInfo/list/user,发现增强部分已经进行了打印

相关推荐
快乐飒男8 小时前
C语言基础18(GDB调试)
c语言·笔记·学习
jcqcool8 小时前
什么是Sight Words(信号词)
学习
老赵的博客8 小时前
音视频-----RTSP协议 音视频编解码
学习
lxlyhwl8 小时前
【Paper Tips】随记1-word版打印公式
学习·word
honey ball9 小时前
滤波器的主要参数
人工智能·单片机·嵌入式硬件·学习
TripleEyeAlien9 小时前
Swift Combine 学习(七):实践应用场景举例
学习·ios·swift
咬光空气10 小时前
Qt 5.14.2 学习记录 —— 사 信号与槽机制(1)
开发语言·qt·学习
刘婉晴10 小时前
【大模型】7 天 AI 大模型学习
人工智能·学习
半夏知半秋11 小时前
python对mongodb的增删查改
服务器·开发语言·笔记·python·学习·mongodb
小可的科研日常11 小时前
快速增加ppt撤回次数的方法
学习