赵宇博的技术博客 赵宇博的技术博客
首页
前端
后端
数据库专栏
k8s专栏
分布式专栏
Linux网络专栏
手写系列专栏
随笔
关于
GitHub (opens new window)
首页
前端
后端
数据库专栏
k8s专栏
分布式专栏
Linux网络专栏
手写系列专栏
随笔
关于
GitHub (opens new window)
  • JVM专题

    • JVM概述
    • JMM(Java内存模型)
    • Javassist字节码工具开发记录
      • 1、JavaAgent
        • 1.1、动态Attach到目标进程
        • 1.2、开启轻量服务器
      • 2、增加字节码反编译功能
        • 2.1、生成代码目录树
        • 2.2、反编译字节码
        • 2.3、结果展示
      • 3、增加单个方法的Watch功能
        • 3.1、UI界面选择方法
        • 3.2、增加按钮,触发Watch
      • 4、Watch支持多方法同时监控
    • Agent 技术
    • GC问题排查思路
  • 源码专题

  • Activi6专题

  • 杂谈

  • 后端
  • JVM专题
zhaoyb
2024-01-17
目录

Javassist字节码工具开发记录

# Javassist字节码工具开发记录

# 1、JavaAgent

打算趁着空闲时间开发一个小工具,用来做线上字节码调试,仿照阿里的Arthas,做一个带有可视化界面的工具

# 1.1、动态Attach到目标进程

1、首先新建一个Maven项目[ ByteCodeTool ],pom文件如下所示:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>org.example</groupId>
  <artifactId>ByteCodeTool</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>ByteCodeTool</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.28.0-GA</version>
    </dependency>
    <dependency>
      <groupId>net.bytebuddy</groupId>
      <artifactId>byte-buddy-agent</artifactId>
      <version>1.12.14</version>
    </dependency>
    <dependency>
      <groupId>com.sun.tools</groupId>
      <artifactId>attach</artifactId>
      <version>8</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>8</source>
          <target>8</target>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-shade-plugin</artifactId>
        <version>3.5.0</version>
        <configuration>
          <transformers>
            <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
              <manifestEntries>
                <Main-Class>org.example.AttachAgent</Main-Class>
                <Agent-Class>org.example.MyAgent</Agent-Class>
                <Can-Retransform-Classes>true</Can-Retransform-Classes>
              </manifestEntries>
            </transformer>
          </transformers>
        </configuration>
        <executions>
          <execution>
            <phase>package</phase>
            <goals>
              <goal>shade</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>

</project>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71

2、新建MyAgent类

package org.example;

import javassist.*;

import java.lang.instrument.Instrumentation;

/**
 * @author zhaoyubo
 * @title MyAgent
 * @description 动态Attach
 * @create 2024/1/17 10:19
 **/
public class MyAgent {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        try {
            System.out.println("+++++++++++++++++++++Attach success+++++++++++++++++++++");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

3、新增AttachAgent启动类

package org.example;

import com.sun.tools.attach.VirtualMachine;
import com.sun.tools.attach.VirtualMachineDescriptor;

import java.net.URL;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.List;
import java.util.Scanner;

/**
 * @author zhaoyubo
 * @title AttachAgent
 * @description 参考Arthas动态获取java进程ID,动态Attach
 * @create 2024/1/17 10:20
 **/
public class AttachAgent {
    public static void main(String[] args) {
        MainConfig.mainPkg = args[0];
        // 获取当前运行的 Java 进程的 PID
        String pid = "";
        Scanner scanner = new Scanner(System.in);
        List<VirtualMachineDescriptor> jps = VirtualMachine.list();
        jps.sort(Comparator.comparing(VirtualMachineDescriptor::displayName));
        int i = 0;
        for (; i < jps.size(); i++) {
            System.out.printf("[%s] %s %s%n", i, jps.get(i).id(), jps.get(i).displayName());
        }
        System.out.printf("[%s] %s%n", i, "Custom PID");
        System.out.println(">>>>>>>>>>>>Please enter the serial number");
        while (true) {
            int index = scanner.nextInt();
            if (index < 0 || index > i) continue;
            if (index == i) {
                System.out.println(">>>>>>>>>>>>Please enter the PID");
                pid = String.valueOf(scanner.nextInt());
                break;
            }
            pid = jps.get(index).id();
            break;
        }
        try {
            // 附加代理到 Java 进程
            VirtualMachine vm = VirtualMachine.attach(pid);
            URL jarUrl = MyAgent.class.getProtectionDomain().getCodeSource().getLocation();
            String curJarPath = Paths.get(jarUrl.toURI()).toString();
            vm.loadAgent(curJarPath,MainConfig.mainPkg);
            System.out.println("*** Attach finish ***");
            MainFrame.out();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

4、启动,测试

工具进程窗口:
> java -jar ByteCodeTool-1.0-SNAPSHOT.jar
[0] 2992
[1] 3256 ByteCodeTool-1.0-SNAPSHOT.jar
[2] 43476 afs-service-1.0.1.jar
[3] 1764 org.example.MainFrame
[4] 18336 org.jetbrains.idea.maven.server.RemoteMavenServer36
[5] Custom PID
++++++++++++++Please enter the serial number+++++++++++++++
2
++++++++++++The PID is 43476++++++++++++
++++++++++++Attach finish++++++++++++

被Attach的进程窗口:
> +++++++++++++++++++++Attach success+++++++++++++++++++++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 1.2、开启轻量服务器

在Attach进程之后,需要开启一个轻量级的http服务器,这样就可以通过动态的传递参数来进行字节码修改,对于服务器选择了开源的**smart-http (opens new window)**,具体代码如下:

public class MyAgent {
    public static void agentmain(String agentArgs, Instrumentation inst) {
        try {
            // 保存全局配置
            MainConfig.inst = inst;
            MainConfig.mainPkg = agentArgs;
            // 开启http服务
            startHttp(agentArgs, inst.getAllLoadedClasses());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * @description 开启Http服务
     * @param agentArgs
     * @param allLoadedClasses
     * @return void
     * @author zhaoyubo
     * @time 2024/1/26 10:01
     **/
    public static void startHttp(String agentArgs, Class[] allLoadedClasses) {
        // 启动一个服务接口,供外部进行服务调用,进行动态字节码操作
        // 创建HttpServer服务器
        HttpBootstrap bootstrap = new HttpBootstrap();
        bootstrap.configuration().debug(true);
        // 1. 实例化路由Handle
        bootstrap.httpHandler(JadController.getAllPackage(allLoadedClasses, agentArgs));
        // 2. 启动服务
        bootstrap.configuration().bannerEnabled(false).debug(false);
        bootstrap.setPort(MainConfig.HTTP_PORT);
        bootstrap.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

客户端使用Swing构建一个操作界面,Swing使用了开源的**beautyeye (opens new window)**进行美化。

public class MainFrame {

    public static void out() {
        try{
            BeautyEyeLNFHelper.launchBeautyEyeLNF();
            UIManager.put("RootPane.setupButtonVisible", false);
            //实例化一个JFrame对象
            JFrame frame = new JFrame("ByteCodeTool");
            frame.setContentPane(new TabPane());
            frame.setVisible(true);
            frame.pack();//使窗体可视
            frame.setSize(1000, 850);//设置窗体显示位置和大小
        }catch(Exception e){
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 2、增加字节码反编译功能

# 2.1、生成代码目录树

目前的设计是想要做一个能展示线上源代码目录的Tree结构,然后通过双击源码文件名来展示反编译的java代码

Tree结构使用Swing来做,并且注册双击事件,得到源码class的文件名,最后通过Javassist来反编译得到java文件,在Swing页面做展示

首先增加一个动态的源码树节点,并增加叶子节点的双击事件,代码如下“:

public class JadPkgTree {

    public static JTree tree = new JTree();

    public static JTree of() throws InterruptedException {
        HttpClient httpClient = new HttpClient("127.0.0.1", MainConfig.HTTP_PORT);
        CountDownLatch cd = new CountDownLatch(1);
        httpClient.get("/all").onSuccess(response -> {
            try {
                ClassObj clazz = new ObjectMapper().readValue(response.body().getBytes(), ClassObj.class);
                MainConfig.classObj = clazz;
                DefaultMutableTreeNode jTreeRoot = buildTree(clazz.getClassName());
                tree = new JTree(jTreeRoot);
                tree.expandRow(1);
                tree.addMouseListener(new MouseAdapter() {
                    @Override
                    public void mouseClicked(MouseEvent e) {
                        // 如果在这棵树上点击了2次,即双击
                        if (e.getSource() == tree && e.getClickCount() == 2) {
                            // 按照鼠标点击的坐标点获取路径
                            TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
                            if (selPath != null)// 谨防空指针异常!双击空白处是会这样
                            {
                                // 获取这个路径上的最后一个组件,也就是双击的地方
                                DefaultMutableTreeNode node = (DefaultMutableTreeNode)selPath.getLastPathComponent();
                                // 调用反编译
                                String result = JadMain.decompile(clazz.getJarPath(), node.toString());
                                // 把这些反编译完成的代码展示到右侧的文本组件中
                                MainConfig.jadText.setText(result);
                            }
                        }

                    }
                });
                cd.countDown();
            } catch (Exception ex) {
                cd.countDown();
            }
        }).onFailure(Throwable::printStackTrace).done();
        // 等待调用完成再返回结果
        cd.await();
        return tree;
    }

    public static DefaultMutableTreeNode buildTree(List<String> classNames) {
        Node root = new Node("代码目录", 0);
        for (String className : classNames) {
            String[] parts = className.split("\\.");

            Node currentNode = root;
            for (String part : parts) {
                Node childNode = findChildNode(currentNode, part);

                if (childNode == null) {
                    int nodeType = (part.equals(parts[parts.length - 1])) ? 2 : 1;
                    childNode = new Node(part, nodeType);
                    currentNode.addChildNode(childNode);
                }

                currentNode = childNode;
            }
        }

        optimizeTree(root);
        return convertToJTree(root);
    }

    public static DefaultMutableTreeNode convertToJTree(Node node) {
        DefaultMutableTreeNode jTreeNode = new DefaultMutableTreeNode(node.nodeName);
        for (Node childNode : node.childNodes) {
            jTreeNode.add(convertToJTree(childNode));
        }
        return jTreeNode;
    }

    public static Node findChildNode(Node node, String nodeName) {
        for (Node childNode : node.childNodes) {
            if (childNode.nodeName.equals(nodeName)) {
                return childNode;
            }
        }
        return null;
    }

    public static void optimizeTree(Node node) {
        if (node.nodeType == 1 && node.pkgNum == 1 && node.fileNum == 0) {
            Node childNode = node.childNodes.get(0);
            optimizeTree(childNode);
            node.childNodes = childNode.childNodes;
            node.pkgNum = childNode.pkgNum;
            node.nodeName = node.nodeName + "." + childNode.nodeName;
            node.nodeType = childNode.nodeType;
        }

        for (Node childNode : node.childNodes) {
            optimizeTree(childNode);
        }
    }

    static class Node {
        String nodeName;
        List<Node> childNodes;
        public int nodeType; // 1 for package, 2 for file
        Integer totalNum = 0;
        Integer pkgNum = 0;
        Integer fileNum = 0;

        public Node(String nodeName, int nodeType) {
            this.nodeName = nodeName;
            this.nodeType = nodeType;
            this.childNodes = new ArrayList<>();
            this.fileNum = 0;
        }

        public void addChildNode(Node childNode) {
            this.childNodes.add(childNode);
            if (childNode.nodeType == 1) {
                this.pkgNum += childNode.pkgNum + 1; // 加上当前节点自身
            }
            if (childNode.nodeType == 2) {
                this.fileNum += childNode.fileNum + 1; // 加上当前节点自身
            }
            this.totalNum += childNode.totalNum + 1; // 加上当前节点自身
        }
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127

# 2.2、反编译字节码

使用开源的CFR包进行字节码反编译

<!-- CFR -->
<dependency>
    <groupId>org.benf</groupId>
    <artifactId>cfr</artifactId>
    <version>0.151</version>
</dependency>
1
2
3
4
5
6

编写主要代码:

public class JadMain {

    public static String decompile(String classFilePath, String methodName) {
        return decompile(classFilePath, methodName, false);
    }

    /**
     * @param classFilePath
     * @param className
     * @param hideUnicode
     * @return
     */
    public static String decompile(String classFilePath, String className, boolean hideUnicode) {
        final StringBuilder result = new StringBuilder(8192);

        OutputSinkFactory mySink = new OutputSinkFactory() {
            @Override
            public List<SinkClass> getSupportedSinks(SinkType sinkType, Collection<SinkClass> collection) {
                return Arrays.asList(SinkClass.STRING, SinkClass.DECOMPILED, SinkClass.DECOMPILED_MULTIVER,
                    SinkClass.EXCEPTION_MESSAGE);
            }

            @Override
            public <T> Sink<T> getSink(final SinkType sinkType, SinkClass sinkClass) {
                return sinkable -> {
                    if (sinkType == SinkType.PROGRESS) {
                        return;
                    }
                    result.append(sinkable);
                };
            }
        };

        HashMap<String, String> options = new HashMap<>();
        options.put("showversion", "false");
        options.put("hideutf", String.valueOf(hideUnicode));
        if (!StringUtils.isBlank(className)) {
            options.put("jarfilter", className);
        }

        CfrDriver driver = new CfrDriver.Builder().withOptions(options).withOutputSink(mySink).build();
        List<String> toAnalyse = new ArrayList<>();
        toAnalyse.add(classFilePath);
        driver.analyse(toAnalyse);
        // 去除无用信息
        result.replace(0, result.lastIndexOf("package"), "");
        return result.toString();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# 2.3、结果展示

1、启动工具类,追加的参数是业务主要的代码包路径

image-20240126135247949

2、输入需要attach的序号,比如 2

image-20240126135336540

image-20240126135354651

3、双击需要反编译的类,右侧就会实时展示反编译之后的java代码

image-20240126135437868

# 3、增加单个方法的Watch功能

# 3.1、UI界面选择方法

首先,UI页面把源码Tree结构做改造,叶子节点原先是类名,要修改为方法名,核心修改代码如下:

// 增加方法结果
String fileName = name.substring(name.lastIndexOf(".") + 1, name.length());
Method[] declaredMethods = allLoadedClass.getDeclaredMethods();
List<String> methodList = new ArrayList<>();
for (Method declaredMethod : declaredMethods) {
    String methodName = declaredMethod.getName();
    methodList.add(methodName);
}
methodMap.put(fileName, methodList);

classObj.setMethodList(methodMap);
1
2
3
4
5
6
7
8
9
10
11

然后增加选中事件,在UI界面选中某个方法之后,把方法名字进行保存

TreeSelectionListener treeSelectionListener = treeSelectionEvent -> {
    JTree treeSource = (JTree)treeSelectionEvent.getSource();
    TreePath[] selectionPaths = treeSource.getSelectionPaths();
    if (null != selectionPaths) {
        MainConfig.watchMethod.remove();
        for (TreePath selectionPath : selectionPaths) {
            Object[] path = selectionPath.getPath();
            String fullClass = "";
            String method = "";
            for (int i = 0; i < path.length; i++) {
                if (i == 0) {
                    continue;
                } else if (i == 1) {
                    fullClass = fullClass + path[i].toString();
                } else if (i == path.length - 1) {
                    method = path[i].toString();
                } else {
                    fullClass = fullClass + "." + path[i].toString();
                }
            }
            MainConfig.watchMethod.addWatch(fullClass, method);
        }
    }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 3.2、增加按钮,触发Watch

JButton watchBtn = new JButton("start watch");
watchBtn.setPreferredSize(new Dimension(700, 30));
watchBtn.addActionListener(e -> {
    // 调用watch方法,进行字节码插桩,然后等待请求调用
    MainConfig.watchText.setText("start watch! waiting..........");
    Clint.watch();
});
1
2
3
4
5
6
7

客户端会把已经选中的方法传入接口里面,然后接口开始使用javassist动态修改字节码文件,给方法添加执行时间参数,打印入参、返回值,把这些需要输出的数据都放入全局配置里面,

WatchTransformer watchTransformer = new WatchTransformer(watchMethod);
MainConfig.inst.addTransformer(watchTransformer, true);
for (Class<?> c : allLoadedClasses) {
    if (c.getName().startsWith("com.doe.afs") && !c.getName().contains("$")) {
        MainConfig.inst.retransformClasses(c);
    }
}
// 进入等待
MainConfig.watchRes.setWait(true);
while (MainConfig.watchRes.isWait()) {
    // 等待watch的目标方法执行
}
MainConfig.inst.removeTransformer(watchTransformer);
response.write(Json.toBytes(MainConfig.watchRes));
1
2
3
4
5
6
7
8
9
10
11
12
13
14

具体的字节码修改代码如下:

CtMethod ctMethod = ctClass.getDeclaredMethod(method);// 得到这方法实例
ctMethod.addLocalVariable("startTime", CtClass.longType);
ctMethod.addLocalVariable("endTime", CtClass.longType);
ctMethod.addLocalVariable("execTime", CtClass.longType);
ctMethod.addLocalVariable("request", MainConfig.classPool.get(String.class.getName()));
ctMethod.addLocalVariable("response", MainConfig.classPool.get(String.class.getName()));
ctMethod.insertBefore("startTime = System.currentTimeMillis();");
ctMethod.insertAfter("endTime = System.currentTimeMillis();");
ctMethod.insertAfter("execTime = endTime - startTime;");
ctMethod.insertAfter("org.byteCode.config.MainConfig.watchRes.setMethodName(\"" + method + "\");");
ctMethod.insertAfter("org.byteCode.config.MainConfig.watchRes.setClassName(\"" + className + "\");");
ctMethod.insertAfter("org.byteCode.config.MainConfig.watchRes.setExecTime(execTime+\"ms\");");
ctMethod.insertAfter("request = org.byteCode.util.Json.toJson($args);");
ctMethod.insertAfter("response = org.byteCode.util.Json.toJson($_);");
ctMethod.insertAfter("org.byteCode.config.MainConfig.watchRes.setRequest(request);");
ctMethod.insertAfter("org.byteCode.config.MainConfig.watchRes.setResponse(response);");
ctMethod.insertAfter("org.byteCode.config.MainConfig.watchRes.setWait(false);");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

具体展示功能如下图所示:

开始监控

监控结束

# 4、Watch支持多方法同时监控

实现思路:使用全局的CountDownLatch,监控n个方法,就定义一个支持n计数器的CountDownLatch,然后通过javassist修改字节码,每个方法执行完毕之后,进行CountDownLatch#countDown()。这样就可以做到,同时监控多个方法,全部执行之后才返回结果的功能。

主要代码如下:

int methodNum = 0;
Map<String, Set<String>> currentWatchMap = watchMethod.currentWatchMap;
Set<String> keySet = currentWatchMap.keySet();
for (String key : keySet) {
    int size = currentWatchMap.get(key).size();
    methodNum = methodNum + size;
}
// 定义计数器
MainConfig.cd = new CountDownLatch(methodNum);
// 进入等待,需要等所有监控的方法都被触发调用才能结束主进程的阻塞
try {
    MainConfig.cd.await();
} catch (InterruptedException e) {
    throw new RuntimeException(e);
}

//字节码修改
ctMethod.insertAfter("org.byteCode.config.MainConfig.cd.countDown();");
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#Javassist
上次更新: 2024/11/25, 20:09:03
JMM(Java内存模型)
Agent 技术

← JMM(Java内存模型) Agent 技术→

最近更新
01
Activiti6-业务实现
12-06
02
Activiti6-API详解
11-28
03
SpringBoot集成Activiti和UI
11-21
更多文章>
Theme by Vdoing | Copyright © 2022-2024 赵宇博 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式