Apache ShenYu 是一个异步的,高性能的,跨语言的,响应式的
API
网关。
在ShenYu
网关中,Apache ShenYu
利用 Java Agent
和 字节码增强
技术实现了无痕埋点,使得用户无需引入依赖即可接入第三方可观测性系统,获取 Traces
、Metrics
和 Logging
。
本文基于
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
的入口函数,在 ShenYu
由 ShenyuAgentBootstrap
提供并实现整个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
的插件有jaeger
、opentelemetry
和zipkin
,metrics
和logging
将在后续的版本中陆续发布。
- 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