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函数的核心逻辑,就是上面的四步操作:

    1. 读取配置文件;
    1. 加载所有插件;
    1. 创建 agent;
    1. 启动插件。

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

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是一个自定义的类加载器,采用单例设计模式。

// 自定义类加载器,继承 ClassLoader
public 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 
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值