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

news/2024/12/27 9:17:20/

本人阅读了 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/news/1558490.html

相关文章

【算法题解】Bindian 山丘信号问题(E. Bindian Signaling)

问题描述 在 Berland 古老的 Bindian 部落中&#xff0c;首都被 nn 座山丘围成一个圆环&#xff0c;每个山丘上都有一名守望者&#xff0c;日夜观察着周围的情况。 如果有危险&#xff0c;守望者可以在山丘上点燃篝火。两座山丘的守望者可以看到彼此的信号&#xff0c;条件是…

算法的学习笔记— 圆圈中最后剩下的数(牛客JZ62)

&#x1f3e0;个人主页&#xff1a;尘觉主页 文章目录 62. 圆圈中最后剩下的数题目链接题目描述解题思路Java 实现思考分析&#x1f604;总结 62. 圆圈中最后剩下的数 题目链接 NowCoder 题目描述 让小朋友们围成一个大圈。然后&#xff0c;随机指定一个数 m&#xff0c;让…

Mysql5.7配置主从实际操作记录

&#x1f440; 什么是MySQL主从配置 指一台服务器充当主数据库服务器&#xff0c;另一台或多台服务器充当从数据库服务器&#xff0c;主服务器中的数据自动复制到从服务器之中。 对于多级复制&#xff0c;数据库服务器即可充当主机&#xff0c;也可充当从机。MySQL主从复制的…

【面试系列】深入浅出 Spring

熟悉Spring&#xff0c;对IOC、AOP、Bean生命周期、循环依赖等有深入了解。 面试题整理 描述 Spring Context 初始化的流程介绍 Bean 的生命周期及作用域Bean 的构造方法、 PostConstruct注解、InitializingBean、init-method 的执行顺序&#xff1f;Spring 如何解决循环依赖&…

C语言从入门到放弃教程

C语言从入门到放弃 1. 介绍1.1 特点1.2 历史与发展1.3 应用领域 2. 安装2.1 编译器安装2.2 编辑器安装 3. 第一个程序1. 包含头文件2. 主函数定义3. 打印语句4. 返回值 4. 基础语法4.1 注释4.1.1 单行注释4.1.2 多行注释 4.2 关键字4.2.1 C语言标准4.2.2 C89/C90关键字&#xf…

【机器学习案列】车牌自动识别系统:基于YOLO11的高效实现

&#x1f9d1; 博主简介&#xff1a;曾任某智慧城市类企业算法总监&#xff0c;目前在美国市场的物流公司从事高级算法工程师一职&#xff0c;深耕人工智能领域&#xff0c;精通python数据挖掘、可视化、机器学习等&#xff0c;发表过AI相关的专利并多次在AI类比赛中获奖。CSDN…

Metricbeat安装教程——Linux——Metricbeat监控ES集群

Metricbeat安装教程——Linux 一、安装 下载安装包&#xff1a; 官网下载地址&#xff1a;https://www.elastic.co/cn/downloads/beats/metricbeat 上传包到linux 切换到安装目录下 解压&#xff1a;tar -zxvf metricbeat-7.17.1-linux-x86_64.tar.gz 重命名安装文件夹 mv met…

【Compose multiplatform教程07】多平台常用组件和重要组件目录

一、基础交互与显示组件 Text 查看示例 功能说明&#xff1a;用于在界面上显示文本内容&#xff0c;支持设置字体、大小、颜色、样式&#xff08;如加粗、斜体、下划线&#xff09;等属性&#xff0c;满足不同的文本展示需求&#xff0c;可传达各种信息给用户。示例场景&#…