简单的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
javapackage 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
javapackage 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: 界面
大致流程即:
- agent(sniff)探针在服务端程序中执行SkyWalking的插件的拦截器逻辑,完成监测数据获取
- agent(sniff)探针通过gRPC(默认)等形式将获取的监测数据传输到OAP系统
- OAP系统将数据进行处理/整合后,存储到Storage中(ES, MySQL, H2等)
- 用户可通过UI界面快捷查询数据,了解请求调用链路,调用耗时等信息
3. SkyWalking Agent
深入理解 Skywalking Agent - 简书 (jianshu.com) <= 推荐先阅读,很详细
-
基础介绍
SkyWalking Agent,是SkyWalking中的组件之一(
skywalking-agent.jar
),在Java中通过Java Agent实现。 -
使用简述
- 通过
java -javaagent:skywalking-agent.jar包的绝对路径 -jar 目标服务jar包绝对路径
指令,在目标服务main方法执行前,会先执行skywalking-agent.jar
的org.apache.skywalking.apm.agent.SkyWalkingAgent#premain
方法 - premian方法执行时,加载与
skywalking-agent.jar
同层级文件目录的./activations
和./plusgins
目录内的所有插件jar包,根据jar文件内的skywalking-plugin.def
配置文件作相关解析工作 - 解析工作完成后,将所有插件实现的拦截器逻辑,通过JVM工具类
Instrumentation
提供的redefine/retransform能力,修改目标类的.class
内容,将拦截器内指定的增强逻辑附加到被拦截的类的原方法实现中 - 目标服务的main方法执行,此时被拦截的多个类内部方法逻辑已经被增强,比如某个方法执行前后额外通过gRPC将方法耗时记录并发送到OAP。简单理解的话,类似Spring中常用的AOP切面技术,这里相当于字节码层面完成切面增强
- 通过
-
实现思考
Byte Buddy可以指定一个拦截器对指定拦截的类中指定的方法进行增强。SkyWalking Java Agent使用ByteBuddy实现,从0到1实现时,避不开以下几个问题需要考虑:
- 每个插件都需要有各自的拦截器逻辑,如何让Byte Buddy使用我们指定的多个拦截器
- 多个拦截器怎么区分各自需要拦截的类和方法
- 如何在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探针入口
javapackage 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方法
javapackage 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类,具体逻辑实现
javapackage 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; } }
-
监听器类
javapackage 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 实现要点分析
-
如何实现单一java agent入口类
仅在
apm-agent
模块提供一个premain
方法入口,其他插件未来都在apm-plugins
内实现 -
如何整合多个插件需要拦截的类范围
AgentBuilder#type
方法中,通过.or(Xxx)
链接多个插件需要拦截的类范围 -
如何整合多个插件需要增强的方法范围
DynamicType.Builder#method
方法中,通过.or(Xxx)
链接多个插件需要增强的方法范围 -
如何整个多个插件各自增强的方法对应的拦截器
DynamicType.Builder.MethodDefinition.ImplementationDefinition#intercept
可以通过andThen
指定多个拦截器 -
如何让被拦截的类中增强的方法走正确的拦截器逻辑?
拦截器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
javapackage 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
javapackage 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
javapackage 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
javapackage 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
javapackage 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的具体实现类,参考如下,多个注解拦截
javapackage 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); } }
多个类名相等情况匹配
javapackage 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方法,在前后以及异常进行增,三种方法的方法增强类代码如下:
-
静态方法拦截类
javapackage 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; } }
-
实例方法拦截类
javapackage 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; } }
-
构造方法拦截类
javapackage 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,其中切面类里面有前置方法,后置方法以及异常执行方法,可以在增强方法中进行执行,那么我们具体实现代码如下:
-
静态方法环绕执行拦截器
javapackage 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); }
-
实例方法环绕拦截器
javapackage 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); }
-
构造方法实例拦截器
javapackage 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环绕增强方法
javapackage 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的环绕增强方法
javapackage 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,发现增强部分已经进行了打印