Appearance
第10讲:深入剖析Agent插件原理,无侵入性埋点
在前面介绍 SkyWalking Agent 启动流程时,同时介绍了插件中 skywalking-agent.def 文件的查找、解析流程,AbstractClassEnhancePluginDefine 抽象类的核心定义,以及插件类与 AgentBuilder 配合为目标类动态添加埋点功能的核心流程。本课时将深入介绍 AbstractClassEnhancePluginDefine 抽象类以及其子类的运行原理。
AbstractClassEnhancePluginDefine 核心实现
在开始之前,先简单回顾上一课时中关于 AbstractClassEnhancePluginDefine 的一个核心知识点:AbstractClassEnhancePluginDefine 是所有插件的父类,SkywalkingAgent.Transformer 会通过其 enhanceClass() 方法返回的 ClassMatch 对象,匹配到要增强的目标类。在不同的插件实现类中,enhanceClass() 方法返回的 ClassMatch 对象不同,例如:
- Dubbo 插件拦截的是 com.alibaba.dubbo.monitor.support.MonitorFilter 这个类;
- Tomcat 插件拦截的是 org.apache.catalina.core.StandardHostValve 这个类。
后面会详细介绍上述两个插件的具体实现。
完成目标类和插件类的匹配之后,会进入 define() 方法,其核心逻辑如下:
- 通过 witnessClass() 方法确定当前插件与当前拦截到的目标类的版本是否匹配。若版本不匹配,则 define() 方法直接结束,当前插件类不会增强该类;若版本匹配,则继续后续逻辑。
- 进入 enhance() 方法执行增强逻辑。
- 设置插件增强标识。
witnessClass() 方法
很多开源组件和工具类库的功能会不断增加,架构也会随之重构,导致不同版本的兼容性得不到很好的保证。例如,MySQL 常用的版本有 5.6、5.7、8.0 多个版本,在使用 JDBC 连接 MySQL 时使用的 mysql-connector-java.jar 包也分为 5.x、6.x、8.x 等版本,对应的 JDBC 协议的版本也各不相同。
SkyWalking Agent 提供的 MySQL 插件本质上是增强 mysql-connector-java.jar 中的关键方法,例如 ConnectionImpl.getInstance() 方法,但在 mysql-connector-java.jar 的 5.x 版本和 8.x 版本中,ConnectionImpl 的包名不同,如下所示:
这仅仅是一个简单的示例,在有的开源组件或类库中,不同版本中同名类的功能和结构已经发生了翻天覆地的变化。要通过一个 SkyWalking Agent 插件完成对一个开源组件所有版本的增强,是非常难实现的,即使勉强能够实现,该插件的实现也会变的非常臃肿,扩展性也会成问题。
SkyWalking 怎么解决这个问题呢?回到 MySQL 示例,SkyWalking 为每个版本的 mysql-connector-java.jar 提供了不同版本的插件,这些插件的 witnessClass() 方法返回值不同,具体返回的是对应版本 mysql-connector-java.jar 所特有的一个类,如下表所示:
若当前类加载器无法扫描到插件 witnessClass() 方法指定的类,表示当前插件版本不合适,即使拦截到了目标类,也不能进行增强。AbstractClassEnhancePluginDefine.define() 方法中的相关片段如下:
java
String[] witnessClasses = witnessClasses();
if (witnessClasses != null) {
for (String witnessClass : witnessClasses) {
// 判断指定类加载器中是否存在witnessClasses()指定的类
if (!WitnessClassFinder.INSTANCE.exist(witnessClass,
classLoader)) {
return null; // 若不存在则表示版本不匹配,直接返回
}
}
}
增强 static 静态方法
完成上述插件版本的匹配之后,开始进入 enhance() 方法对目标类进行增强。如下图所示, ClassEnhancePluginDefine 继承了 AbstractClassEnhancePluginDefine 抽象类:
在 ClassEnhancePluginDefine 实现的 enhance() 方法中,会分别完成对 static 静态方法以及实例方法的增强:
java
protected DynamicType.Builder<?> enhance(...) throws PluginException {
// 增强static方法
newClassBuilder = this.enhanceClass(typeDescription,
newClassBuilder, classLoader);
// 增强构造方法和实例方法
newClassBuilder = this.enhanceInstance(typeDescription,
newClassBuilder, classLoader, context);
return newClassBuilder;
}
在增强静态方法时会使用到 StaticMethodsInterceptPoint 这个接口,它描述了当前插件要拦截目标类的哪些 static 静态方法,以及委托给哪个类去增强,其定义如下:
java
public interface StaticMethodsInterceptPoint {
// 用于匹配目标静态方法
ElementMatcher<MethodDescription> getMethodsMatcher();
// 拦截到的静态方法交给哪个Interceptor来增强
String getMethodsInterceptor();
// 增强过程中是否需要修改参数
boolean isOverrideArgs();
}
这里以 mysql-8.x-plugin 插件中的实现为例进行说明,其中ConnectionImplCreateInstrumentation 这个插件类的 enhanceClass() 方法如下:
java
protected ClassMatch enhanceClass() { // 拦截目标类为ConnectionImpl
return byName("com.mysql.cj.jdbc.ConnectionImpl");
}
其 getStaticMethodsInterceptPoints() 方法返回的下面这个 StaticMethodsInterceptPoint 实现(StaticMethodsInterceptPoint 接口的实现基本都是这种匿名内部类):
java
new StaticMethodsInterceptPoint[] {
new StaticMethodsInterceptPoint() {
@Override
public ElementMatcher<MethodDescription> getMethodsMatcher() {
return named("getInstance"); // 增强 getInstance()方法
}
@Override
public String getMethodsInterceptor() {
// 委托给 ConnectionCreateInterceptor进行增强
return "org.apache.skywalking.apm.plugin.jdbc
.mysql.v8.ConnectionCreateInterceptor";
}
@Override
public boolean isOverrideArgs() {
return false; // 增强过程中无需修改方法参数
}
}
}
也就是说,ConnectionImplCreateInstrumentation 这个插件拦截的是 com.mysql.jdbc.ConnectionImpl.getInstance() 这个静态方法。
接下来回到 ClassEnhancePluginDefine.enhanceClass() 方法的具体实现:
java
private DynamicType.Builder<?> enhanceClass(TypeDescription typeDescription,
DynamicType.Builder<?> newClassBuilder, ClassLoader classLoader) throws PluginException {
// 获取当前插件的静态方法拦截点,如果该插件不增强静态方法,则该数组为空
StaticMethodsInterceptPoint[] staticMethodsInterceptPoints =
getStaticMethodsInterceptPoints();
String enhanceOriginClassName = typeDescription.getTypeName();
for (StaticMethodsInterceptPoint staticMethodsInterceptPoint :
staticMethodsInterceptPoints) {
// 进行具体增强的Interceptor名称
String interceptor = staticMethodsInterceptPoint
.getMethodsInterceptor();
// 在增强过程中,是否要修改参数。
if (staticMethodsInterceptPoint.isOverrideArgs()) {
// 前面介绍了 Byte Buddy 用法,这里也是一样的,通过method()方法
// 指定拦截方法的条件
newClassBuilder = newClassBuilder.method(isStatic()
.and(staticMethodsInterceptPoint.getMethodsMatcher()))
.intercept(
MethodDelegation.withDefaultConfiguration()
.withBinders( // 要用Morph注解,需要先绑定
Morph.Binder.install(OverrideCallable.class)
// StaticMethodsInterWithOverrideArgs后面展开说
).to(new StaticMethodsInterWithOverrideArgs(interceptor))
);
} else { // 下面是不需要修改参数的增强
newClassBuilder = newClassBuilder.method(isStatic()
.and(staticMethodsInterceptPoint.getMethodsMatcher()))
.intercept(MethodDelegation.withDefaultConfiguration()
.to(new StaticMethodsInter(interceptor))
);
}
}
return newClassBuilder;
}
根据前文对 Byte Buddy API 的介绍,通过 method() 方法拦截到静态方法之后,如果需要修改方法参数,则会通过 StaticMethodsInterWithOverrideArgs 对象进行增强,其中的 intercept() 方法是其核心实现:
java
@RuntimeType
public Object intercept(@Origin Class<?> clazz,
@AllArguments Object[] allArguments, @Origin Method method,
@Morph OverrideCallable zuper) throws Throwable {
// 加载插件指定的StaticMethodsAroundInterceptor
StaticMethodsAroundInterceptor interceptor =
InterceptorInstanceLoader
.load(staticMethodsAroundInterceptorClassName,
clazz.getClassLoader());
MethodInterceptResult result = new MethodInterceptResult();
// 调用 interceptor.before()做前置处理
interceptor.beforeMethod(clazz, method, allArguments,
method.getParameterTypes(), result);
Object ret = null;
try {
// 根据before()的处理结果判定是否调用目标方法
if (!result.isContinue()) {
ret = result._ret();
} else {
// 注意:这里是需要传参的,这些参数我们是可以在before()方法中改动
// 的,这就是OverrideArgs的意义
ret = zuper.call(allArguments);
}
} catch (Throwable t) {
// 如果出现异常,会先通知interceptor中的
// handleMethodException()方法进行处理
interceptor.handleMethodException(clazz, method, allArguments,
method.getParameterTypes(), t);
throw t;
} finally { // 通过after()方法进行后置处理
ret = interceptor.afterMethod(clazz, method, allArguments,
method.getParameterTypes(), ret);
}
return ret;
}
如果不需要修改方法参数,则会通过 StaticMethodsInter 对象进行增强,其实现与 StaticMethodsInterWithOverrideArgs 类似,唯一区别在于调用目标方法时无法修改参数。
上面使用的 StaticMethodsAroundInterceptor 是个接口,其中定义了如下三个方法:
- before():在目标方法之前调用。
- after():在目标方法之后调用。
- handleMethodException():在目标方法抛出异常时调用。
通过实现 StaticMethodsAroundInterceptor 接口,各个 Agent 插件就可以在静态方法前后添加自定义的逻辑了。
前面提到的 mysql-8.x-plugin 中的 ConnectionImplCreateInstrumentation 自然也实现了该接口。通过对 StaticMethodsInterWithOverrideArgs 以及 StaticMethodsAroundInterceptor 接口的介绍,我们会发现 Agent 插件对静态方法的增强逻辑与 Spring AOP 中环绕通知的逻辑非常类似。
设计模式 TIP
ClassEnhancePluginDefine 是个典型的模板方法模式的使用场景,其 enhanceClass() 方法只实现了增强静态方法的基本流程,真正的增强逻辑全部通过 getStaticMethodsInterceptPoints() 抽象方法推迟到子类实现。在后面增强对象的构造方法和实例方法时,同样会看到类似的实现。
增强实例对象
分析完增强 static 静态方法的相关逻辑之后,我们继续分析增强一个 Java 实例对象的相关逻辑 ------ 入口是 enhanceInstance() 方法。enhanceInstance() 方法将分成三个部分来分析其实现:
- 实现 EnhancedInstance 接口
- 增强构造方法
- 增强实例方法
实现 EnhancedInstance 接口
enhanceInstance() 方法首先会为目标类添加了一个字段,同时会让目标类实现 EnhancedInstance 接口,具体实现如下:
java
// EnhanceContext记录了整个增强过程中的上下文信息,里面就两个boolean值
if (!context.isObjectExtended()) {
newClassBuilder = newClassBuilder
// 定义一个字段private volatile的字段,该字段为Object类型
.defineField("_$EnhancedClassField_ws", Object.class,
ACC_PRIVATE | ACC_VOLATILE)
// 实现EnhancedInstance接口的方式是读写新
// 增的"_$EnhancedClassField_ws"字段
.implement(EnhancedInstance.class)
.intercept(FieldAccessor.ofField(CONTEXT_ATTR_NAME));
context.extendObjectCompleted(); // 标记一下上线文信息
}
EnhancedInstance 接口中定义了 getSkyWalkingDynamicField() 和setSkyWalkingDynamicField() 两个方法,分别读写新增的 _$EnhancedClassField_ws 字段。以前文 demo-webapp 示例中的 HelloWorldController 这个类为例,在 skywalking-agent/debugging/ 目录下可以看到增强后的类如下:
java
// 实现了EnhancedInstance接口
public class HelloWorldController implements EnhancedInstance {
private volatile Object _$EnhancedClassField_ws; // 新增字段
// 对EnhancedInstance的实现
public Object getSkyWalkingDynamicField() {
return this._$EnhancedClassField_ws;
}
public void setSkyWalkingDynamicField(Object var1) {
this._$EnhancedClassField_ws = var1;
}
... ... // 省略其他业务逻辑相关的方法
}
增强构造方法
接下来,ehanceInstance() 方法会增强实例对象的构造方法,具体流程与增强 static 静态方法的流程类似,唯一区别是这里使用的是 ConstructorInterceptPoint,相关代码片段如下:
java
ConstructorInterceptPoint[] constructorInterceptPoints =
getConstructorsInterceptPoints();
for (ConstructorInterceptPoint constructorInterceptPoint :
constructorInterceptPoints) {
newClassBuilder = newClassBuilder.constructor(
constructorInterceptPoint.getConstructorMatcher())
// 这里对 SuperMethodCall的使用方式和介绍 Byte Buddy基础时说的一毛一样
.intercept(SuperMethodCall.INSTANCE
.andThen(MethodDelegation.withDefaultConfiguration()
.to(new ConstructorInter(constructorInterceptPoint
.getConstructorInterceptor(), classLoader))
)
);
}
ConstructorInterceptPoint 中描述了插件要增强的构造方法以及增强的 Interceptor 类,与StaticMethodsInterceptPoint 类似,不再展开介绍。
ConstructorInter 与 StaticMethodsInter 类似(这里没有修改构造方法参数的 OverriderArgs 版本,因为此时的构造方法已经调用完成了),ConstructorInter.intercept() 方法的实现如下:
java
@RuntimeType
public void intercept(@This Object obj,
@AllArguments Object[] allArguments) {
// 前面已经让该对象实现了EnhancedInstance接口,所以这里的类型转换是安全的
EnhancedInstance targetObject = (EnhancedInstance)obj;
interceptor.onConstruct(targetObject, allArguments);
}
这里使用的 InstanceConstructorInterceptor 接口与前文介绍的 StaticMethodsAroundInterceptor 接口作用相同,都是留给各个插件去实现增强逻辑的。InstanceConstructorInterceptor 接口的定义如下:
java
public interface InstanceConstructorInterceptor {
void onConstruct(EnhancedInstance objInst, Object[] allArguments);
}
mysql-8.x-plugin 插件对 ConnectionImpl 的增强
到这里你可能感觉实现逻辑有点乱,这里我将以 mysql-8.x-plugin 插件为例,把静态方法增强、构造方法增强等逻辑串起来。
首先来看 mysql-connector-java-8.x.jar 中 com.mysql.cj.jdbc.ConnectionImpl.getInstance() 方法,这是我们创建数据连接的最常用方法,具体实现:
java
public static JdbcConnection getInstance(HostInfo hostInfo)
throws SQLException {
return new ConnectionImpl(hostInfo); // 创建 ConnectionImpl实例
}
先来看 mysql-8.x-plugin 模块的 skywalking-plugin.def 文件,其中定义了ConnectionInstrumentation 这个插件类,它会被 AgentClassLoader 加载,其 enhanceClass() 方法返回的 Matcher 拦截的目标类是 com.mysql.cj.jdbc.ConnectionImpl。
虽然 ConnectionInstrumentation 并不拦截构造方法(因为它的 getConstructorsInterceptPoints() 方法返回的是空数组),但是依然会修改 ConnectionImpl,为其添加 _$EnhancedClassField_ws 字段并实现 EnhanceInstance接口。
在 skywalking-plugin.def 文件中还定义了 ConnectionImplCreateInstrumentation 这个插件类,正如前面介绍的那样,它会拦截 com.mysql.cj.jdbc.ConnectionImpl 的 getInstance() 方法,并委托给 ConnectionCreateInterceptor 进行增强。ConnectionCreateInterceptor 中的 before() 和 handleMethodException() 方法都是空实现,其 after() 方法会记录新建 Connection 的一些信息,具体实现如下:
java
public Object afterMethod(Class clazz, Method method,
Object[] allArguments, Class<?>[] parameterTypes, Object ret) {
if (ret instanceof EnhancedInstance) { // ConnectionImpl已经被增强了
// ConnectionInfo中记录了DB名称、DB类型以及地址等等信息,具体构造过程省
// 略,它会被记录到前面新增的 _$EnhancedClassField_ws 那个字段中
ConnectionInfo connectionInfo = ...
((EnhancedInstance) ret).setSkyWalkingDynamicField(
connectionInfo);
}
return ret;
}
另外,这里还会看到一个 AbstractMysqlInstrumentation 抽象类,继承关系如下图所示:
AbstractMysqlInstrumentation 实现了 witnessClasses() 方法以及 ClassEnhancePluginDefine 中的三个 get*InterceptPoints() 抽象方法(这三个方法都返回 null),其中 witnessClasses() 方法返回"com.mysql.cj.interceptors.QueryInterceptor"字符串,witnessClasses() 方法作用不再重复。
AbstractMysqlInstrumentation 的子类只需根据需求实现相应的 get*InterceptPoints() 方法即可,无需再提供其他剩余 get*InterceptPoints() 方法的空实现。在其他版本的 MySQL 插件中也有 AbstractMysqlInstrumentation 这个抽象类,功能相同,不再重复。
增强实例方法
最后,我们来看 enhanceInstance() 方法对实例方法的增强,其实和增强静态方法的套路一样,我们直接看代码吧:
java
InstanceMethodsInterceptPoint[] instanceMethodsInterceptPoints =
getInstanceMethodsInterceptPoints();
for (InstanceMethodsInterceptPoint instanceMethodsInterceptPoint :
instanceMethodsInterceptPoints) {
String interceptor = instanceMethodsInterceptPoint
.getMethodsInterceptor();
// 目标方法的匹配条件
ElementMatcher.Junction<MethodDescription> junction =
not(isStatic()).and(instanceMethodsInterceptPoint
.getMethodsMatcher());
if (instanceMethodsInterceptPoint instanceof
DeclaredInstanceMethodsInterceptPoint) {
// 目标方法必须定义在目标类中
junction = junction.and(ElementMatchers.
<MethodDescription>isDeclaredBy(typeDescription));
}
if (instanceMethodsInterceptPoint.isOverrideArgs()){ //修改方法参数
newClassBuilder = newClassBuilder
.method(junction) // 匹配目标方法
.intercept(MethodDelegation.withDefaultConfiguration()
// 使用@Morph注解之前,需要通过Morph.Binder绑定一下
.withBinders(Morph.Binder.install(OverrideCallable.class))
.to(new InstMethodsInterWithOverrideArgs(interceptor,
classLoader)));
} else {
// ...省略不需要重载参数的部分...
}
}
增强实例方法过程中使用到的类,在增强静态方法中都有对应的类,如下表所示:
这些类的具体功能不再展开介绍了。
最后依然以 mysql-8.x-plugin 插件为例介绍一下它对实例方法的增强过程,其中 ConnectionInstrumentation.getInstanceMethodsInterceptPoints() 方法返回了 5 个 InstanceMethodsInterceptPoint 对象,这里只看其中的第一个对象:它负责拦截 ConnectionImpl 的 prepareStatement() 方法,并委托给 CreatePreparedStatementInterceptor(不修改方法参数),具体实现代码就不展示了。
在 CreatePreparedStatementInterceptor 中,before() 和 handleMethodException() 方法都是空实现,其 after() 方法实现如下:
java
public Object afterMethod(EnhancedInstance objInst, Method method,
Object[] allArguments, Class<?>[] argumentsTypes,
Object ret) throws Throwable {
if (ret instanceof EnhancedInstance) { // ConnectionImpl已被增强过
// 更新_$EnhancedClassField_ws字段,StatementEnhanceInfos中不仅封
// 装了原有的ConnectionInfo,还包含了具体执行的SQL语句和SQL参数等信息
((EnhancedInstance)ret).setSkyWalkingDynamicField(
new StatementEnhanceInfos(
(ConnectionInfo)objInst.getSkyWalkingDynamicField(),
(String)allArguments[0], "PreparedStatement"));
}
return ret;
}
InterceptorInstanceLoader
前面加载 Interceptpr 的 ClassLoader 并没有使用 AgentClassLoader 的默认实例或是Application ClassLoader,而是通过 InterceptorInstanceLoader 完成加载的。 在 InterceptorInstanceLoader 里面会维护一个 ClassLoader Cache,以及一个 Instance Cache,如下所示:
java
// 记录了 instanceKey与实例之间的映射关系,保证单例
static ConcurrentHashMap<String, Object> INSTANCE_CACHE =
new ConcurrentHashMap<String, Object>();
// 记录了 targetClassLoader以及其子 AgentClassLoader的对应关系
static Map<ClassLoader, ClassLoader> EXTEND_PLUGIN_CLASSLOADERS =
new HashMap<ClassLoader, ClassLoader>();
在通过 InterceptorInstanceLoader.load() 这个静态方法加载 Interceptor 类时的核心逻辑如下:
java
public static <T> T load(String className,
ClassLoader targetClassLoader){
if (targetClassLoader == null) {
targetClassLoader =
InterceptorInstanceLoader.class.getClassLoader();
}
// 通过该 instanceKey保证该 Interceptor在一个 ClassLoader中只创建一次
String instanceKey = className + "_OF_" +
targetClassLoader.getClass().getName() + "@" +
Integer.toHexString(targetClassLoader.hashCode());
Object inst = INSTANCE_CACHE.get(instanceKey);
if (inst == null) {
// 查找targetClassLoader对应的子AgentClassLoader
ClassLoader pluginLoader =
EXTEND_PLUGIN_CLASSLOADERS.get(targetClassLoader);
if (pluginLoader == null) {
// 为 targetClassLoader创建子AgentClassLoader
pluginLoader = new AgentClassLoader(targetClassLoader);
EXTEND_PLUGIN_CLASSLOADERS.put(targetClassLoader,
pluginLoader);
}
// 通过子AgentClassLoader加载Interceptor类
inst = Class.forName(className, true,
pluginLoader).newInstance();
if (inst != null) { // 记录Interceptor对象
INSTANCE_CACHE.put(instanceKey, inst);
}
}
return (T) inst;
}
以 demo-webapp 为例,其类加载器的结构如下图所示:
总结
本课时深入介绍了 Agent 插件增强目标类的实现,这是 Agent 最核心功能,其中深入分析了增强静态方法、构造方法、实例方法的原理,以及插件如何让目标实例对象实现 EnhanceInstance 接口,如何为目标实例对象添加新字段等。为了帮助你更好的理解,在分析的过程中还以 mysql-8.x-plugin 插件为例将上述核心逻辑串连起来。