手写SkyWalking-Agent模块(初始化、MySQL、MVC插件编写)
使用 ByteBuddy类库 和 JavaAgent机制 实现 类似 SkyWalking-Agent 模式下的无侵入式的动态可插拔功能
准备工作
新建一个springboot程序,带有一个查询数据库的接口
由于程序简单,这里只贴出一些需要的代码
@RestController
@RequestMapping("/userInfo")
public class UserInfoController {
@Resource
private UserInfoService userInfoService;
@GetMapping("/list")
public List<UserInfoDO> selectUserList(String userName) {
return userInfoService.selectUserList(userName);
}
}
SQL
create database app CHARACTER SET utf8mb4;
use app;
drop table if exists user_info;
CREATE TABLE `user_info` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(50) NOT NULL COMMENT '用户名',
`pwd` varchar(50) NOT NULL COMMENT '密码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 comment '管理员表';
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');
运行程序访问接口成功,并打印日志:
Agent 模块初始状态
一、SpringMVC-agent编写(实现接口访问耗时统计)
编写流程梳理:
这里用bytebuddy
作了如下处理
1.
type()
监听使用@Controller或@RestController的类2.注册自定义转换器
Transformer
对监听的类进行处理3.在自定义转换器中对使用了@org.springframework.web.bind.annotation.xxxMaping注解的共有方法进行拦截,交给类
SpringControllerInterceptor
进行处理4.将转换器
装载到instrumentation上
1. pom依赖 及 打包插件
<dependencies>
<!--byte-buddy-->
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.12.10</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.29</version>
</dependency>
</dependencies>
<!--Agent 打包插件-->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.5.5</version>
<configuration>
<archive>
<manifestEntries>
<!--Premain 入口类地址-->
<Premain-Class>com.roadjava.skywalking.agent.demo.standalone.plugins.springmvc.AgentDemo</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
</manifestEntries>
</archive>
<descriptorRefs>
<!--jar包的名称-->
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>one_jar</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
这个pom.xml中的plugin配置,因为要打包,这里我用assembly直接maven打包,命令:
mvn clean package
这样可以直接打包并加入引用的jar,自动生成配置好的MF文件,比较方便
2. permain 入口类
新建agent程序,定义程序入口的premain方法,在MF文件中定义premain方法
public static void premain(String agentArgs, Instrumentation inst) {}
这个方法相当于普通Java程序的入口方法main,agentArgs相当于main方法里的 String[] args,但是不是数组状态,而是命令行怎么输入的,这里就怎么传递,需要手动处理,inst,就是刚刚说到的Instrumentation类,其由sun或openjdk来对其进行实现,所以具体实现可以不用管,只管应用接口即可,当然有兴趣的也可以打开源码了解一下
代码:
@Slf4j
public class AgentDemo {
// @Controller注解的路径
private static final String CONTROLLER_NAME = "org.springframework.stereotype.Controller";
// @RestController注解的路径
private static final String REST_CONTROLLER_NAME = "org.springframework.web.bind.annotation.RestController";
public static void premain(String args, Instrumentation instrumentation){
log.info("进入到premain,args:{}",args);
// 构建 AgentBuilder
AgentBuilder builder = new AgentBuilder.Default()
// type() 需要拦截的类 的规则
.type(isAnnotatedWith(named(CONTROLLER_NAME).or(named(REST_CONTROLLER_NAME))))
// 拦截完后 执行的转换器
.transform(new AgentTransformer());
builder.installOn(instrumentation);
}
}
3. MVCAgentTransformer 转化器编写
实现 AgentBuilder.Transformer 接口,重写transform()方法
编写 builder.method() 拦截方法的规则逻辑
拦截成功后,传递给mvc的拦截器进行增强
代码:
@Slf4j
public class AgentTransformer implements AgentBuilder.Transformer{
private static final String MAPPING_PKG_PREFIX = "org.springframework.web.bind.annotation";
private static final String MAPPING_SUFFIX = "Mapping";
/**
* 当被type(ELementMatcher<.?super TypeDescription>typeMatcher)匹配后会进入到此方法
* @param typeDescription 表示要被加载的类的信息
* @return
*/
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader, // 加载 typeDescription这个类的类加载器
JavaModule javaModule) {
log.info("actualName to transform:{}",typeDescription.getActualName());
// 拦截方法
// 规则: 不是静态方法 并且 被 @xxxMapping注解修饰的方法
return builder.method(
not(isStatic()).and(isAnnotatedWith(
nameStartsWith(MAPPING_PKG_PREFIX).and(nameEndsWith(MAPPING_SUFFIX))
)))
// 拦截到方法后,传递给 SpringMvcInterceptor拦截器 做 逻辑增强
.intercept(MethodDelegation.to(new SpringMvcInterceptor()));
}
}
4. McvInterceptor拦截器 字节码方法增强逻辑
在 SpringControllerInterceptor中,定义如上的intercept方法,加入@RuntimeType
注解,就可以进行切片操作了
注解解释:
@This
Object targetObj,增强的目标类
@Origin
Method method,目标方法对象
@AllArguments
Object[] args,用于注入目标方法执行时的参数列表,
@SuperCall
Callable<?> callable,个人理解为目标方法执行所在线程或者其控制器,使用callable.call()方式触发目标方法执行。这里把拦截的方法的方法名、参数列表、返回值、异常信息及执行耗时打印出来
代码:
@Slf4j
public class SpringMvcInterceptor {
@RuntimeType
public Object interceptor(
@This Object targetObj, // 增强的目标类
@Origin Method targetMethod, // 目标方法信息
@AllArguments Object[] targetMethodArgs, // 方法信息
@SuperCall Callable<?> zuper // 执行方法调用
) {
log.info("before controller exec,methodName:{},args:{}", targetMethod.getName(), Arrays.toString(targetMethodArgs));
long start = System.currentTimeMillis();
Object call = null;
try {
call = zuper.call();
log.info("after controller exec,result:{}", call);
} catch (Exception e) {
log.error("controller exec error", e);
} finally {
long end = System.currentTimeMillis();
log.info("finally controller exec in {} ms", end - start);
}
return call;
}
}
常用注解含义
-
@RuntimeType 注解:
告诉 Byte Buddy 不要进行严格的参数类型检测,在参数匹配失败时,尝试使用类型转换方式(runtime type casting)进行类型转换,匹配相应方法。
-
@This 注解:
注入被拦截的目标对象。
-
@AllArguments 注解:
注入目标方法的全部参数,是不是感觉与 Java 反射的那套 API 有点类似了?
-
@Origin 注解:
注入目标方法对应的 Method 对象。如果拦截的是字段的话,该注解应该标注到 Field 类型参数。
-
@Super 注解:
注入目标对象。通过该对象可以调用目标对象的所有方法。
-
@SuperCall:
这个注解比较特殊,我们要在 intercept() 方法中调用目标方法的话,需要通过这种方式注入,
@SuperCall与 Spring AOP 中的 ProceedingJoinPoint.proceed() 方法有点类似,需要注意的是,这里不能修改调用参数,从上面的示例的调用也能看出来,参数不用单独传递,都包含在其中了。
另外,@SuperCall 注解还可以修饰 Runnable 类型的参数,只不过目标方法的返回值就拿不到了。
5. package 打包
注意修改 pom 文件中 <Premain-Class> 路径
将插件打包
选择这个带依赖的Jar包,进行Agent的加载
拷贝Jar包的绝对路径:
D:\Agent-SkyWalking\standlone-plugins\springmvc-standalone-plugin\target\springmvc-standalone-plugin-1.0-SNAPSHOT-jar-with-dependencies.jar
6. 修改启动VM参数 -javaagent:Jar包路径,启动程序
-javaagent:D:\Agent-SkyWalking\standlone-plugins\springmvc-standalone-plugin\target\springmvc-standalone-plugin-1.0-SNAPSHOT-jar-with-dependencies.jar
7. 启动成功,字节码植入,通过Arthas反编译
启动成功截图:
Arthas 反编译
查看代码
二、MySQL -agent (统计DDL的耗时)
和上面步骤相似,不做过多书写,只放核心逻辑
1. premain 入口类
阅读mysql.jdbc的源码,找到我们需要拦截的 运行DDL语句的类
- com.mysql.cj.jdbc.ClientPreparedStatement
- com.mysql.cj.jdbc.ServerPreparedStatement
@Slf4j
public class AgentDemo {
private static final String CLIENT_PS_NAME = "com.mysql.cj.jdbc.ClientPreparedStatement";
private static final String SERVER_PS_NAME = "com.mysql.cj.jdbc.ServerPreparedStatement";
public static void premain(String args, Instrumentation instrumentation) {
log.info("进入到premain,args:{}",args);
AgentBuilder builder = new AgentBuilder.Default()
.type(named(CLIENT_PS_NAME).or(named(SERVER_PS_NAME)))
.transform(new AgentTransfAormer());
builder.installOn(instrumentation);
}
}
2. MySQLAgentTransfAormer 编写
拦截 方法名是 execute,executeUpdate,executeQuery 的实现
@Slf4j
public class AgentTransformer implements AgentBuilder.Transformer {
/**
* @param typeDescription 表示要被加载的类的信息
*/
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
TypeDescription typeDescription,
ClassLoader classLoader, // 加载 typeDescription这个类的类加载器
JavaModule module) {
log.info("actualName to transform:{}", typeDescription.getActualName());
DynamicType.Builder.MethodDefinition.ReceiverTypeDefinition<?> intercept = builder
.method(
named("execute")
.or(named("executeUpdate"))
.or(named("executeQuery"))
).intercept(MethodDelegation.to(new MysqlInterceptor()));
return intercept;
}
}
3. MysqlLnterceptor 拦截器 字节码方法增强
统计 运行耗时,打印在控制台
@Slf4j
public class MysqlInterceptor {
@RuntimeType
public Object intercept(
@This Object targetObj,
@Origin Method targetMethod,
@AllArguments Object[] targetMethodArgs,
@SuperCall Callable<?> zuper
) {
log.info("before mysql exec,methodName:{},args:{}",targetMethod.getName(),Arrays.toString(targetMethodArgs));
long start = System.currentTimeMillis();
Object call = null;
try {
call = zuper.call();
log.info("after mysql exec,result:{}",call);
}catch (Exception e) {
log.error("mysql exec error",e);
}finally {
long end = System.currentTimeMillis();
log.info("finally mysql exec in {} ms",end - start);
}
return call;
}
}
4. 启动成功,运行结果
打包,-javaagent:路径运行
目前指令需要挂载多个agent,接下来需要去优化,类似SkyWalking只需要一个javaagent:就可以挂载需要的plugin
-javaagent:D:\Agent-SkyWalking\standlone-plugins\springmvc-standalone-plugin\target\springmvc-standalone-plugin-1.0-SNAPSHOT-jar-with-dependencies.jar -javaagent:D:\Agent-SkyWalking\standlone-plugins\mysql-standalone-plugin\target\mysql-standalone-plugin-1.0-SNAPSHOT-jar-with-dependencies.jar*
总结
java agent主要用于监控,所以一般来说,正常的写业务的人员很少用到,而jvm中自带的工具,如jmap,jstat,jstack等都是用javaagent方式监控,抓取jvm信息以供分析,但是我们也可以了解一下相关的内容,用于自己业务上定制的监控,如针对某些特定接口的性能监控,参数记录等,可以方便平时开发或者线上的问题排查,而使用bytebuddy则让我们更高效的处理字节码,相比于asm,javaassist,bytebuddy更强大高效