写点什么

Apache ShenYu 源码阅读系列 -Agent 模块源码分析

作者:子夜2104
  • 2022 年 3 月 14 日
  • 本文字数:20419 字

    阅读完需:约 67 分钟

Apache ShenYu源码阅读系列-Agent模块源码分析

Apache ShenYu 是一个异步的,高性能的,跨语言的,响应式的 API 网关。


ShenYu网关中,Apache ShenYu 利用 Java Agent字节码增强 技术实现了无痕埋点,使得用户无需引入依赖即可接入第三方可观测性系统,获取 TracesMetricsLogging


本文基于shenyu-2.4.2版本进行源码分析,官网的介绍请参考 可观测性


具体而言,就是shenyu-agent模块,它基于 Java Agent 机制,通过ByteBuddy字节码增强库,在类加载时增强对象,属于静态代理。


  • AOP 术语


在分析源码之前,介绍下AOP相关的术语,便于后续的理解:


  • JoinPoint :连接点,程序运行中的时间点,比如方法的执行点;

  • PointCut :切入点,匹配 JoinPoint 的条件;

  • Advice:通知,具体的执行逻辑;

  • Target :目标对象;

  • Proxy :代理对象。

  • 关于 Byte Buddy


Byte Buddy是一个代码生成和操作库,在Java应用程序的运行期间创建和修改Java类。可以利用它创建任何类,不像JDK动态代理那样强制实现一个接口。此外,Byte Buddy提供了方便的 API,用于手动、使用Java代理或在构建期间改变类。


  • 提供了非常方便的 API 接口,与强大的类,方法等匹配功能;

  • 开箱即用,零学习成本,屏蔽了底层操作字节码技术;

  • 强大的开放定制性功能,可以为任何实现的方法自定义字节码;

  • 最少运行时生成代码原则,性能高效;

1. premain 入口

premain()函数javaagent 的入口函数,在 ShenYuShenyuAgentBootstrap 提供并实现整个agent的逻辑。


/** * agent 启动入口类 */public class ShenyuAgentBootstrap {        /**     * 入口函数 premain.     */    public static void premain(final String arguments, final Instrumentation instrumentation) throws Exception {        // 1. 读取配置文件        ShenyuAgentConfigUtils.setConfig(ShenyuAgentConfigLoader.load());        // 2. 加载所有插件        ShenyuAgentPluginLoader.getInstance().loadAllPlugins();        // 3. 创建 agent        AgentBuilder agentBuilder = new AgentBuilder.Default().with(new ByteBuddy().with(TypeValidation.ENABLED))                .ignore(ElementMatchers.isSynthetic())                .or(ElementMatchers.nameStartsWith("org.apache.shenyu.agent."));        agentBuilder.type(ShenyuAgentTypeMatcher.getInstance())                .transform(new ShenyuAgentTransformer())                .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)                .with(new TransformListener()).installOn(instrumentation);        // 4. 启动插件        PluginLifecycleManager lifecycleManager = new PluginLifecycleManager();        lifecycleManager.startup(ShenyuAgentConfigUtils.getPluginConfigMap());        Runtime.getRuntime().addShutdownHook(new Thread(lifecycleManager::close));    }}
复制代码


premain函数的核心逻辑,就是上面的四步操作:


  • 读取配置文件;

  • 加载所有插件;

  • 创建 agent;

  • 启动插件。


接下来的源码分析就依次分析这四个操作。

2. 读取配置文件

  • ShenyuAgentConfigLoader#load()


配置文件的处理由 ShenyuAgentConfigLoader 完成,代码实现如下:


public final class ShenyuAgentConfigLoader {    // 配置文件路径    private static final String CONFIG_PATH = "config-path";        /**     * 加载配置文件.     */    public static ShenyuAgentConfig load() throws IOException {        // 读取配置文件路径        String configPath = System.getProperty(CONFIG_PATH);        // 如果没有配置,就读取默认的文件 shenyu-agent.yaml        File configFile = StringUtils.isEmpty(configPath) ? ShenyuAgentLocator.locatorConf("shenyu-agent.yaml") : new File(configPath);        // 读取配置文件并解析        return ShenyuYamlEngine.agentConfig(configFile);    }}
复制代码


可以通过config-path指定配置文件的路径,如果没有指定的话,就读取默认的配置文件 shenyu-agent.yaml,然后通过ShenyuYamlEngine来解析配置文件。


配置文件的格式是yaml格式,如何配置,请参考官网的介绍 可观测性


默认配置文件shenyu-agent.yaml的格式内容如下:


appName: shenyu-agent  # 指定一个名称supports:  # 当前支持哪些功能  tracing: # 链路追踪的插件#    - jaeger   #    - opentelemetry     - zipkin  metrics:  # 统计度量插件    -   logging:  # 日志信息插件    -   plugins:  # 每个插件的具体配置信息  tracing:   # 链路追踪的插件    jaeger:  # jaeger的相关配置      host: "localhost"      port: 5775      props:        SERVICE_NAME: "shenyu-agent"        JAEGER_SAMPLER_TYPE: "const"        JAEGER_SAMPLER_PARAM: "1"    opentelemetry:  # opentelemetry的相关配置      props:        otel.traces.exporter: jaeger #zipkin #otlp        otel.resource.attributes: "service.name=shenyu-agent"        otel.exporter.jaeger.endpoint: "http://localhost:14250/api/traces"    zipkin: # zipkin的相关配置      host: "localhost"      port: 9411      props:        SERVICE_NAME: "shenyu-agent"        URL_VERSION: "/api/v2/spans"        SAMPLER_TYPE: "const"        SAMPLER_PARAM: "1"  metrics:   # 统计度量插件    prometheus: # prometheus的相关配置      host: "localhost"      port: 8081      props:  logging:   # 日志信息插件    elasticSearch: # es的相关配置      host: "localhost"      port: 8082      props:    kafka:   # kafka的相关配置      host: "localhost"      port: 8082      props:
复制代码


需要开启哪个插件,就在supports中指定,然后再plugins指定插件的配置信息。


到目前为止,Apache ShenYu 发布的最新版本是2.4.2 版本,可以支持tracing的插件有jaegeropentelemetryzipkinmetricslogging将在后续的版本中陆续发布。


  • ShenyuYamlEngine#agentConfig()


ShenyuYamlEngine提供了如何自定义加载yaml格式的文件。


    public static ShenyuAgentConfig agentConfig(final File yamlFile) throws IOException {        try (               // 读取文件流                FileInputStream fileInputStream = new FileInputStream(yamlFile);                InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream)        ) {            //指定对应的class            Constructor constructor = new Constructor(ShenyuAgentConfig.class);            //指定属性的class            TypeDescription customTypeDescription = new TypeDescription(AgentPluginConfig.class);            customTypeDescription.addPropertyParameters("plugins", Map.class);            constructor.addTypeDescription(customTypeDescription);            //通过Yaml工具包读取yaml文件             return new Yaml(constructor, new Representer(DUMPER_OPTIONS)).loadAs(inputStreamReader, ShenyuAgentConfig.class);        }    }
复制代码


ShenyuAgentConfig是指定的 Class 类:


public final class ShenyuAgentConfig {    // appName 服务名称,默认是 shenyu-agent     private String appName = "shenyu-agent";    // supports 支持哪些插件    private Map<String, List<String>> supports = new LinkedHashMap<>();    // plugins 插件的属性信息    private Map<String, Map<String, AgentPluginConfig>> plugins = new LinkedHashMap<>();    }
复制代码


AgentPluginConfig是指定插件的 Class 类:


public final class AgentPluginConfig {    // 指定插件的 host    private String host;     // 指定插件的 port    private int port;     // 指定插件的 password    private String password;     // 指定插件的 其他属性props     private Properties props;}
复制代码


通过配置文件,用户可以指定启用哪个插件,指定插件的属性信息。

3. 加载插件

  • ShenyuAgentPluginLoader#loadAllPlugins()


读取配置文件后,需要根据用户自定义的配置信息,加载指定的插件。由ShenyuAgentPluginLoader来完成。


ShenyuAgentPluginLoader是一个自定义的类加载器,采用单例设计模式。


// 自定义类加载器,继承 ClassLoaderpublic final class ShenyuAgentPluginLoader extends ClassLoader implements Closeable {    // 私有变量        private static final ShenyuAgentPluginLoader AGENT_PLUGIN_LOADER = new ShenyuAgentPluginLoader();    // 私有构造器    private ShenyuAgentPluginLoader() {        super(ShenyuAgentPluginLoader.class.getClassLoader());    }        // 公开静态方法    public static ShenyuAgentPluginLoader getInstance() {        return AGENT_PLUGIN_LOADER;    }        /**     * 加载所有的插件.     */    public void loadAllPlugins() throws IOException {        // 1.定位插件路径        File[] jarFiles = ShenyuAgentLocator.locatorPlugin().listFiles(file -> file.getName().endsWith(".jar"));        if (Objects.isNull(jarFiles)) {            return;        }        // 2.加载插件定义        Map<String, ShenyuAgentJoinPoint> pointMap = new HashMap<>();        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {            for (File each : jarFiles) {                outputStream.reset();                JarFile jar = new JarFile(each, true);                jars.add(new PluginJar(jar, each));            }        }               loadAgentPluginDefinition(pointMap);        Map<String, ShenyuAgentJoinPoint> joinPointMap = ImmutableMap.<String, ShenyuAgentJoinPoint>builder().putAll(pointMap).build();        // 3.设置拦截点        ShenyuAgentTypeMatcher.getInstance().setJoinPointMap(joinPointMap);    }}
复制代码

3.1 定位插件路径

  • ShenyuAgentLocator#locatorPlugin()


整个shenyu项目经过maven打包后(执行mvn clean install命令),agent打包目录如下:



插件文件都是jar包形式存在的。


  • conf目录是配置文件的目录位置;

  • plugins目录是各个插件的目录位置。


相应的定位插件路径源码处理逻辑如下:


// 默认插件位于   /plugins 目录下public static File locatorPlugin() {        return new File(String.join("", locatorAgent().getPath(), "/plugins"));}
// 定位shenyu-agent.jar的绝对路径public static File locatorAgent() { // 找 ShenyuAgentLocator 所在的类路径(包名) String classResourcePath = String.join("", ShenyuAgentLocator.class.getName().replaceAll("\\.", "/"), ".class"); // 找到 类 的绝对路径:磁盘路径+类路径 URL resource = ClassLoader.getSystemClassLoader().getResource(classResourcePath); assert resource != null; String url = resource.toString(); // 是否是以jar包形式存在 int existFileInJarIndex = url.indexOf('!'); boolean isInJar = existFileInJarIndex > -1; // 从jar包找到路径 或 从资源文件中找路径 return isInJar ? getFileInJar(url, existFileInJarIndex) : getFileInResource(url, classResourcePath); }
// 从jar包找到路径 private static File getFileInJar(final String url, final int fileInJarIndex) { // jar包所在的绝对路径 String realUrl = url.substring(url.indexOf("file:"), fileInJarIndex); try { // 以绝对路径创建File对象 File agentJarFile = new File(new URL(realUrl).toURI()); // 获取父文件 return agentJarFile.exists() ? agentJarFile.getParentFile() : null; } catch (final MalformedURLException | URISyntaxException ex) { return null; } }
复制代码


拿到所有的插件文件后,会去加载插件定义,即拦截点。

3.2 加载拦截点

  • ShenyuAgentPluginLoader#loadAgentPluginDefinition()


拦截点的默认配置是在 conf/tracing-point.yaml文件中,配置格式如下:


pointCuts:  - targetClass: org.apache.shenyu.plugin.global.GlobalPlugin  # 拦截目标类    points:      - type: instanceMethod # 拦截点类型        name: execute  # 拦截目标方法    handlers:   # 处理器      jaeger:   # 用于链路追踪的jaeger插件        - org.apache.shenyu.agent.plugin.tracing.jaeger.handler.JaegerGlobalPluginHandler      opentelemetry: # 用于链路追踪的opentelemetry插件        - org.apache.shenyu.agent.plugin.tracing.opentelemetry.handler.OpenTelemetryGlobalPluginHandler      zipkin:    # 用于链路追踪的zipkin插件        - org.apache.shenyu.agent.plugin.tracing.zipkin.handler.ZipkinGlobalPluginHandler                // ......
复制代码


加载拦截点的方式是通过SPI的方式进行加载的,然后再收集这些拦截点。


 private void loadAgentPluginDefinition(final Map<String, ShenyuAgentJoinPoint> pointMap) {        SPILoader.loadList(AgentPluginDefinition.class)  // SPI 加载拦截点                .forEach(each -> each.collector().forEach(def -> {  // 收集拦截点                    String classTarget = def.getClassTarget();                    if (pointMap.containsKey(classTarget)) {                        ShenyuAgentJoinPoint pluginInterceptorPoint = pointMap.get(classTarget);                        pluginInterceptorPoint.getConstructorPoints().addAll(def.getConstructorPoints()); // 构造器类型拦截点                        pluginInterceptorPoint.getInstanceMethodPoints().addAll(def.getInstanceMethodPoints()); // 实例方法类型拦截点                        pluginInterceptorPoint.getStaticMethodPoints().addAll(def.getStaticMethodPoints()); // 静态方法类型拦截点                    } else {                        pointMap.put(classTarget, def);                    }                }));    }
复制代码


  • SPILoader.loadList(AgentPluginDefinition.class)


AgentPluginDefinition是拦截点接口,由@SPI标记:


@SPI // 该接口通过SPI进行加载public interface AgentPluginDefinition {        /**     * 收集拦截点      */    Collection<ShenyuAgentJoinPoint> collector();}
复制代码


TracingAgentPluginDefinition是它的一个实现类,用于定义链路追踪的拦截点:


@Join // SPI的实现类public final class TracingAgentPluginDefinition extends AbstractAgentPluginDefinition {           // 创建拦截点    @Override    protected Collection<JoinPointBuilder> joinPointBuilder() {       // ......    }}
public abstract class AbstractAgentPluginDefinition implements AgentPluginDefinition { // 创建拦截点 protected abstract Collection<JoinPointBuilder> joinPointBuilder(); // 收集拦截点信息 @Override public final Collection<ShenyuAgentJoinPoint> collector() { //...... }}
复制代码


类之间的继承关系如下:



  • AgentPluginDefinition#collector()

  • AgentPluginDefinition只是一个接口,定义了收集拦截点的操作方法,具体实现交给了子类。


public abstract class AbstractAgentPluginDefinition implements AgentPluginDefinition {        // 子类去实现如何创建拦截点    protected abstract Collection<JoinPointBuilder> joinPointBuilder();
@Override public final Collection<ShenyuAgentJoinPoint> collector() { // 获取拦截点构建器 Collection<JoinPointBuilder> joinPointBuilders = joinPointBuilder(); // 创建拦截点对象 ShenyuAgentJoinPoint return joinPointBuilders.stream().map(JoinPointBuilder::install).collect(Collectors.toList()); }}
// 创建拦截点对象 ShenyuAgentJoinPointpublic ShenyuAgentJoinPoint install() { // 四个构造参数分别是:目标对象,构造器拦截点,实例方法拦截点,静态方法拦截点 return new ShenyuAgentJoinPoint(classTarget, constructorPoints, instanceMethodPoints, classStaticMethodPoints);}
复制代码


  • TracingAgentPluginDefinition#joinPointBuilder()

  • 创建用于链路追踪的拦截点。


@Joinpublic final class TracingAgentPluginDefinition extends AbstractAgentPluginDefinition {    // 创建拦截点        @Override    protected Collection<JoinPointBuilder> joinPointBuilder() {        PointCutConfig config = null;        try {            // 读取默认的拦截点配置文件            config = ShenyuYamlEngine.unmarshal(ShenyuAgentLocator.locatorConf("tracing-point.yaml"), PointCutConfig.class);        } catch (IOException e) {            LOG.error("Exception loader tracing point config is", e);        }        // 创建拦截点          return JoinPointBuilderFactory.create(config);    }}
复制代码


  • JoinPointBuilderFactory#create()

  • 根据指定的配置文件创建拦截点 。


public static Collection<JoinPointBuilder> create(final PointCutConfig config) {        //如果没有配置文件或为空,则返回空集合        if (Objects.isNull(config) || config.getPointCuts().isEmpty()) {            return Collections.emptyList();        }        return config.getPointCuts().stream() // 获取配置文件中定义的拦截点                .filter(pointCut -> StringUtils.isNotEmpty(pointCut.getTargetClass())                        && !pointCut.getPoints().isEmpty() && !pointCut.getHandlers().isEmpty()) // 拦截点必须要指定目标类,切入点,处理器                .map(pointCut -> {                    JoinPointBuilder builder = ShenyuAgentJoinPoint.interceptClass(pointCut.getTargetClass()); // 设置需要拦截的目标类                    Set<String> supports = ShenyuAgentConfigUtils.getSupports(); // 获取当前支持哪些插件                    List<String> handlers = pointCut.getHandlers().entrySet().stream()                            .filter(entry -> supports.contains(entry.getKey())) // 指定的处理器必须是当前可支持的插件                            .flatMap(entry -> entry.getValue().stream())                            .collect(Collectors.toList());                    String[] instanceMethods = pointCut                            .getPoints()                            .stream()                            .filter(point -> PointType.INSTANCE_METHOD.getName().equals(point.getType()))                            .map(Point::getName) // 拦截实例方法                            .toArray(String[]::new);                    if (instanceMethods.length > 0) {                        builder.aroundInstanceMethod(ElementMatchers.namedOneOf(instanceMethods)).handlers(handlers).build(); // 为实例方法添加匹配器用于后续运行时动态匹配,并添加对应的处理器                    }                    String[] staticMethods = pointCut                            .getPoints()                            .stream()                            .filter(point -> PointType.STATIC_METHOD.getName().equals(point.getType()))                            .map(Point::getName)                            .toArray(String[]::new); // 拦截静态方法                    if (staticMethods.length > 0) {                        builder.aroundStaticMethod(ElementMatchers.namedOneOf(staticMethods)).handlers(handlers).build();// 为静态方法添加匹配器用于后续运行时动态匹配,并添加对应的处理器                    }                    String[] constructorPoints = pointCut                            .getPoints()                            .stream()                            .filter(point -> PointType.CONSTRUCTOR.getName().equals(point.getType()))                            .map(Point::getName)                            .toArray(String[]::new); // 拦截构造器                    if (constructorPoints.length > 0) {                        builder.onConstructor(ElementMatchers.namedOneOf(constructorPoints)).handlers(handlers).build();// 为构造器添加匹配器用于后续运行时动态匹配,并添加对应的处理器                    }                    return builder;                }).collect(Collectors.toList()); // 返回匹配结果    }
复制代码


创建拦截点的主要实现逻辑是:根据配置文件读取配置信息,为指定的目标对象的目标方法添加相应的处理器。处理器有三种:实例方法处理器,静态方法处理器,构造函数处理器。


这里用到了ElementMatchers.namedOneOf() 方法,它表示方法名称在指定的参数中,就可以匹配上这个方法。ElementMatchersbytebuddy中的一个类,在ShenYu中,agent的创建也通过bytebuddy完成的。


后续将收集到的拦截点创建为拦截点对象ShenyuAgentJoinPoint


    public final Collection<ShenyuAgentJoinPoint> collector() {        // 获取拦截点        Collection<JoinPointBuilder> joinPointBuilders = joinPointBuilder();        // 创建拦截点对象        return joinPointBuilders.stream().map(JoinPointBuilder::install).collect(Collectors.toList());    }
复制代码


收集完拦截点之后,用Map保存了这些拦截点信息。


// pointMap: Key和Value分别表示目标类,拦截点private void loadAgentPluginDefinition(final Map<String, ShenyuAgentJoinPoint> pointMap) {        SPILoader.loadList(AgentPluginDefinition.class)  // SPI 加载拦截点                .forEach(each -> each.collector().forEach(def -> {  // 收集拦截点                    String classTarget = def.getClassTarget();                    if (pointMap.containsKey(classTarget)) {                        ShenyuAgentJoinPoint pluginInterceptorPoint = pointMap.get(classTarget);                        pluginInterceptorPoint.getConstructorPoints().addAll(def.getConstructorPoints()); // 构造器类型拦截点                        pluginInterceptorPoint.getInstanceMethodPoints().addAll(def.getInstanceMethodPoints()); // 实例方法类型拦截点                        pluginInterceptorPoint.getStaticMethodPoints().addAll(def.getStaticMethodPoints()); // 静态方法类型拦截点                    } else {                        pointMap.put(classTarget, def); // 将拦截点信息保存到Map中                    }                }));    }
复制代码

3.3 设置拦截点

在加载所有插件的过程中最后一步是设置拦截点。


     public void loadAllPlugins() throws IOException {        // 1.定位插件路径        // ......        // 2.加载插件定义        // ......        // 3.设置拦截点        ShenyuAgentTypeMatcher.getInstance().setJoinPointMap(joinPointMap);    }
复制代码


设置拦截点就是将拦截点集合保存到ShenyuAgentTypeMatcher类中。它实现了ElementMatcher接口,用于自定义匹配逻辑。ElementMatcher也是bytebuddy中的接口。


// 使用单例设计模式public final class ShenyuAgentTypeMatcher extends ElementMatcher.Junction.AbstractBase<TypeDefinition> {    // 创建实例    private static final ShenyuAgentTypeMatcher SHENYU_AGENT_TYPE_MATCHER = new ShenyuAgentTypeMatcher();    // 拦截点集合    private Map<String, ShenyuAgentJoinPoint> joinPointMap;        private ShenyuAgentTypeMatcher() {    }        /**     * 获取单例     */    public static ShenyuAgentTypeMatcher getInstance() {        return SHENYU_AGENT_TYPE_MATCHER;    }     //自定义匹配逻辑,目标类在拦截点集合中就匹配成功    @Override    public boolean matches(final TypeDefinition target) {        return joinPointMap.containsKey(target.getTypeName());    }        /**     * 设置拦截点集合     */    public void setJoinPointMap(final Map<String, ShenyuAgentJoinPoint> joinPointMap) {        this.joinPointMap = joinPointMap;    }}
复制代码

4. 创建 agent

通过创建的 agent,用于改变目标类的行为。


 public static void premain(final String arguments, final Instrumentation instrumentation) throws Exception {        // 1. 读取配置文件        // ......        // 2. 加载所有插件        // ......        // 3. 创建 agent        AgentBuilder agentBuilder = new AgentBuilder.Default().with(new ByteBuddy().with(TypeValidation.ENABLED)) // 通过ByteBuddy创建Agent,开启类型校验                .ignore(ElementMatchers.isSynthetic()) // 忽略合成类                .or(ElementMatchers.nameStartsWith("org.apache.shenyu.agent.")); // 忽略org.apache.shenyu.agent 的类        agentBuilder.type(ShenyuAgentTypeMatcher.getInstance())//匹配加载类型,匹配器是ShenyuAgentTypeMatcher                .transform(new ShenyuAgentTransformer()) // 匹配成功的,通过ShenyuAgentTransformer改变其行为                .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION) //指定重定义策略                .with(new TransformListener()) //指定一个监听器,监听运行过程中的事件                .installOn(instrumentation); // 将修改应用到instrumentation中        // 4. 启动插件        // ......    }
复制代码


在创建 agent 的过程中,需要注意两个点:


  • 是否匹配成功,由ShenyuAgentTypeMatcher决定;

  • 匹配成功的类,通过ShenyuAgentTransformer改变其行为;


接下来我们就来着重分析这两个类。

4.1 定义匹配逻辑

  • ShenyuAgentTypeMatcher#matches()


ShenyuAgentTypeMatcher使用单例的设计模式,实现了ElementMatcher接口,重写了matches()方法。


是否能够匹配成功的逻辑是:如果目标类在拦截点joinPointMap集合中,就匹配成功。



public final class ShenyuAgentTypeMatcher extends ElementMatcher.Junction.AbstractBase<TypeDefinition> { // ...... private Map<String, ShenyuAgentJoinPoint> joinPointMap;
// 自定义匹配逻辑:如果目标类在拦截点joinPointMap集合中,就匹配成功 @Override public boolean matches(final TypeDefinition target) { return joinPointMap.containsKey(target.getTypeName()); } }
复制代码

4.2 改变匹配类的行为

在加载目标类时,如果匹配成功,会通过ShenyuAgentTransformer改变其行为,它实现了Transformer接口,重写了transform()方法,Transformer也是bytebuddy的一个接口。


public final class ShenyuAgentTransformer implements Transformer {    //在匹配类中额外定义一个字段,传递上下文信息    private static final String EXTRA_DATA = "_$EXTRA_DATA$_";    //类型匹配器        private static final ShenyuAgentTypeMatcher MATCHER = ShenyuAgentTypeMatcher.getInstance();        //重写transform方法,重新定义匹配类的行为    //加载类期间,执行一次    @Override    public Builder<?> transform(final Builder<?> builder, final TypeDescription typeDescription, final ClassLoader classLoader, final JavaModule module) {        //不是匹配的类就跳过        if (!MATCHER.containsType(typeDescription)) {            return builder;        }        //为该类新增加一个字段        Builder<?> result = builder.defineField(EXTRA_DATA, Object.class, Opcodes.ACC_PRIVATE | Opcodes.ACC_VOLATILE).implement(TargetObject.class).intercept(FieldAccessor.ofField(EXTRA_DATA));        // 获取拦截点        ShenyuAgentJoinPoint joinPoint = MATCHER.loadShenyuAgentJoinPoint(typeDescription);        //拦截构造器        result = interceptorConstructorPoint(typeDescription, joinPoint.getConstructorPoints(), result);        //拦截静态方法        result = interceptorStaticMethodPoint(typeDescription, joinPoint.getStaticMethodPoints(), result);        //拦截实例方法        result = interceptorInstanceMethodPoint(typeDescription, joinPoint.getInstanceMethodPoints(), result);        return result;    }
// ......}
复制代码


transform()方法中,重新定义了匹配类的行为:


  • 新增了字段,是TargetObject的子类,用于传递上下文;

  • 根据指定配置,是否拦截构造器;

  • 根据指定配置,是否拦截静态方法;

  • 根据指定配置,拦截实例方法;

4.2.3 拦截实例方法
  • ShenyuAgentTransformer#interceptorInstanceMethodPoint()


根据切面构建实例方法拦截点,获取Builder对象。


    private Builder<?> interceptorInstanceMethodPoint(final TypeDescription description, final Collection<InstanceMethodPointCut> pointCuts, final Builder<?> builder) {        Collection<ShenyuAgentTransformerPoint<?>> points = description.getDeclaredMethods().stream()                .filter(each -> !(each.isAbstract() || each.isSynthetic())) //过滤抽象方法,合成方法                .map(each -> buildInstanceMethodTransformationPoint(pointCuts, each)) //构建实例方法拦截点                .filter(Objects::nonNull)                .collect(Collectors.toList());        return getBuilder(description, builder, points); //获取Builder对象    }
复制代码


  • ShenyuAgentTransformer#buildInstanceMethodTransformationPoint()


过滤匹配上的方法,为方法获取对应的handler处理器,最后创建实例方法拦截点对象。


    private ShenyuAgentTransformerPoint<?> buildInstanceMethodTransformationPoint(final Collection<InstanceMethodPointCut> pointCuts, final InDefinedShape methodDescription) {        List<InstanceMethodPointCut> points = pointCuts.stream().filter(point -> point.getMatcher().matches(methodDescription)).collect(Collectors.toList()); //过滤能够匹配上的方法        if (points.isEmpty()) {            return null;        }        List<InstanceMethodHandler> handlers = points.stream()                .flatMap(pointCut -> pointCut.getHandlers().stream())                .map(handler -> (InstanceMethodHandler) MATCHER.getOrCreateInstance(handler)) //获取对应的handler处理器                .filter(Objects::nonNull)                .collect(Collectors.toList());        return new ShenyuAgentTransformerPoint<>(methodDescription, new InstanceMethodInterceptor(handlers));//创建实例方法拦截点对象,创建实例方法拦截器    }
复制代码


方法能否匹配成功:当前方法名称是否是在tracing-point.yaml文件中配置的方法名称;


handler的获取:是根据tracing-point.yaml文件中配置的全限定名去加载对应的类。


  • InstanceMethodInterceptor#intercept()


实例方法拦截器InstanceMethodInterceptor会在运行期间动态处理拦截方法。


public class InstanceMethodInterceptor {    // ......    /**     * 拦截目标对象.     *     * @param target 当前被拦截的目标对象     * @param method 目标对象的目标方法     * @param args 目标方法的参数     * @param callable 目标方法调用     * @return 目标方法调用结果     * @throws 异常信息     */    @RuntimeType //定义运行时的目标方法    public Object intercept(@This final Object target, @Origin final Method method, @AllArguments final Object[] args, @SuperCall final Callable<?> callable) throws Exception {        //目标方法执行结果        Object result = null;        //目标对象        TargetObject instance = (TargetObject) target;        //依次调用handler        for (InstanceMethodHandler handler : handlerList) {            MethodResult methodResult = new MethodResult();            // 前置处理逻辑            try {                handler.before(instance, method, args, methodResult);            } catch (final Throwable ex) {                LOG.error("Failed to execute the before method of method {} in class {}", method.getName(), target.getClass(), ex);            }            //调用目标方法            try {                result = callable.call();            } catch (final Throwable ex) {         //处理异常                try {                    handler.onThrowing(instance, method, args, ex);                } catch (final Throwable ignored) {                    LOG.error("Failed to execute the error handler of method {} in class {}", method.getName(), target.getClass(), ex);                    throw ex;                }            } finally {                //后置处理逻辑                try {                    result = handler.after(instance, method, args, methodResult, result);                } catch (final Throwable ex) {                    LOG.error("Failed to execute the after method of method {} in class {}", method.getName(), target.getClass(), ex);                }            }        }        //返回目标方法调用结果        return result;    }}
复制代码


实例方法拦截器在目标方法调用前,增加了前置处理逻辑,后置处理逻辑,以及异常处理逻辑。


这里用到了Byte Buddy的几个注解:


  • @RuntimeType: 定义运行时的目标方法,提示ByteBuddy禁用严格的类型检查;

  • @This:当前被拦截的、动态生成的实例对象;

  • @Origin:原有方法;

  • @AllArguments:获取所有入参;

  • @SuperCall:用于调用父类版本的方法。


实例方法处理器InstanceMethodHandler只是定义了三个接口,具体实现逻辑由具体插件去处理。


public interface InstanceMethodHandler {     // 前置处理逻辑    default void before(final TargetObject target, final Method method, final Object[] args, final MethodResult result) {    }        // 后置处理逻辑    default Object after(final TargetObject target, final Method method, final Object[] args, final MethodResult methodResult, final Object result) {        return result;    }       // 异常处理逻辑    default void onThrowing(final TargetObject target, final Method method, final Object[] args, final Throwable throwable) {    }}
复制代码


  • ShenyuAgentTransformer#getBuilder()

  • AgentBuilder获取时,为指定的目标方法指定对应的拦截器,在原有方法上添加新的逻辑,改变原有方法行为。


        private static Builder<?> getBuilder(final TypeDescription description, final Builder<?> builder, final Collection<ShenyuAgentTransformerPoint<?>> points) {        final Builder<?>[] result = {builder};        points.forEach(point -> {            try {                result[0] = builder.method(ElementMatchers.is(point.getDescription()))//指定目标方法                        .intercept(MethodDelegation.withDefaultConfiguration().to(point.getInterceptor()));//指定拦截器                // CHECKSTYLE:OFF            } catch (final Throwable ex) {                // CHECKSTYLE:ON                LOG.error("Failed to load handler class: {}", description.getTypeName(), ex);            }        });        return result[0];    }
复制代码


通过以上的处理逻辑,就可以实现无侵入拦截实例方法了。


拦截方法intercept()的处理逻辑是:


  • 依次处理每个handler

  • 调用handler的前置方法;

  • 调用目标方法;

  • 如果目标方法有异常,则调用handler的异常处理方法;

  • 调用handler的后置方法。


接下来看看拦截静态方法。

4.2.3 拦截静态方法
  • ShenyuAgentTransformer#interceptorStaticMethodPoint()


过滤出静态方法,然后为静态方法构建静态方法拦截点。


    private Builder<?> interceptorStaticMethodPoint(final TypeDescription description, final Collection<StaticMethodPointCut> pointCuts, final Builder<?> builder) {        Collection<ShenyuAgentTransformerPoint<?>> points = description.getDeclaredMethods().stream()                .filter(each -> each.isStatic() && !(each.isAbstract() || each.isSynthetic())) // 当前方法是静态方法,不是抽象方法,不是合成方法                .map(methodDescription -> buildStaticMethodTransformationPoint(pointCuts, methodDescription)) // 构建静态方法拦截点                .filter(Objects::nonNull)                .collect(Collectors.toList());        return getBuilder(description, builder, points);    }
复制代码


  • ShenyuAgentTransformer#buildStaticMethodTransformationPoint()


根据配置文件进行过滤,判断当前的静态方法是否需要拦截。然后获取对应的处理器,最后构建静态方法拦截器对象。


    private ShenyuAgentTransformerPoint<?> buildStaticMethodTransformationPoint(final Collection<StaticMethodPointCut> pointCuts, final InDefinedShape methodDescription) {        List<StaticMethodPointCut> staticMethodPoints = pointCuts.stream().filter(point -> point.getMatcher().matches(methodDescription)).collect(Collectors.toList()); //根据配置文件进行过滤,判断当前的静态方法是否需要拦截        if (staticMethodPoints.isEmpty()) { // 如果没有配置,就直接返回了            return null;        }        List<StaticMethodHandler> handlers = staticMethodPoints.stream()                .flatMap(pointCut -> pointCut.getHandlers().stream())                .map(handler -> (StaticMethodHandler) MATCHER.getOrCreateInstance(handler)) //获取对应的处理器                .filter(Objects::nonNull)                .collect(Collectors.toList());        return new ShenyuAgentTransformerPoint<>(methodDescription, new StaticMethodInterceptor(handlers)); //构建静态方法拦截器对象    }
复制代码


  • StaticMethodInterceptor#intercept()


在运行时,会拦截目标方法,执行拦截器的处理逻辑 。



public class StaticMethodInterceptor { //...... /** * 拦截目标方法. */ @RuntimeType public Object intercept(@Origin final Class<?> klass, @Origin final Method method, @AllArguments final Object[] args, @SuperCall final Callable<?> callable) throws Exception { Object result = null; // handler循环处理 for (StaticMethodHandler handler : handlerList) { MethodResult methodResult = new MethodResult(); try { //前置方法 handler.before(klass, method, args, new MethodResult()); } catch (final Throwable ex) { LOG.error("Failed to execute the before method of method {} in class {}", method.getName(), klass, ex); } try { // 调用当前方法 // 目标方法是不是应该只会被调用一次? result = callable.call(); } catch (final Throwable ex) { try { //异常逻辑处理 handler.onThrowing(klass, method, args, ex); } catch (final Throwable ignored) { LOG.error("Failed to execute the error handler of method {} in class {}", method.getName(), klass, ex); throw ex; } } finally { try { // 后置方法 handler.after(klass, method, args, methodResult); } catch (final Throwable ex) { LOG.error("Failed to execute the after method of method {} in class {}", method.getName(), klass, ex); } } } //返回方法调用结果 return result; }}
复制代码


拦截方法intercept()的处理逻辑是:


  • 依次处理每个handler

  • 调用handler的前置方法;

  • 调用目标方法;

  • 如果目标方法有异常,则调用handler的异常处理方法;

  • 调用handler的后置方法。


最后再看看如何拦截构造器。

4.2.3 拦截构造器
  • ShenyuAgentTransformer#interceptorConstructorPoint()


过滤出构造器,然后构建构造器的拦截点,最后创建builder对象,为构造方法添加拦截器。


 private Builder<?> interceptorConstructorPoint(final TypeDescription description, final Collection<ConstructorPointCut> constructorPoints, final Builder<?> builder) {        Collection<ShenyuAgentTransformerPoint<? extends ConstructorInterceptor>> constructorAdviceComposePoints = description.getDeclaredMethods().stream()                .filter(MethodDescription::isConstructor) //过滤出构造器                .map(each -> buildConstructorTransformerPoint(constructorPoints, each))//构建构造器的拦截点                .filter(Objects::nonNull)                .collect(Collectors.toList());        final Builder<?>[] result = {builder};     // 创建builder对象,为构造方法添加拦截器        constructorAdviceComposePoints.forEach(point -> {            try {                result[0] = builder.constructor(ElementMatchers.is(point.getDescription()))                        .intercept(SuperMethodCall.INSTANCE.andThen(MethodDelegation.withDefaultConfiguration()                                .to(point.getInterceptor())));//先调用父类构造器,然后添加拦截器                // CHECKSTYLE:OFF            } catch (final Throwable ex) {                // CHECKSTYLE:ON                LOG.error("Failed to load handler class: {}", description.getTypeName(), ex);            }        });        return result[0];    }
复制代码


  • ShenyuAgentTransformer#buildConstructorTransformerPoint()


先获取到构造器拦截点,然后为拦截点创建handler实例对象,最后创建构造器拦截器对象。


private ShenyuAgentTransformerPoint<? extends ConstructorInterceptor> buildConstructorTransformerPoint(            final Collection<ConstructorPointCut> constructorPoints, final InDefinedShape methodDescription) {    //获取构造器拦截点        List<ConstructorPointCut> constructorPointCutList = constructorPoints.stream().filter(each -> each.getMatcher().matches(methodDescription)).collect(Collectors.toList());        if (constructorPointCutList.isEmpty()) {            return null;        }        List<ConstructorHandler> handlers = constructorPointCutList.stream()                .flatMap(pointCut -> pointCut.getHandlers().stream())                .map(handler -> (ConstructorHandler) MATCHER.getOrCreateInstance(handler)) //创建拦截点的handler实例对象                .filter(Objects::nonNull)                .collect(Collectors.toList());        return new ShenyuAgentTransformerPoint<>(methodDescription, new ConstructorInterceptor(handlers));//创建构造器拦截器    }
复制代码


  • ConstructorInterceptor#intercept()


构造器拦截器:在调用目标方法的构造器之前,执行每个handler的处理逻辑。



public class ConstructorInterceptor { //...... /** * 拦截方法. */ @RuntimeType public void intercept(@This final TargetObject target, @AllArguments final Object[] args) { for (ConstructorHandler handler : handlerList) { try { // handler处理逻辑 handler.onConstructor(target, args); } catch (final Throwable throwable) { LOG.error("Constructor advice execution error. class: {}", target.getClass().getTypeName(), throwable); } } }}
复制代码


分析到此,我们分析完了创建agent的整个过程:


  • 根据配置文件,定义匹配逻辑ShenyuAgentTypeMatcher

  • 定义ShenyuAgentTransformer对象,改变匹配类的行为;

  • 通过InstanceMethodInterceptor拦截实例对象方法;

  • 通过StaticMethodInterceptor拦截静态方法;

  • 通过ConstructorInterceptor拦截构造器。


这里没有提到每个handler的处理逻辑,是因为handler的实现逻辑由每个插件自定义。比如,当前实例方法拦截器InstanceMethodHandler的实现类就有jaegeropentelemetryzipkin


5. 启动插件

创建完 agent之后,启动各个插件。


 public static void premain(final String arguments, final Instrumentation instrumentation) throws Exception {        // 1. 读取配置文件        // ......        // 2. 加载所有插件        // ......        // 3. 创建 agent               // 4. 启动插件        PluginLifecycleManager lifecycleManager = new PluginLifecycleManager();        lifecycleManager.startup(ShenyuAgentConfigUtils.getPluginConfigMap());        //添加hook函数用于关闭插件        Runtime.getRuntime().addShutdownHook(new Thread(lifecycleManager::close));    }
复制代码


  • PluginLifecycleManager

  • PluginLifecycleManager负责插件的生命周期管理,用于启动插件和关闭插件。

  • 插件的启动,必须要在配置文件中指定。



public class PluginLifecycleManager { //...... /** * 启动插件. */ public void startup(final Map<String, AgentPluginConfig> configMap) { //从配置文件中获取支持的插件名称 Set<String> support = ShenyuAgentConfigUtils.getSupports(); configMap.entrySet().stream() .filter(entry -> support.contains(entry.getKey())) //包含在配置文件中 .forEach(entry -> Optional.ofNullable(SPILoader.load(AgentPluginBootService.class, entry.getKey())) //通过SPI加载插件启动类 .ifPresent(bootService -> { try { LOG.info("start shenyu plugin: {}", entry.getKey()); bootService.start(entry.getValue()); // 启动插件:执行插件的具体启动逻辑 } catch (final Throwable ex) { LOG.error("Failed to start shenyu plugin", ex); } })); } /** * 关闭插件 */ public void close() { //通过SPI加载插件启动类 SPILoader.loadList(AgentPluginBootService.class).forEach(each -> { try { each.close(); // 关闭插件:执行插件的具体关闭逻辑 } catch (final Throwable ex) { LOG.error("Failed to close shenyu agent plugin", ex); } }); }}
复制代码


插件的启动和关闭也是有每个插件具体去实现的,然后通过SPI去加载。

6. 总结

  • shenyu-agent模块的实现主要是通过Byte Buddy工具包;

  • 在配置文件shenyu-agent.yaml中,指定插件信息;

  • 插件加载过程通过SPI完成;

  • 拦截点通过配置文件指定,设计灵活;

  • 插件接口定义和实现分开,支持多种插件类型。

发布于: 刚刚阅读数: 2
用户头像

子夜2104

关注

还未添加个人签名 2018.03.21 加入

还未添加个人简介

评论

发布
暂无评论
Apache ShenYu源码阅读系列-Agent模块源码分析_子夜2104_InfoQ写作平台