(目录)
※ 抽象Agent模块 (一条指令挂载所有plugins 和 通用的类与方法的字节码增强框架)
※ 可插拔式插件加载实现要点分析
怎么做到只指定一个-javaagent参数
怎么加载多个plugin?
怎么把typeDescription和要拦截的method对应起来
怎么把typeDescription和要拦截的method的拦截器Interceptor对应起来
接下来我们依次解决这些问题,代码思路借鉴SkyWalking-Agent模块的源码
构建以下目录结构的项目↓↓
实现只指定一个-javaagent参数,实现挂载所有jar
SkyWalking源码plugins目录下有成千上百个插件,都通过-javaagent名命一个一个挂载肯定不现实
怎么去解决这个问题,想一下思路?
只需要一个留 PreMain 入口类就行
怎么加载多个plugin?
最简单的想法:type()中把需要拦截的都写上
public static void premain(String args, Instrumentation instrumentation) {
log.info("进入到premain,args:{}",args);
AgentBuilder builder = new AgentBuilder.Default()
.type(
// springmvc
isAnnotatedWith(named(CONTROLLER_NAME).or(named(REST_CONTROLLER_NAME)))
// mysql
.or(named(CLIENT_PS_NAME).or(named(SERVER_PS_NAME)))
// es
.or(xx))
.transform(new Transformer())
.installOn(instrumentation);
}
怎么把typeDescription和要拦截的method对应起来
我们想一下之前拦截method是不是都是指定对应的规则去进行拦截,那我们可不可以使用 or 把条件联系起来?
eg:如下所示,答案是肯定不行,具体原因的话自己想一下
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
TypeDescription typeDescription,
// 加载 typeDescription这个类的类加载器
ClassLoader classLoader,
JavaModule module) {
log.info("actualName to transform:{}", typeDescription.getActualName());
DynamicType.Builder.MethodDefinition.ReceiverTypeDefinition<?> intercept = builder
.method(
// mysal 拦截方法
named("execute").or(named("executeUpdate")).or(named("executeQuery"))
// mvc 拦截方法
.or(not(isStatic()).and(isAnnotatedWith(
nameStartsWith(MAPPING_PKG_PREFIX).and(nameEndsWith(MAPPING_SUFFIX))
)))
// es 拦截方法
.or(xx)
)
return intercept;
}
所以我们需要想其他办法去抽象出一个组件,形成自定义通用的匹配规则
怎么把typeDescription和要拦截的method的拦截器Interceptor对应起来
同上,我们怎么去选择 我们想要的拦截器:
目前是无法去自定义的选择我们需要的Interceptor的
eg:bytebuddy的Api只能指定一拦截器的实现
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
TypeDescription typeDescription,
// 加载 typeDescription这个类的类加载器
ClassLoader classLoader,
JavaModule module) {
log.info("actualName to transform:{}", typeDescription.getActualName());
DynamicType.Builder.MethodDefinition.ReceiverTypeDefinition<?> intercept = builder
.method(
// mysal 拦截方法
named("execute").or(named("executeUpdate")).or(named("executeQuery"))
// mvc
.or(not(isStatic()).and(isAnnotatedWith(
nameStartsWith(MAPPING_PKG_PREFIX).and(nameEndsWith(MAPPING_SUFFIX))
)))
// es
.or(xx)
)
/*
指定拦截器时 , 需要选择 指定的 Interceptor
// springmvc
new SpringMvcInterceptor()
// mysql
new MysqlInterceptor()
// es
new EsInterceptor()
*/
.intercept(MethodDelegation.to(new MysqlInterceptor()));
return intercept;
}
以上的问题,我们目前是无法依靠现有的api去完成可插拔式插件的实现的
,
只有一个办法了:抽象组件
研究SkyWalking-Agent模块抽取出其中主要实现,来按照下面思路一步一步的实现一下
※ 组件的抽象 ※
通过上面的分析,我们现在的核心问题
:
把 typeDescription 、 method 、lnterceptor 对应起来
点进type()
的源码,查看他的接收参数类型是 ElementMatcher<? super TypeDescription> typeMatcher
点进method()
的源码,查看接收参数:ElementMatcher<? super MethodDescription> matcher
通过上面信息抽象出以下的组件
AbstractClassEnhancePluginDefine
所有插件的顶级父类 , 只要是插件必须直接或间接的集成这个类
ClassMatch
获取当前插件 需要拦截的类
ConstructorMethodsInterceptorPoint
构造方法拦截点
InstanceMethodsInterceptorPoint
实例方法的拦截点
StaticMethodsInterceptorPoint
静态方法拦截点
AbstractClassEnhancePluginDefine 所有插件的顶级父类
具体代码:
AbstractClassEnhancePluginDefine
所有插件的顶级父类
/**
* @Description: 所有插件的顶级父类 , 只要是插件必须直接或间接的集成这个类
* @Author: Perceus
* Date: 2023-09-09
* Time: 16:46
*/
public abstract class AbstractClassEnhancePluginDefine {
/**
* 获取当前插件 需要拦截的类
* @return
*/
protected abstract ClassMatch enhanceClass();
/**
* 获取当前插件,需要拦截的 实例方法 拦截点
* 一个插件可以多个拦截点
* @return
*/
protected abstract InstanceMethodsInterceptorPoint[] getInstanceMethodsInterceptorPoints();
/**
* 获取当前插件,需要拦截的 构造方法 拦截点
* 一个插件可以多个拦截点
* @return
*/
protected abstract ConstructorMethodsInterceptorPoint[] getConstructorMethodsInterceptorPoints();
/**
* 获取当前插件,需要拦截的 静态方法 拦截点
* 一个插件可以多个拦截点
* @return
*/
protected abstract StaticMethodsInterceptorPoint[] getStaticMethodsInterceptorPoints();
}
MethodsInterceptorPoint (构造、静态、实例方法拦截点)
ConstructorMethodsInterceptorPoint
构造方法拦截点
/**
* @Description: 构造方法拦截点
* @Author: Perceus
* Date: 2023-09-09
* Time: 17:30
*/
public interface ConstructorMethodsInterceptorPoint {
/**
* 要拦截哪些方法
* @return 作为method()方法的参数
*/
ElementMatcher<? super MethodDescription> getConstructorMatcher();
/**
* 获取被增强方法对应的拦截器
* @return 拦截器路径
*/
String getConstructorInterceptor();
}
InstanceMethodsInterceptorPoint
实例方法的拦截点*
/**
* @Description: 实例方法的拦截点
* @Author: Perceus
* Date: 2023-09-09
* Time: 17:20
*/
public interface InstanceMethodsInterceptorPoint {
/**
* 要拦截哪些方法
* @return 作为method()方法的参数
*/
ElementMatcher<? super MethodDescription> getMethodsMatcher();
/**
* 获取被增强方法对应的拦截器
* @return 拦截器路径
*/
String getMethodsInterceptor();
}
StaticMethodsInterceptorPoint
静态方法拦截点
/**
* @Description: 静态方法拦截点
* @Author: Perceus
* Date: 2023-09-09
* Time: 17:31
*/
public interface StaticMethodsInterceptorPoint {
/**
* 要拦截哪些方法
* @return 作为method()方法的参数
*/
ElementMatcher<? super MethodDescription> getStaticMatcher();
/**
* 获取被增强方法对应的拦截器
* @return 拦截器路径
*/
String getStaticInterceptor();
}
我们现在重新去写插件,现在我们肯定不能向之前那种写单独的premain去进行命令的挂载了
一、MySQL 插件的抽象
定义 MySQL 插件
, 继承AbstractClassEnhancePluginDefine重写getInstanceMethodsInterceptorPoints()的方法实现
(方法拦截表达式、Interceptor路径)编写 Match规则实现
,构造类的拦截条件
1. MysqlInstrumentation 定义MySQL插件
MysqlInstrumentation类
代码实现:
/**
* @Description: 定义MySQL插件 , 继承AbstractClassEnhancePluginDefine
* @Author: Perceus
* Date: 2023-09-09
* Time: 17:44
*/
public class MysqlInstrumentation extends AbstractClassEnhancePluginDefine {
private static final String CLIENT_PS_NAME = "com.mysql.cj.jdbc.ClientPreparedStatement";
private static final String SERVER_PS_NAME = "com.mysql.cj.jdbc.ServerPreparedStatement";
private static final String INTERCEPTOR = "com.roadjava.skywalking.agent.demoapm.plugins.mysql.interceptor.MysqlInterceptor";
@Override
protected ClassMatch enhanceClass() {
// 构造多个类名的拦截条件
// named(CLIENT_PS_NAME).or(named(SERVER_PS_NAME)) 条件
return MultiClassNameMatch.byMultiClassMatch(CLIENT_PS_NAME, SERVER_PS_NAME);
}
// 这个插件拦截的方法只用 拦截实例方法
@Override
protected InstanceMethodsInterceptorPoint[] getInstanceMethodsInterceptorPoints() {
return new InstanceMethodsInterceptorPoint[]{
new InstanceMethodsInterceptorPoint() {
/**
* 方法拦截表达式
*/
@Override
public ElementMatcher<? super MethodDescription> getMethodsMatcher() {
return named("execute")
.or(named("executeUpdate"))
.or(named("executeQuery"));
}
/**
* Interceptor拦截器路径
* @return
*/
@Override
public String getMethodsInterceptor() {
return INTERCEPTOR;
}
}
};
}
@Override
protected ConstructorMethodsInterceptorPoint[] getConstructorMethodsInterceptorPoints() {
return null;
}
@Override
protected StaticMethodsInterceptorPoint[] getStaticMethodsInterceptorPoints() {
return null;
}
}
2. ClassMatch 表示要匹配类的 最顶层接口(两类:NameMatch、IndirectMatch)
ClassMatch
/**
* @Description: 表示要匹配类的 最顶层接口,有两类
* - NameMatch
* - IndirectMatch
* @Author: Perceus
* Date: 2023-09-09
* Time: 17:16
*/
public interface ClassMatch {
}
NameMatch
:专门用于 类名称=xxx , 仅适用于 named(xxx)
/**
* @Description: 专门用于 类名称=xxx , 仅适用于 named(xxx)
* eg:.type(named(xxx))
* @Author: Perceus
* Date: 2023-09-09
* Time: 18:40
*/
public class NameMatch implements ClassMatch{
}
3. IndirectMatch 所有非NameMatch的情况都要实现 IndirectMatch
/**
* @Description: 所有非NameMatch的情况都要实现IndirectMatch
* @Author: Perceus
* Date: 2023-09-09
* Time: 18:42
*/
public interface IndirectMatch extends ClassMatch{
/**
* 用于构造type()的参数
* 比如构建 -> named(CLIENT_PS_NAME).or(named(SERVER_PS_NAME))
*/
ElementMatcher.Junction<? super TypeDescription> buildJunction();
/**
* 用于判断typeDescription是否满足当前匹配器(IndirectMatch的实现类)的条件
* @param typeDescription 待判断的类
* @return true:匹配 false:不匹配
*/
boolean isMatch(TypeDescription typeDescription);
}
4. MultiClassNameMatch 多个类型条件相等的匹配器
/**
* @Description: 多个类型条件相等的匹配器
* @Author: Perceus
* Date: 2023-09-09
* Time: 18:44
*/
public class MultiClassNameMatch implements IndirectMatch {
/**
* 要匹配的类名称
*/
private List<String> needMatchClassNames;
private MultiClassNameMatch(String[] classNames) {
if (classNames == null || classNames.length == 0) {
throw new IllegalArgumentException("needMatchClassNames can not be null");
}
this.needMatchClassNames = Arrays.asList(classNames);
}
/**
* 多个类在这里 要求是 or的关系
* @return
*/
@Override
public ElementMatcher.Junction<? super TypeDescription> buildJunction() {
ElementMatcher.Junction<? super TypeDescription> junction = null;
// 构造类拦截条件
for(String needMatchClassName:needMatchClassNames){
if (junction == null) {
junction = named(needMatchClassName);
}else {
junction = junction.or(named(needMatchClassName));
}
}
return null;
}
public static IndirectMatch byMultiClassMatch(String... classNames) {
return new MultiClassNameMatch(classNames);
}
}
二、MVC 插件的抽象
步骤还是类似上面,只不过细节有一些不一样
1. ClassAnnotationNameMatch 某个类要同时含有某几个注解匹配器
我现在所写的Match规则:
是某个类同时含有某几个注解的匹配器,关系是and
代码如下:
/**
* @Description: 某个类要同时含有某几个注解匹配器
* @Author: Perceus
* Date: 2023-09-09
* Time: 19:10
*/
public class ClassAnnotationNameMatch implements IndirectMatch {
/**
* 要包含的注解列表
*/
private List<String> annotations;
private ClassAnnotationNameMatch(String[] annotations) {
if (annotations == null || annotations.length == 0) {
throw new IllegalArgumentException("annotations can not be null");
}
this.annotations = Arrays.asList(annotations);
}
/**
* 多个注解要求是and的关系
* @return
*/
@Override
public ElementMatcher.Junction<? super TypeDescription> buildJunction() {
ElementMatcher.Junction<? super TypeDescription> junction = null;
for (String anno : annotations) {
if (junction == null) {
junction = buildEachAnno(anno);
} else {
junction = junction.and(buildEachAnno(anno));
}
}
return junction;
}
private ElementMatcher.Junction<TypeDescription> buildEachAnno(String anno) {
return isAnnotatedWith(named(anno));
}
public static IndirectMatch byClassAnnotationMatch(String... annotations) {
return new ClassAnnotationNameMatch(annotations);
}
}
我所需要的是:
MVC拦截的类包含 @Controller 或者 @RestController 注解的类,关系是or
所以像Mysql那种一次性调用就不行,代码如下:
@Override
protected ClassMatch enhanceClass() {
// 构造 isAnnotatedWith(named(CONTROLLER_NAME).or(named(REST_CONTROLLER_NAME)))条件
// 但是 ClassAnnotationNameMatch中的条件是and , 所以不能这么构建
// 把这个类抽象成抽象类 , 让子类 去实现这个逻辑
return ClassAnnotationNameMatch.byClassAnnotationMatch(CONTROLLER_NAME, REST_CONTROLLER_NAME);
}
2. SpringmvcCommonInstrumentation抽象类 (SpringMVC插件公共类)
现在抽象出一个SpringmvcCommonInstrumentation抽象类
,enhanceClass()方法逻辑由子类构造
/**
* @Description: SpringMVC插件公共类 , 继承AbstractClassEnhancePluginDefine
* @Author: Perceus
* Date: 2023-09-09
* Time: 17:44
*/
public abstract class SpringmvcCommonInstrumentation extends AbstractClassEnhancePluginDefine {
private static final String INTERCEPTOR = "com.roadjava.skywalking.agent.demo.apm.plugins.springmvc.interceptor.SpringmvcInterceptor";
private static final String MAPPING_PKG_PREFIX = "org.springframework.web.bind.annotation";
private static final String MAPPING_SUFFIX = "Mapping";
/*
@Override
protected ClassMatch enhanceClass() {
// 构造 isAnnotatedWith(named(CONTROLLER_NAME).or(named(REST_CONTROLLER_NAME)))条件
// 但是 ClassAnnotationNameMatch中的条件是and , 所以不能这么构建
// 把这个类抽象成抽象类 , 让子类 去实现这个逻辑
return ClassAnnotationNameMatch.byClassAnnotationMatch(CONTROLLER_NAME, REST_CONTROLLER_NAME);
}
*/
// 这个插件拦截的方法只用 拦截实例方法
@Override
protected InstanceMethodsInterceptorPoint[] getInstanceMethodsInterceptorPoints() {
return new InstanceMethodsInterceptorPoint[]{
new InstanceMethodsInterceptorPoint() {
/**
* 方法拦截表达式
* not(isStatic()).and(isAnnotatedWith(
* nameStartsWith(MAPPING_PKG_PREFIX).and(nameEndsWith(MAPPING_SUFFIX))
* ))
*/
@Override
public ElementMatcher<? super MethodDescription> getMethodsMatcher() {
return not(isStatic()).and(isAnnotatedWith(
nameStartsWith(MAPPING_PKG_PREFIX).and(nameEndsWith(MAPPING_SUFFIX))
));
}
/**
* Interceptor拦截器路径
* @return
*/
@Override
public String getMethodsInterceptor() {
return INTERCEPTOR;
}
}
};
}
@Override
protected ConstructorMethodsInterceptorPoint[] getConstructorMethodsInterceptorPoints() {
return new ConstructorMethodsInterceptorPoint[0];
}
@Override
protected StaticMethodsInterceptorPoint[] getStaticMethodsInterceptorPoints() {
return new StaticMethodsInterceptorPoint[0];
}
}
3. ControllerInstrumentation 抽象类实现 拦截@Controller注解
拦截带有**@Controller注解的SpringMVC**插件 ,
继承SpringmvcCommonInstrumentation
/**
* @Description: 拦截带有@Controller注解的SpringMVC插件 , 继承SpringmvcCommonInstrumentation
* @Author: Perceus
* Date: 2023-09-09
* Time: 17:44
*/
public class ControllerInstrumentation extends SpringmvcCommonInstrumentation {
// @Controller注解的路径
private static final String CONTROLLER_NAME = "org.springframework.stereotype.Controller";
@Override
protected ClassMatch enhanceClass() {
// 构造 isAnnotatedWith((named(CONTROLLER_NAME))条件
return ClassAnnotationNameMatch.byClassAnnotationMatch(CONTROLLER_NAME);
}
}
4. RestControllerInstrumentation 抽象类实现 拦截带有**@RestController**注解
拦截带有@RestController注解的SpringMVC插件 ,
继承SpringmvcCommonInstrumentation
/**
* @Description: 拦截带有@RestController注解的SpringMVC插件 , 继承SpringmvcCommonInstrumentation
* @Author: Perceus
* Date: 2023-09-09
* Time: 17:44
*/
public class RestControllerInstrumentation extends SpringmvcCommonInstrumentation {
// @RestController注解的路径
private static final String REST_CONTROLLER_NAME = "org.springframework.web.bind.annotation.RestController";
@Override
protected ClassMatch enhanceClass() {
// 构造 isAnnotatedWith((named(REST_CONTROLLER_NAME))条件
return ClassAnnotationNameMatch.byClassAnnotationMatch(REST_CONTROLLER_NAME);
}
}
三、maven-antrun-plugin 创建指定目录并拷贝jar包到指定目录
apm-agent包下pom中引入maven插件
<!-- 创建指定目录并拷贝jar包到指定目录 -->
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.8</version>
<executions>
<!-- 在clean阶段删除dist目录 -->
<execution>
<id>clean</id>
<phase>clean</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<delete dir="${project.basedir}/../../dist" />
</target>
</configuration>
</execution>
<!-- 在package阶段创建/dist、/dist/plugins、拷贝agent.jar到/dist下 -->
<execution>
<id>package</id>
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<target>
<mkdir dir="${project.basedir}/../../dist" />
<copy file="${project.build.directory}/apm-agent-1.0-SNAPSHOT-jar-with-dependencies.jar" tofile="${project.basedir}/../../dist/apm-agent-1.0-SNAPSHOT-jar-with-dependencies.jar" overwrite="true" />
<mkdir dir="${project.basedir}/../../dist/plugins" />
</target>
</configuration>
</execution>
</executions>
</plugin>
运行打包命令,根目录下生成一个dist文件夹,里面拷贝了一个jar包和创建了一个plugins文件夹