SkyWalking Java Agent 配置初始化流程分析

基于 SkyWalking Java Agent 8.8.0 版本

上一篇文章我们通过 SkyWalking Java Agent 日志组件分析一文详细介绍了日志相关的底层实现原理,今天我们要正式进入 premain 方法了,premain 方法见名知义就是在我们 Java 程序的 main 方法之前运行的方法,一般我们通过 JVM 参数 -javaagent:/path/to/skywalking-agent.jar 的方式指定代理程序。

今天我们要分析的是 SkyWalking Java Agent 配置相关内容,我们接触到的框架大都需要一些配置文件,比如 SpringBoot 中的 application.yml。 SkyWalking Java Agent 在 premain 方法中首先做的就是通过 SnifferConfigInitializer.initializeCoreConfig(agentArgs); 初始化核心配置。

/**
 * The main entrance of sky-walking agent, based on javaagent mechanism.
 */
public class SkyWalkingAgent {
    private static ILog LOGGER = LogManager.getLogger(SkyWalkingAgent.class);

    /**
     * Main entrance. Use byte-buddy transform to enhance all classes, which define in plugins.
     */
    public static void premain(String agentArgs, Instrumentation instrumentation) throws PluginException {
        final PluginFinder pluginFinder;
        try {
            SnifferConfigInitializer.initializeCoreConfig(agentArgs);
        } catch (Exception e) {
            // try to resolve a new logger, and use the new logger to write the error log here
            LogManager.getLogger(SkyWalkingAgent.class)
                    .error(e, "SkyWalking agent initialized failure. Shutting down.");
            return;
        } finally {
            // refresh logger again after initialization finishes
            LOGGER = LogManager.getLogger(SkyWalkingAgent.class);
        }
        // 省略部分代码....
        
    }
    // 省略部分代码....
}        
复制代码

今天我们要重点分析的就是这行代码的内部实现

SnifferConfigInitializer.initializeCoreConfig(agentArgs);
复制代码

建议在继续阅读之前大家可以先阅读 skywalking-java 项目官方提供的关于配置相关的文档

初始化核心配置

SnifferConfigInitializer 类使用多种方式初始化配置,内部实现有以下几个重要步骤:

skywalking-java-config.png

1.loadConfig() 加载配置文件

  • 从指定的配置文件路径读取配置文件内容,通过 -Dskywalking_config=/xxx/yyy 可以指定配置文件位置;
  • 如果没有指定配置文件路径,则从默认配置文件 config/agent.config 读取;
  • 将配置文件内容加载到 Properties;
/**
 * Load the specified config file or default config file
 *
 * @return the config file {@link InputStream}, or null if not needEnhance.
 */
private static InputStreamReader loadConfig() throws AgentPackageNotFoundException, ConfigNotFoundException {
    // System.getProperty() 读取 Java 虚拟机中的系统属性, Java 虚拟机中的系统属性在运行Java程序的时候通过 java -Dk1=v1 配置.
    String specifiedConfigPath = System.getProperty(SPECIFIED_CONFIG_PATH);
    // 使用指定的配置文件或默认的配置文件, AgentPackagePath.getPath() 获取 skywalking-agent.jar 所在目录
    File configFile = StringUtil.isEmpty(specifiedConfigPath) ? new File(
        AgentPackagePath.getPath(), DEFAULT_CONFIG_FILE_NAME) : new File(specifiedConfigPath);

    if (configFile.exists() && configFile.isFile()) {
        try {
            LOGGER.info("Config file found in {}.", configFile);

            return new InputStreamReader(new FileInputStream(configFile), StandardCharsets.UTF_8);
        } catch (FileNotFoundException e) {
            throw new ConfigNotFoundException("Failed to load agent.config", e);
        }
    }
    throw new ConfigNotFoundException("Failed to load agent.config.");
}
复制代码

2.replacePlaceholders() 解析占位符 placeholder

  • ​从配置文件中读取到的配置值都是以 placeholder 形式(比如 agent.service_name=${SW_AGENT_NAME:Your_ApplicationName})存在的,这里需要将占位符解析为实际值。
/**
 * Replaces all placeholders of format {@code ${name}} with the corresponding property from the supplied {@link
 * Properties}.
 *
 * @param value      the value containing the placeholders to be replaced
 * @param properties the {@code Properties} to use for replacement
 * @return the supplied value with placeholders replaced inline
 */
public String replacePlaceholders(String value, final Properties properties) {
    return replacePlaceholders(value, new PlaceholderResolver() {
        @Override
        public String resolvePlaceholder(String placeholderName) {
            return getConfigValue(placeholderName, properties);
        }
    });
}

// 优先级 System.Properties(-D) > System environment variables > Config file
private String getConfigValue(String key, final Properties properties) {
    // 从Java虚拟机系统属性中获取(-D)
    String value = System.getProperty(key);
    if (value == null) {
        // 从操作系统环境变量获取, 比如 JAVA_HOME、Path 等环境变量
        value = System.getenv(key);
    }
    if (value == null) {
        // 从配置文件中获取
        value = properties.getProperty(key);
    }
    return value;
}
复制代码

3.overrideConfigBySystemProp() 读取 System.getProperties() 中以 skywalking. 开头的系统属性覆盖配置;

/**
 * Override the config by system properties. The property key must start with `skywalking`, the result should be as
 * same as in `agent.config`
 * <p>
 * such as: Property key of `agent.service_name` should be `skywalking.agent.service_name`
 */
private static void overrideConfigBySystemProp() throws IllegalAccessException {
    Properties systemProperties = System.getProperties();
    for (final Map.Entry<Object, Object> prop : systemProperties.entrySet()) {
        String key = prop.getKey().toString();
        // 必须是以 skywalking. 开头的属性
        if (key.startsWith(ENV_KEY_PREFIX)) {
            String realKey = key.substring(ENV_KEY_PREFIX.length());
            AGENT_SETTINGS.put(realKey, prop.getValue());
        }
    }
}
复制代码

4.overrideConfigByAgentOptions() 解析 agentArgs 参数配置覆盖配置;

扫描二维码关注公众号,回复: 13653346 查看本文章
  • agentArgs 就是 premain 方法的第一个参数,以 -javaagent:/path/to/skywalking-agent.jar=k1=v1,k2=v2的形式传值。

5.initializeConfig() 将以上读取到的配置信息映射到 Config 类的静态属性,后面会重点分析;

6.configureLogger() 根据配置的 Config.Logging.RESOLVER 重配置 Log,更多关于日志参见文章 SkyWalking Java Agent 日志组件分析

static void configureLogger() {
    switch (Config.Logging.RESOLVER) {
        case JSON:
            LogManager.setLogResolver(new JsonLogResolver());
            break;
        case PATTERN:
        default:
            LogManager.setLogResolver(new PatternLogResolver());
    }
}
复制代码

7.必填参数验证,验证非空参数 agent.service_name 和 collector.servers。

定位 skywalking-agent.jar 所在目录

skywalking-agent 目录结构如下:

skywalking-agent.png

config 目录存放的是默认配置文件 agent.config,读取默认配置文件,以及后面加载插件都需要用到 skywalking-agent.jar 所在目录。

/**
 * Load the specified config file or default config file
 *
 * @return the config file {@link InputStream}, or null if not needEnhance.
 */
private static InputStreamReader loadConfig() throws AgentPackageNotFoundException, ConfigNotFoundException {
    // System.getProperty() 读取 Java 虚拟机中的系统属性, Java 虚拟机中的系统属性在运行Java程序的时候通过 java -Dk1=v1 配置.
    String specifiedConfigPath = System.getProperty(SPECIFIED_CONFIG_PATH);
    // 使用指定的配置文件或默认的配置文件, AgentPackagePath.getPath() 获取 skywalking-agent.jar 所在目录
    File configFile = StringUtil.isEmpty(specifiedConfigPath) ? new File(
        AgentPackagePath.getPath(), DEFAULT_CONFIG_FILE_NAME) : new File(specifiedConfigPath);

    if (configFile.exists() && configFile.isFile()) {
        try {
            LOGGER.info("Config file found in {}.", configFile);

            return new InputStreamReader(new FileInputStream(configFile), StandardCharsets.UTF_8);
        } catch (FileNotFoundException e) {
            throw new ConfigNotFoundException("Failed to load agent.config", e);
        }
    }
    throw new ConfigNotFoundException("Failed to load agent.config.");
}
复制代码

new File(AgentPackagePath.getPath(), DEFAULT_CONFIG_FILE_NAME) 定位默认配置文件的位置, AgentPackagePath.getPath() 方法用来获取 skywalking-agent.jar 所在目录

/**
 * AgentPackagePath is a flag and finder to locate the SkyWalking agent.jar. It gets the absolute path of the agent jar.
 * The path is the required metadata for agent core looking up the plugins and toolkit activations. If the lookup
 * mechanism fails, the agent will exit directly.
 */
public class AgentPackagePath {
    private static final ILog LOGGER = LogManager.getLogger(AgentPackagePath.class);

    private static File AGENT_PACKAGE_PATH;

    public static File getPath() throws AgentPackageNotFoundException {
        if (AGENT_PACKAGE_PATH == null) {
            // 返回 skywalking-agent.jar 文件所在的目录 E:\develop\source\sample\source\skywalking-java\skywalking-agent
            AGENT_PACKAGE_PATH = findPath();
        }
        return AGENT_PACKAGE_PATH;
    }

    public static boolean isPathFound() {
        return AGENT_PACKAGE_PATH != null;
    }

    private static File findPath() throws AgentPackageNotFoundException {
        // 将 AgentPackagePath 全类名中的.替换成 /
        // org/apache/skywalking/apm/agent/core/boot/AgentPackagePath.class
        String classResourcePath = AgentPackagePath.class.getName().replaceAll("\.", "/") + ".class";
        // 使用 AppClassLoader 加载资源,通常情况下 AgentPackagePath 类是被 AppClassLoader 加载的。
        URL resource = ClassLoader.getSystemClassLoader().getResource(classResourcePath);
        if (resource != null) {
            String urlString = resource.toString();
            //jar:file:/E:/source/skywalking-java/skywalking-agent/skywalking-agent.jar!/org/apache/skywalking/apm/agent/core/boot/AgentPackagePath.class
            LOGGER.debug("The beacon class location is {}.", urlString);

            // 判断 url 中是否包含!,如果包含则说明 AgentPackagePath.class 是包含在jar中。
            int insidePathIndex = urlString.indexOf('!');
            boolean isInJar = insidePathIndex > -1;

            if (isInJar) {
                // file:/E:/source/skywalking-java/skywalking-agent/skywalking-agent.jar
                urlString = urlString.substring(urlString.indexOf("file:"), insidePathIndex);
                File agentJarFile = null;
                try {
                    // E:\source\skywalking-java\skywalking-agent\skywalking-agent.jar
                    agentJarFile = new File(new URL(urlString).toURI());
                } catch (MalformedURLException | URISyntaxException e) {
                    LOGGER.error(e, "Can not locate agent jar file by url:" + urlString);
                }
                if (agentJarFile.exists()) {
                    // 返回 skywalking-agent.jar 文件所在的目录
                    return agentJarFile.getParentFile();
                }
            } else {
                int prefixLength = "file:".length();
                String classLocation = urlString.substring(
                    prefixLength, urlString.length() - classResourcePath.length());
                return new File(classLocation);
            }
        }

        LOGGER.error("Can not locate agent jar file.");
        throw new AgentPackageNotFoundException("Can not locate agent jar file.");
    }

}
复制代码

通过类加载器 AppClassLoader 加载 AgentPackagePath.class 资源,定位到 skywalking-agent.jar 所在的目录,保存到静态成员变量 AGENT_PACKAGE_PATH 中,下次获取直接读取静态变量。

配置优先级

Agent Options > System.Properties(-D) > System environment variables > Config file

System.getProperties() 和 System.getenv() 区别,请参考文章 www.cnblogs.com/clarke157/p…

  • System.getProperties() 获取 Java 虚拟机相关的系统属性(比如 java.version、 java.io.tmpdir 等),通过 java -D配置;
  • System.getenv() 获取系统环境变量(比如 JAVA_HOME、Path 等),通过操作系统配置。

将配置信息映射到 Config 类

在我们的日常开发中一般是直接从 Properties 读取需要的配置项,SkyWalking Java Agent 并没有这么做,而是定义一个配置类 Config,将配置项映射到 Config 类的静态属性中,其他地方需要配置项的时候,直接从类的静态属性获取就可以了,非常方便使用。

ConfigInitializer 就是负责将 Properties 中的 key/value 键值对映射到类(比如 Config 类)的静态属性,其中 key 对应类的静态属性,value 赋值给静态属性的值。

/**
 * This is the core config in sniffer agent.
 */
public class Config {

    public static class Agent {
        /**
         * Namespace isolates headers in cross process propagation. The HEADER name will be `HeaderName:Namespace`.
         */
        public static String NAMESPACE = "";

        /**
         * Service name is showed in skywalking-ui. Suggestion: set a unique name for each service, service instance
         * nodes share the same code
         */
        @Length(50)
        public static String SERVICE_NAME = "";
     
        // 省略部分代码....
    }
    
    public static class Collector {
        /**
         * Collector skywalking trace receiver service addresses.
         */
        public static String BACKEND_SERVICE = "";
        
        // 省略部分代码....
    }
    
    // 省略部分代码....

    public static class Logging {
        /**
         * Log file name.
         */
        public static String FILE_NAME = "skywalking-api.log";

        /**
         * Log files directory. Default is blank string, means, use "{theSkywalkingAgentJarDir}/logs  " to output logs.
         * {theSkywalkingAgentJarDir} is the directory where the skywalking agent jar file is located.
         * <p>
         * Ref to {@link WriterFactory#getLogWriter()}
         */
        public static String DIR = "";
    }

    // 省略部分代码....

}
复制代码

比如通过 agent.config 配置文件配置服务名称

# The service name in UI
agent.service_name=${SW_AGENT_NAME:Your_ApplicationName}
复制代码
  • agent 对应 Config 类的静态内部类 Agent ;
  • service_name 对应静态内部类 Agent 的静态属性 SERVICE_NAME。

SkyWalking Java Agent 在这里面使用了下划线而不是驼峰来命名配置项,将类的静态属性名称转换成下划线配置名称非常方便,直接转成小写就可以通过 Properties 获取对应的值了。

ConfigInitializer 类源码:

/**
 * Init a class's static fields by a {@link Properties}, including static fields and static inner classes.
 * <p>
 */
public class ConfigInitializer {
    public static void initialize(Properties properties, Class<?> rootConfigType) throws IllegalAccessException {
        initNextLevel(properties, rootConfigType, new ConfigDesc());
    }

    private static void initNextLevel(Properties properties, Class<?> recentConfigType,
                                      ConfigDesc parentDesc) throws IllegalArgumentException, IllegalAccessException {
        for (Field field : recentConfigType.getFields()) {
            if (Modifier.isPublic(field.getModifiers()) && Modifier.isStatic(field.getModifiers())) {
                String configKey = (parentDesc + "." + field.getName()).toLowerCase();
                Class<?> type = field.getType();

                if (type.equals(Map.class)) {
                    /*
                     * Map config format is, config_key[map_key]=map_value
                     * Such as plugin.opgroup.resttemplate.rule[abc]=/url/path
                     */
                    // Deduct two generic types of the map
                    ParameterizedType genericType = (ParameterizedType) field.getGenericType();
                    Type[] argumentTypes = genericType.getActualTypeArguments();

                    Type keyType = null;
                    Type valueType = null;
                    if (argumentTypes != null && argumentTypes.length == 2) {
                        // Get key type and value type of the map
                        keyType = argumentTypes[0];
                        valueType = argumentTypes[1];
                    }
                    Map map = (Map) field.get(null);
                    // Set the map from config key and properties
                    setForMapType(configKey, map, properties, keyType, valueType);
                } else {
                    /*
                     * Others typical field type
                     */
                    String value = properties.getProperty(configKey);
                    // Convert the value into real type
                    final Length lengthDefine = field.getAnnotation(Length.class);
                    if (lengthDefine != null) {
                        if (value != null && value.length() > lengthDefine.value()) {
                            value = value.substring(0, lengthDefine.value());
                        }
                    }
                    Object convertedValue = convertToTypicalType(type, value);
                    if (convertedValue != null) {
                        // 通过反射给静态属性设置值
                        field.set(null, convertedValue);
                    }
                }
            }
        }
        // recentConfigType.getClasses() 获取 public 的 classes 和 interfaces
        for (Class<?> innerConfiguration : recentConfigType.getClasses()) {
            // parentDesc 将类(接口)名入栈
            parentDesc.append(innerConfiguration.getSimpleName());
            // 递归调用
            initNextLevel(properties, innerConfiguration, parentDesc);
            // parentDesc 将类(接口)名出栈
            parentDesc.removeLastDesc();
        }
    }

    // 省略部分代码....
}

class ConfigDesc {
    private LinkedList<String> descs = new LinkedList<>();

    void append(String currentDesc) {
        if (StringUtil.isNotEmpty(currentDesc)) {
            descs.addLast(currentDesc);
        }
    }

    void removeLastDesc() {
        descs.removeLast();
    }

    @Override
    public String toString() {
        return String.join(".", descs);
    }
}

复制代码

ConfigInitializer.initNextLevel 方法涉及到的技术点有反射、递归调用、栈等。

笔者在阅读源码过程对部分代码添加的注释已经提交到 GitHub 上了,具体参见 github.com/geekymv/sky…

更多精彩内容请关注公众号 geekymv,喜欢请分享给更多的朋友哦」如有问题,欢迎交流。

猜你喜欢

转载自juejin.im/post/7050487296243531784