PDF书籍《手写调用链监控APM系统-Java版》第3章 配置文件系统的建立

本人阅读了 Skywalking 的大部分核心代码,也了解了相关的文献,对此深有感悟,特此借助巨人的思想自己手动用JAVA语言实现了一个 "调用链监控APM" 系统。本书采用边讲解实现原理边编写代码的方式,看本书时一定要跟着敲代码。

作者已经将过程写成一部书籍,奈何没有钱发表,如果您知道渠道可以联系本人。一定重谢。

本书涉及到的核心技术与思想

JavaAgent , ByteBuddy,SPI服务,类加载器的命名空间,增强JDK类,kafka,插件思想,切面,链路栈等等。实际上远不止这么多,差不多贯通了整个java体系。

适用人群

自己公司要实现自己的调用链的;写架构的;深入java编程的;阅读Skywalking源码的;

版权

本书是作者呕心沥血亲自编写的代码,不经同意切勿拿出去商用,否则会追究其责任。

书籍目录和原版PDF+源码请见:

PDF书籍《手写调用链监控APM系统-Java版》第1章 开篇介绍-CSDN博客

第2章 配置文件系统的建立

配置文件对任何一个系统来说都是必不可少的,分为静态配置和动态配置两种。动态配置典型的就是nacos的配置中心,可以做到在服务端配置中心修改一个配置项,然后各个客户端微服务都会感知,然后自动修改各自jvm进程里面的配置。 当然这需要进行网络交互,我们重点不在于此。

我们就采用静态配置方式,设计一个本地的配置文件,项目启动时就会加载文件,然后读取解析文件,将值设置到jvm进程的程序中。配置文件值修改了,需要重启JVM。

本书设计方案是扫描项目环境下 hadluo-agent.config 文件,此文件类似属性文件,格式如下:

# kafka集群的地址
Bootstrap.servers = 127.0.0.1
# https://zhida.zhihu.com/search?content_id=251890693&content_type=Article&match_order=3&q=kafka&zhida_source=entity的topic
Bootstrap.topic = topic_apm
# 服务名称
#Agent.serviceName =
# 实例名
#Agent.serviceInstance =

等号前面的对应的是模块配置部分,后面的是值。

模块配置又分为两块,点号前面的是哪个模块的配置,点号后面的是模块的具体配置项。

我们将上面的配置文件进行抽象,可以得到对应的配置类:

public class Config {
    public static class Agent {
        public static String serviceName ;
        public static String serviceInstance ;
    }
    public static class Bootstrap {
        public static String servers ="127.0.0.1";
        public static String topic = "hadluo-apm-topic";
    }
}

上面配置类指定了2个模块,Agent和Bootstrap,Agent模块又包含serviceName和serviceInstance配置项。

2.1 配置文件的加载

我们开始实现配置代码,首先在apm-commons项目下新建类:

com.hadluo.apm.commons.Config

public class Config {
    public static class Agent {
        //agent的服务名称
        public static String serviceName ;
        // 实例名
        public static String serviceInstance ;
    }
    public static class Bootstrap {
        // 后端OAP kafka地址
        public static String servers;
        // kafka topic
        public static String topic;
    }
}

暂时我们先想到的是这几个配置,后续我们会不断增加。

然后需要实现读取解析配置文件并映射到Config类的逻辑,在apm-agent-core模块里面新建类:

com.hadluo.apm.agentcore.config.SnifferConfigInitializer

public class SnifferConfigInitializer {
    public static void initializeCoreConfig(String agentOptions) throws Exception {
        // 设置 应用名
        injectConfig("Agent.serviceName",agentOptions) ;
        // 设置 serviceInstance
        injectConfig("Agent.serviceInstance", UUID.randomUUID().toString().replaceAll("-", "") + "@" + OSUtil.getIPV4()) ;
        // 读取环境下面的 hadluo-agent.config 文件
        File confFile = ResourceFinder.findFile("hadluo-agent.config").get(0);
        Files.lines(Paths.get(confFile.getAbsolutePath())).forEach(line -> {
            // 遍历每一行文件内容
            line = line.trim();
            // 如果是 # 开头的 都是 注释
            if(line.startsWith("#") || line.isEmpty()){
                return ;
            }
            if(line.contains("#")){
                // 后面有注释需要截取掉
                line = line.substring(0,line.indexOf("#")) ;
            }
            if(!line.contains("=") || !line.contains(".")){
                // 没有 = 和 . 都是不合法的
                return ;
            }
            try {
                // 反射注入到装载类的静态字段里面
                injectConfig(line.split("=")[0].trim(),line.split("=")[1].trim()) ;
            } catch (Exception e) {
                Logs.err(SnifferConfigInitializer.class , "配置文件错误" , e);
            }
        }) ;

    }
    private static void injectConfig (String key , String value) throws Exception {
        String[] splits = key.split("\\.");
        // 获取模块类
        Class<?> moduelClass = Class.forName(Config.class.getName() + "$" + splits[0]) ;
        // 获取静态字段
        Field field = moduelClass.getField(splits[1]);
        // 设置值
        field.set(null , value);
    }
}

说明: ResourceFinder和OSUtil都是工具类。

以上代码不难,injectConfig 就是通过反射Config静态字段将值设置到字段上面。注意**内部类是用进行分隔的。**比如:com.hadluo.apm.commons.ConfigBootstrap

initializeCoreConfig逻辑先inject了serviceName和serviceInstance, 然后去解析配置文件,将每个配置都进行inject, 解析到#就是注释,这也符合properties文件的格式。这样如果配置文件设置了serviceName和serviceInstance也就优先配置文件的。

2.2 配置文件的测试

在premain方法中,添加下面代码进行调用和测试:

// 1. 初始化配置
try {
    SnifferConfigInitializer.initializeCoreConfig(args);
} catch (Exception e) {
    Logs.err(AgentMain.class , "初始化配置失败" , e);
    return ;
}
// 测试打印,后续要去掉
System.out.println("servers="+Config.Bootstrap.servers);
System.out.println("topic="+Config.Bootstrap.topic);
System.out.println("serviceInstance="+Config.Agent.serviceInstance);
System.out.println("serviceName="+Config.Agent.serviceName);

新建 hadluo-agent.config 配置文件放到resource目录下:

修改amp-agent-core的代码后,需要重新进行 package编译,生成jar,然后启动测试SpringBoot项目会打印出:

发现我们的配置值已经成功都写到了Config装载类中了。serviceInstance也已经成功注入,这些都为以后链路数据奠定了雄厚的基础。

serviceName的值是取得启动参数里面的,我们没有配置所以为空,理论上这个应该是自动获取到微服务的名称然后设置,获取微服务的名称需要用到插桩插件,我们后续讲解。

2.3 本章小结

本章主要讲解了如何设计一个简单的配置文件,并编写出了核心类SnifferConfigInitializer 去加载解压配置,相对较简单,真实的apm设计的配置是通过AOP后端可以动态修改的,类似与微服务里面的注册中心,由于配置文件不是本书的重点,所以这样简单设计了。

本章涉及的工具代码

com.hadluo.apm.commons.OSUtil

public class OSUtil {
    private static volatile String OS_NAME;
    private static volatile String HOST_NAME;
    private static volatile List<String> IPV4_LIST;
    private static volatile int PROCESS_NO = 0;
    public static String getOsName() {
        if (OS_NAME == null) {
            OS_NAME = System.getProperty("os.name");
        }
        return OS_NAME;
    }
    public static String getHostName() {
        if (HOST_NAME == null) {
            try {
                InetAddress host = InetAddress.getLocalHost();
                HOST_NAME = host.getHostName();
            } catch (UnknownHostException e) {
                HOST_NAME = "unknown";
            }
        }
        return HOST_NAME;
    }
    public static List<String> getAllIPV4() {
        if (IPV4_LIST == null) {
            IPV4_LIST = new LinkedList<>();
            try {
                Enumeration<NetworkInterface> interfs = NetworkInterface.getNetworkInterfaces();
                while (interfs.hasMoreElements()) {
                    NetworkInterface networkInterface = interfs.nextElement();
                    Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
                    while (inetAddresses.hasMoreElements()) {
                        InetAddress address = inetAddresses.nextElement();
                        if (address instanceof Inet4Address) {
                            String addressStr = address.getHostAddress();
                            if ("127.0.0.1".equals(addressStr)) {
                                continue;
                            } else if ("localhost".equals(addressStr)) {
                                continue;
                            }
                            IPV4_LIST.add(addressStr);
                        }
                    }
                }
            } catch (SocketException e) {

            }
        }
        return IPV4_LIST;
    }
    public static String getIPV4() {
        final List<String> allIPV4 = getAllIPV4();
        if (allIPV4.size() > 0) {
            return allIPV4.get(0);
        } else {
            return "no-hostname";
        }
    }
    public static int getProcessNo() {
        if (PROCESS_NO == 0) {
            try {
                PROCESS_NO = Integer.parseInt(ManagementFactory.getRuntimeMXBean().getName().split("@")[0]);
            } catch (Exception e) {
                PROCESS_NO = -1;
            }
        }
        return PROCESS_NO;
    }
}

com.hadluo.apm.commons.ResourceFinder

public class ResourceFinder {

    /***
     * 从所有环境中搜索文件,包括第三方jar
     * @param fileName
     * @return
     * @throws IOException
     */
    public static List<File> findFile(String fileName) throws IOException {
        // 所有classpath环境加载
        String DEFAULT_RESOURCE_PATTERN = "**/*.";
        String endPrifix = fileName.substring(fileName.lastIndexOf(".") + 1);
        DEFAULT_RESOURCE_PATTERN = DEFAULT_RESOURCE_PATTERN + endPrifix;
        ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
        String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
                + org.springframework.util.ClassUtils.convertClassNameToResourcePath(
                SystemPropertyUtils.resolvePlaceholders(""))
                + "/" + DEFAULT_RESOURCE_PATTERN;
        Resource[] resources = resourcePatternResolver.getResources(packageSearchPath);
        List<File> files = new ArrayList<File>();
        for (Resource resource : resources) {
            if (resource.getFilename().trim().equals(fileName.trim())) {
                try {
                    files.add(resource.getFile());
                } catch (Exception e) {
                }
                // 有可能是在jar里面
                try {
                    files.add(parserJar(resource, fileName));
                } catch (URISyntaxException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        return files;
    }

    private static File parserJar(Resource resource, String fileName) throws IOException, URISyntaxException {
        // 在linux spring打包后运行:/opt/neighbour-business-friends.jar!/BOOT-INF/lib/neighbour-agent-elasticsearch-starter-0.0.19.jar!/agent-client-0.0.1-SNAPSHOT-jar-with-dependencies.jar
        // 本地运行 : /F:/hadluo/code_src/hadluo-smart-apm/hadluo-smartapm-starter/target/classes/apm-agent-core-1.0-jar-with-dependencies.jar!/hadluo-apm/hadluo-apm-plugin.def
        // 区别就在于spring 会把运行的应该达成jar,多了这一层
        String jarPath = resource.toString().replace("URL [jar:file:", "").replace("]", "").trim();
        String[] fileItems = jarPath.split("!");
        String copyTempDir = null;
        String lastLevelJar = null;

        if (fileItems.length == 1) {
            if (new File(fileItems[0]).getName().equals(fileName)) {
                return new File(fileItems[0]);
            }
        }
        for (String fileItem : fileItems) {
            if (copyTempDir != null) {
                // 从 上一层的jar中 拷贝出 当前的文件 到 copyTempDir
                File currentFile = loadRecourseFromJarByFolder(lastLevelJar, copyTempDir, getName(fileItem));
                if (fileName.equals(currentFile.getName())) {
                    // 如果当前的文件就是我们要提取的 文件,直接返回了
                    return currentFile;
                }
                // 还要继续下一层 解析
                copyTempDir = currentFile.getParent();
                lastLevelJar = currentFile.getAbsolutePath();
            }

            if (fileItem.endsWith(".jar")) {
                // 需要从jar中拷贝出来
                copyTempDir = new File(fileItem).getParent();
                lastLevelJar = fileItem;
            }

        }
        return null;
    }

    private static String getName(String path) {
        return new File(path).getName();
    }

    /**
     * 提取jar包文件夹到指定文件
     *
     * @throws IOException
     */
    private static File loadRecourseFromJarByFolder(String jarFilePath, String destinationDirectory, String filter)
            throws IOException {
//		String jarFilePath = "path/to/your.jar"; // JAR文件的路径
//		String destinationDirectory = "path/to/destination/directory"; // 目标文件夹的路径
        FileInputStream fis = new FileInputStream(jarFilePath);
        BufferedInputStream bis = new BufferedInputStream(fis);
        JarInputStream jis = new JarInputStream(bis);
        try {
            JarEntry entry;
            while ((entry = jis.getNextJarEntry()) != null) {
                if (!entry.isDirectory()) {
                    String fileName = entry.getName();
                    if (!fileName.contains(filter)) {
                        continue;
                    }

                    File outputFile = new File(destinationDirectory, fileName);
                    File parentDir = outputFile.getParentFile();
                    if (parentDir != null && !parentDir.exists()) {
                        parentDir.mkdirs();
                    }
                    FileOutputStream fos = new FileOutputStream(outputFile);
                    BufferedOutputStream bos = new BufferedOutputStream(fos);
                    try{
                        byte[] buffer = new byte[4096];
                        int bytesRead;
                        while ((bytesRead = jis.read(buffer)) != -1) {
                            bos.write(buffer, 0, bytesRead);
                        }
                        return outputFile;
                    }finally {
                        bos.close();
                        fos.close();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}
相关推荐
言之。几秒前
Redis 集群方案
java·数据库·redis
后端转全栈_小伵3 分钟前
从 Coding (Jenkinsfile) 到 Docker:全流程自动化部署 Spring Boot 实战指南(简化篇)
java·spring boot·后端·docker·自动化·集成学习
JasonYin~15 分钟前
HarmonyOS NEXT 实战之元服务:静态案例效果---音乐排行榜
java·华为·harmonyos
m0_7482517232 分钟前
Spring Boot——统一功能处理
java·spring boot·后端
love静思冥想32 分钟前
Apache Commons Pool :介绍与使用
java·apache·线程池优化
xmh-sxh-13141 小时前
常用的前端框架有哪些
java
老马啸西风1 小时前
NLP 中文拼写检测纠正论文 A Hybrid Approach to Automatic Corpus Generation 代码实现
java
小蒜学长1 小时前
基于Spring Boot的宠物领养系统的设计与实现(代码+数据库+LW)
java·前端·数据库·spring boot·后端·旅游·宠物
L.S.V.1 小时前
Java 溯本求源之基础(三十一)——泛型
java·开发语言
Redamancy_Xun1 小时前
开源软件兼容性可信量化分析
java·开发语言·程序人生·网络安全·测试用例·可信计算技术