自己动手写一个 Arthas 在线诊断工具系列说明

news2024/12/28 16:14:09

相关文章:

  • 自己动手写一个分库分表中间件(十)线上优化之数据库连接超时优化
  • 自己动手写分布式任务调度框架
  • 自己动手写 Java 虚拟机(二)-查找 Class 文件
  • 自己动手调试 JDK(CLion)
  • Java Agent 的简单使用

“自己动手”系列说明

在软件开发的世界中,框架无疑是我们最好的朋友。它们帮助我们抽象复杂性,提供了一种结构化的方式来组织我们的代码,并且通常包含了大量的实用功能,使我们能够更快地开发出高质量的软件。然而,对于许多开发者来说,框架往往是一个黑盒,我们使用它,但往往对其内部的工作原理知之甚少。

系统性地完整学习一个框架是一项耗时且耗力的任务,而且学习的知识很容易被忘记,也很容易走进死胡同。这是因为许多框架经过长时间的迭代,其内部会有各种封装和抽象,这使得开发人员很难快速理解框架的核心逻辑。

在自己动手系列中,将从零开始,实现一些最流行的 Java 框架的核心功能。我的目标是抓大放小,快速理解框架的设计原理和工作机制,从而对框架有一个基本的认知

通过这种方式,可以为后续更深入地理解这些框架,理解它们为什么会这样设计,以及它们是如何解决复杂问题的学习打下基础。这将帮助我们更好地使用这些框架,也会提高我们的编程技能,使我们能够更好地设计和实现我们自己的代码。

本期“自己动手系列”是实现一个非常 Demo 级别的类 Arthas 诊断工具。可以实现类似于 Arthas watch 命令的功能。

效果

先看一下效果,我先启动一个 Spring Boot 工程,其中有一个 Controller:

/**
 * @author dongguabai
 * @date 2024-01-14 23:22
 */
@RestController
public class TestController {

    @GetMapping("/test")
    public String test(@RequestParam("id") String id) {
        return new Date().toLocaleString() + "->" + id;
    }
}

然后启动自己实现的 Arthas 工具,输入想要诊断的 Spring Boot 应用的进程 ID:

选择 Spring Boot 工程的进程号:

Currently running Java processes:
5600 App
5648 Launcher
5649 Console
5651 Jps
87382 RemoteMavenServer
760 
11934 org.eclipse.equinox.launcher_1.6.700.v20231214-2017.jar
5599 Launcher
> 5600
Attach success
Attach success. Please input the class and method (format: com.example.MyClass#myMethod):
>

再输入想要诊断的函数:

Currently running Java processes:
5600 App
5648 Launcher
5649 Console
5651 Jps
87382 RemoteMavenServer
760 
11934 org.eclipse.equinox.launcher_1.6.700.v20231214-2017.jar
5599 Launcher
> 5600
Attach success
Attach success. Please input the class and method (format: com.example.MyClass#myMethod):
> dongguabai.spring.boot.demo.sb1.TestController#test
Listening on port 56494
Agent applied to dongguabai.spring.boot.demo.sb1.TestController#test
> 

然后调用 dongguabai.spring.boot.demo.sb1.TestController#test 函数:

➜  lib curl http://localhost:8080/test\?id\=AS123
2024-1-15 16:26:03->AS123%                                                                                              ➜  lib curl http://localhost:8080/test\?id\=AS123
2024-1-15 16:27:06->AS123%                                                                                              ➜  lib curl http://localhost:8080/test\?id\=AS123
2024-1-15 16:27:07->AS123%  

再观察诊断工具的控制台:

Currently running Java processes:
5600 App
5648 Launcher
5649 Console
5651 Jps
87382 RemoteMavenServer
760 
11934 org.eclipse.equinox.launcher_1.6.700.v20231214-2017.jar
5599 Launcher
> 5600
Attach success
Attach success. Please input the class and method (format: com.example.MyClass#myMethod):
> dongguabai.spring.boot.demo.sb1.TestController#test
Listening on port 56494
Agent applied to dongguabai.spring.boot.demo.sb1.TestController#test
> Arguments: [AS123]
Return: 2024-1-15 16:26:03->AS123
Arguments: [AS123]
Return: 2024-1-15 16:27:06->AS123
Arguments: [AS123]
Return: 2024-1-15 16:27:07->AS123

可以看到打印出了诊断函数的请求参数和响应参数。

实现原理

控制台

首先需要给用户提供一个交互式的控制台,可以基于 JLine 实现,先看一个简单的 Demo:

public class JLineDemo {

    public static void main(String[] args) throws Exception {
        Terminal terminal = TerminalBuilder.builder().system(true).build();
        LineReader lineReader = LineReaderBuilder.builder().terminal(terminal).build();

        String line;
        while (true) {
            line = lineReader.readLine("> ");
            System.out.println("Input: " + line);
            if ("quit".equalsIgnoreCase(line)) {
                break;
            }
        }
    }
}

运行后会进入一个控制台,可以进行交互:

> 1
Input: 1
> daad
Input: daad
> 

获取正在运行的 JVM 进程

Arthas 启动后会看到当前正在运行的 JVM 进程,可以基于 jps 命令去做这里就涉及到两点:

  1. Java 执行 jps 命令
  2. 解析命令结果

这也不难,直接使用 Runtime API 即可,再与上面的控制台结合起来:

public class ConsoleDemo {
    public static void main(String[] args) {
        try {
            Terminal terminal = TerminalBuilder.terminal();
            LineReader reader = LineReaderBuilder.builder().terminal(terminal).build();
            Set<String> pids = getRunningJavaProcesses();
            interactWithUser(reader, pids);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static Set<String> getRunningJavaProcesses() throws Exception {
        Set<String> pids = new HashSet<>();
        Process process = Runtime.getRuntime().exec("jps");
        try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            String line;
            System.out.println("Currently running Java processes:");
            while ((line = bufferedReader.readLine()) != null) {
                System.out.println(line);
                String pid = line.split(" ")[0];
                pids.add(pid);
            }
        }
        return pids;
    }

    private static void interactWithUser(LineReader reader, Set<String> pids) {
        while (true) {
            String input = reader.readLine("> ");
            if ("quit".equals(input)) {
                break;
            } else {
                System.out.println("Input:" + input);
            }
        }
    }
}

运行:

Currently running Java processes:
5600 App
87382 RemoteMavenServer
760 
8553 Launcher
8554 ConsoleDemo
8558 Jps
11934 org.eclipse.equinox.launcher_1.6.700.v20231214-2017.jar
5599 Launcher
> 1
Input:1
> 222
Input:222
> 

Java Agent

在Java Agent 的简单使用中介绍过,Java Agent 有两种启动场景,JVM 启动时和运行时候,而目前的场景显然就是在运行时候动态加载 Java Agent。

Attach

可以基于 VirtualMachine 来实现:

private static void attachToProcess(String pid) throws Exception {
    VirtualMachine vm = VirtualMachine.attach(pid);
    System.out.println("Attach success");
    vm.detach();
}

Java Agent

因为是在 JVM 运行时被调用,所以这里要使用 agentmain 函数,同时需要在目标函数执行前后打印出请求参数和响应参数:

public class Agent {

    public static void agentmain(String agentArgs, Instrumentation inst) {
        //initializeAgent
    }
    
    private static void initializeAgent(String agentArgs, final Instrumentation inst) throws Exception {
        String[] args = agentArgs.split("#");
        final String className = args[0];
        final String methodName = args[1];
        System.out.println("className:" + className);
        System.out.println("methodName:" + methodName);
        Class[] allLoadedClasses = inst.getAllLoadedClasses();
        System.out.println(allLoadedClasses.length);

        inst.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className1, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                if (!className1.replace("/", ".").equals(className)) {
                    return null;
                }

                try {
                    ClassPool cp = ClassPool.getDefault();
                    CtClass cc = cp.get(className1.replace("/", "."));
                    CtMethod m = cc.getDeclaredMethod(methodName);
                    System.out.println("Transforming class: " + cc.getName());
                    System.out.println("Transforming method: " + m.getName());
                    m.insertBefore("System.out.println(\"Arguments: \" + java.util.Arrays.toString($args));");
                    m.insertAfter("System.out.println(\"Return: \" + $_);");
                    return cc.toBytecode();
                } catch (Exception e) {
                    System.out.println("Failed to transform class: " + className1);
                    e.printStackTrace();
                    return null;
                }
            }
        }, true);

        for (Class<?> clazz : allLoadedClasses) {
            System.out.println("Loaded class: " + clazz.getName());
            if (clazz.getName().equals(className)) {
                System.out.println("Retransforming class: " + clazz.getName());
                inst.retransformClasses(clazz);
            }
        }
    }
}

pom.xml 如下:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-jar-plugin</artifactId>
  <version>3.2.0</version>
  <configuration>
    <archive>
      <manifest>
        <addClasspath>true</addClasspath>
        <mainClass>blog.dongguabai.arthas.Agent</mainClass>
        <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
      </manifest>
      <manifestEntries>
        <Agent-Class>blog.dongguabai.arthas.Agent</Agent-Class>
        <Can-Retransform-Classes>true</Can-Retransform-Classes>
      </manifestEntries>
    </archive>
  </configuration>
</plugin>

IPC 机制

这里有个细节,控制台(也就是 Arthas.jar)是一个独立的 JVM 进程,Java Agent 从目标 JVM 进程采集到数据后如何返回给控制台呢。其实也很简单,通过 Socket 通信即可。

Agent 直接通过 Socket 发送数据给控制台:

 m.insertBefore("{ java.net.Socket socket = new java.net.Socket(\"localhost\", " + port + "); " +
                        "java.io.PrintWriter out = new java.io.PrintWriter(socket.getOutputStream(), true); " +
                        "out.println(\"Arguments: \" + java.util.Arrays.toString($args)); " +
                        "out.close(); socket.close(); }");
                m.insertAfter("{ java.net.Socket socket = new java.net.Socket(\"localhost\", " + port + "); " +
                        "java.io.PrintWriter out = new java.io.PrintWriter(socket.getOutputStream(), true); " +
                        "out.println(\"Return: \" + $_); " +
                        "out.close(); socket.close(); }");

控制台接收 Socket 数据并且打印:

    private static void applyAgent(String pid, String className, String methodName) throws Exception {
        VirtualMachine vm = VirtualMachine.attach(pid);
        ServerSocket serverSocket = new ServerSocket(0);
        int port = serverSocket.getLocalPort();
        new Thread(() -> {
            System.out.println("Listening on port " + port);
            while (true) {
                try (Socket clientSocket = serverSocket.accept();
                     BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
                    String inputLine;
                    while ((inputLine = in.readLine()) != null) {
                        System.out.println(inputLine);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        ...
    }

总结

本文实现一个非常 Demo 级别的类 Arthas 诊断工具,其中涉及到了 JLine、Java Agent、ClassLoader、Javassist、Socket 等技术。

尽管工具还很简单,功能也很有限,但它为理解和学习如 Arthas 这样的复杂诊断工具提供了一个实践入口。后续也会继续扩展这个工具,添加更多的功能,如更详细的方法追踪,更丰富的 JVM 信息获取,甚至是内存和 CPU 的监控等。
源码地址:https://gitee.com/dongguabai/blog/tree/master/dongguabai-arthas

欢迎关注公众号:
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1386786.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux网络文件共享服务之FTP协议

目录 一、存储类型 1、直连式存储&#xff08;DAS&#xff09; 2、存储区域网络&#xff08;SAN&#xff09; 3、网络附加存储&#xff08;NAS&#xff09; 二、 FTP文件传输协议 1、FTP协议的工作原理 1.1 FTP协议的工作流程 1.2 FTP协议的两种工作模式 1.2.1 主动模…

6314A/B/C 稳定光源

01 6314A/B/C 稳定光源 产品综述&#xff1a; 6314系列稳定光源包括6314A稳定光源(1310NM单波长)、6314B稳定光源(1550NM单波长)、6314C稳定光源(1310NM &1550NM双波长)。6314系列稳定光源采用高精度自动功率控制技术和自动温度控制技术。6314系列稳定光源配备多种模块&…

Mysql中设置只允许指定ip能连接访问(可视化工具的方式)

场景 Mysql中怎样设置指定ip远程访问连接&#xff1a; Mysql中怎样设置指定ip远程访问连接_navicat for mysql 设置只有某个ip可以远程链接-CSDN博客 前面设置root账户指定ip能连接访问是通过命令行的方式&#xff0c;如果通过可视化工具比如Navicat来实现。 注&#xff1a…

二叉树题目:从前序与后序遍历序列构造二叉树

文章目录 题目标题和出处难度题目描述要求示例数据范围 前言解法一思路和算法代码复杂度分析 解法二思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;从前序与后序遍历序列构造二叉树 出处&#xff1a;889. 从前序与后序遍历序列构造二叉树 难度 7 级 题目描述…

7_1 tesseract 安装及使用

1、 安装tesseract   OCR&#xff0c;即Optical Character Recognition&#xff0c;光学字符识别&#xff0c;是指通过扫描字符&#xff0c;然后通过其形状将其翻译成电子文本的过程。对于图形验证码来说&#xff0c;它们都是一些不规则的字符&#xff0c;这些字符确实是由字…

运筹说 第99期 | 非线性规划—最优性条件

通过上期学习&#xff0c;大家已经了解了非线性规划中无约束极值问题及其求解方法。本期小编将为大家介绍最优性条件&#xff0c;包括可行下降方向、库恩-塔克条件等内容。 1 可行下降方向 起约束作用 假定X(0)是上述问题的一个可行解&#xff0c;满足所有约束。对某约束条件g…

2024年,如何打造惊艳的个人博客/出版系统并且赚点小钱?

几年前&#xff0c;我就推荐过用Markdown写作静态博客。静态博客几乎是零托管成本&#xff0c;比较适合个人博客起步。Markdown便于本地搜索&#xff0c;也可当作是个人知识库方案。 现在有了新的进展。我不仅构建了一个视觉上相当不错的个人网站&#xff0c;还美化了github、…

Linux/Frolic

Enumeration nmap 还是扫描系统对外开放的端口情况&#xff0c;对外开放了22,139,445,还有9999端口&#xff0c;显示是http服务&#xff0c;使用了nginx 1.10.3 ┌──(kali㉿kali)-[~/HTB/Frolic] └─$ nmap -sC -sV -oA nmap -Pn 10.10.10.111 Starting Nmap 7.93 ( http…

pandas进行数据计算时如何处理空值的问题?

目录 1.数据预览&#xff1a; 2.解决方法 &#xff08;1&#xff09;问题示例 &#xff08;2&#xff09;方法 A.方法一 B.方法二 1.数据预览&#xff1a; 2.解决方法 &#xff08;1&#xff09;问题示例 如下图如果不理睬这些空值的话&#xff0c;计算总分便也会是空值…

uni-app购物车页面详细代码

效果图&#xff1a; 这里的购物车加减用的是uni-app中的sku插件 代码附下&#xff08;全&#xff09;&#xff1a; <script setup lang"ts"> import {reqMemberCartList,reqMemberdelentCart,reqMemberPutCart,putMemberCartSelectedAPI, } from /services/…

linux后台进程的总结

文章目录 方案1 nohup &方案2 screen 方案1 nohup & 1、单独使用 nohup 执行脚本&#xff0c;如下图所示&#xff0c;终端会被接管&#xff0c;就是标准输入stdin 被关闭了&#xff0c;使用ctrlc会导致终止执行&#xff0c;但是可以关闭这个终端&#xff0c;重新打开终…

c5060:out can‘t be used with used with non-varying visibility

openGL系列文章目录 文章目录 openGL系列文章目录前言一、GLSL language integration是什么&#xff1f;二、GLSL language integration配置二、GLSL language integration编译报错解决 前言 GLSL插件下载和安装&#xff1a;GLSL language integration下载地址 你也可以在visu…

【python可视化大屏】使用python实现可拖拽数据可视化大屏

介绍&#xff1a; 我在前几期分享了关于爬取weibo评论的爬虫&#xff0c;同时也分享了如何去进行数据可视化的操作。但是之前的可视化都是单独的&#xff0c;没有办法在一个界面上展示的。这样一来呢&#xff0c;大家在看的时候其实是很不方便的&#xff0c;就是没有办法一目了…

软件测试|教你使用dataclass

前言 当我们需要在Python中定义一种简单的数据容器类时&#xff0c;dataclass是一个非常有用的工具。它允许我们轻松地创建具有一些自动化特性的类&#xff0c;例如自动生成__init__()、__repr__()和__eq__()等方法。本文将详细介绍dataclass的使用&#xff0c;并提供示例来说…

Qt点击按钮在其附近弹出一个窗口

效果 FS_PopupWidget.h #ifndef FS_POPUPWIDGET_H #define FS_POPUPWIDGET_H#pragma once#include <QToolButton> #include <QWidgetAction> #include <QPointer>class QMenu;class FS_PopupWidget : public QToolButton {Q_OBJECTpublic:FS_PopupWidget(QW…

3dmax有哪些技巧?3damx不为人知的秘密

在装修设计和建筑可视化领域&#xff0c;3D MAX是一款强大的工具&#xff0c;可以帮助我们创建出高质量的3D模型和动画。然而&#xff0c;要充分发挥其潜力&#xff0c;我们需要掌握一些3D MAX渲染的技巧。以下是一些实用的技巧&#xff0c;帮助你提高渲染效率和质量。 合理设…

【总结】Dinky学习笔记

概述 Dinky 是一个开箱即用、易扩展&#xff0c;以 Apache Flink 为基础&#xff0c;连接 OLAP 和数据湖等众多框架的一站式实时计算平台&#xff0c;致力于流批一体和湖仓一体的探索与实践 官网&#xff1a;Dinky 核心特性 沉浸式&#xff1a;提供专业的 DataStudio 功能&a…

mysql8 源码编译 客户端连接运行 报段异常解决

mysql8 源码编译 客户端连接运行 报段异常解决。解决方案&#xff1a;删除之前编译的文件。先安装libncurses-dev依赖&#xff0c;在重新编译。原因&#xff1a;第一次编译没有libncurses-dev依赖&#xff0c;编译告警&#xff0c;再次编译有缓存&#xff0c;没有引入声明头文件…

Umi3 创建,配置环境,路由传参(代码示例)

目录 创建项目 配置环境 创建脚手架 项目结构及其目录、 路由 配置路由 嵌套路由 编程式导航和声明式导航 声明式导航 编程式导航 约定式路由 路由传参 query传参&#xff08;问号&#xff09; 接收参数 params传参&#xff08;动态传参&#xff09; 接收参数 创…

[zabbix] zabbix监控

一、温习zabbix自定义监控 二、zabbix 自动发现与自动注册 2.1 zabbix 自动发现 //zabbix 自动发现&#xff08;对于 agent2 是被动模式&#xff09; zabbix server 主动的去发现所有的客户端&#xff0c;然后将客户端的信息登记在服务端上。 缺点是如果定义的网段中的主机数…