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

embedded/2024/12/28 4:12:55/

本人阅读了 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
# kafka的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 topicpublic 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) ;// 设置 serviceInstanceinjectConfig("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.Config$Bootstrap

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中 拷贝出 当前的文件 到 copyTempDirFile 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;}
}

http://www.ppmy.cn/embedded/149354.html

相关文章

框架专题:设计模式

1. 什么是设计模式&#xff1f; 设计模式是一套经过验证的解决方案&#xff0c;用于解决软件开发中的常见问题。它不是具体的代码实现&#xff0c;而是一种可复用的思想和原则。设计模式的核心目标是提高代码的复用性、可扩展性和可维护性。 1.1 设计模式的三大特性 可重用性…

【C++boost::asio网络编程】有关服务端退出方法的笔记

有关服务端退出方法的笔记 C风格的信号关闭boost::asio中的关闭方式 原来服务端的main函数如下 int main() {try{boost::asio::io_context ioc;Server s(ioc, 8888);ioc.run();}catch (const std::exception&){}return 0; }上面弊端在于缺乏好的退出机制&#xff0c;目前&a…

Android okhttp 网络链接各阶段监控

步骤 1: 添加依赖 在项目的 build.gradle 文件中&#xff0c;添加 OkHttp 依赖&#xff1a; implementation com.squareup.okhttp3:okhttp:4.11.0 步骤 2: 创建自定义的 EventListener 创建一个自定义的 EventListener 类&#xff1a; import android.util.Log import okht…

【漏洞复现】灵当CRM datapdf.php 任意文件读取漏洞

免责声明 请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,作者不为此承担任何责任。工具来自网络,安全性自测,如有侵权请联系删除。本次测试仅供学习使用,如若非法他用,与平台和本文作…

Kafka、RocketMQ、RabbitMQ 对比

面试中对 Kafka 、 RocketMQ 、和 RabbitMQ 的对比是常见问题&#xff0c;可以从以下几个维度进行分析&#xff1a; 1️⃣ 基础概念 特性KafkaRocketMQRabbitMQ开发语言Java ScalaJavaErlang定位分布式流处理平台分布式消息中间件高效、可靠的消息队列消息模型Topic &#xf…

信息抽取(NLP)是什么技术有哪些应用?

信息抽取是将非结构化的信息转化为结构化信息的过程。一般应用于电商平均分析、知识图谱和大模型训练。 不同模型的对比 1. 规则模型 优点 简单直观:基于人工设定的规则,不需要大量的数据集进行训练,只要规则制定者对目标信息有清晰的理解即可开始构建。例如,对于一些具有…

taro中实现带有途径点的路径规划

前言 taro中实现带有途径点的路径规划 import React, {useState, useEffect} from "react"; import {View, Map, ScrollView} from tarojs/components import Taro, {useRouter} from tarojs/taro; import request from ../../request; import api from ../../reque…

Spring Boot 中 Map 的最佳实践

在Spring Boot中使用Map时&#xff0c;请遵循以下最佳实践: 1.避免在Controller中 直接使用Map。应该使用RequestBody 接收-个DTO对象或者 RequestParam接收参数&#xff0c;然后在Service中处 理Map。 2.避免在Service中 直接使用原始的Map。应该使用Autowired 注入-个专门…