

Java Agent:通灵之术
source link: https://blog.51cto.com/lsieun/5080318
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

1. 通灵之术
在《火影忍者》中,通灵之术,属于时空间忍术的一种。
那么,“通灵之术”,在Java领域,代表什么意思呢?就是将正在运行的JVM当中的class进行导出。
本文的主要目的:借助于Java Agent将class文件从JVM当中导出。
2. 准备工作
开发环境:
- JDK版本:Java 8
- 开发工具:记事本或
vi
创建文件目录结构:准备一个prepare.sh
文件
#!/bin/bash
mkdir -p application/{src,out}/sample/
touch application/src/sample/{HelloWorld.java,Program.java}
mkdir -p java-agent/{src,out}/
touch java-agent/src/{ClassDumpAgent.java,ClassDumpTransformer.java,ClassDumpUtils.java,manifest.txt}
mkdir -p tools-attach/{src,out}/
touch tools-attach/src/Attach.java
目录结构:(编译之前)
java-agent-summoning-jutsu
├─── application
│ └─── src
│ └─── sample
│ ├─── HelloWorld.java
│ └─── Program.java
├─── java-agent
│ └─── src
│ ├─── ClassDumpAgent.java
│ ├─── ClassDumpTransformer.java
│ ├─── ClassDumpUtils.java
│ └─── manifest.txt
└─── tools-attach
└─── src
└─── Attach.java
目录结构:(编译之后)
java-agent-summoning-jutsu
├─── application
│ ├─── out
│ │ └─── sample
│ │ ├─── HelloWorld.class
│ │ └─── Program.class
│ └─── src
│ └─── sample
│ ├─── HelloWorld.java
│ └─── Program.java
├─── java-agent
│ ├─── out
│ │ ├─── ClassDumpAgent.class
│ │ ├─── classdumper.jar
│ │ ├─── ClassDumpTransformer.class
│ │ ├─── ClassDumpUtils.class
│ │ └─── manifest.txt
│ └─── src
│ ├─── ClassDumpAgent.java
│ ├─── ClassDumpTransformer.java
│ ├─── ClassDumpUtils.java
│ └─── manifest.txt
└─── tools-attach
├─── out
│ └─── Attach.class
└─── src
└─── Attach.java
3. Application
3.1. HelloWorld.java
package sample;
public class HelloWorld {
public static int add(int a, int b) {
return a + b;
}
public static int sub(int a, int b) {
return a - b;
}
}
3.2. Program.java
package sample;
import java.lang.management.ManagementFactory;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class Program {
public static void main(String[] args) throws Exception {
// (1) print process id
String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();
System.out.println(nameOfRunningVM);
// (2) count down
int count = 600;
for (int i = 0; i < count; i++) {
String info = String.format("|%03d| %s remains %03d seconds", i, nameOfRunningVM, (count - i));
System.out.println(info);
Random rand = new Random(System.currentTimeMillis());
int a = rand.nextInt(10);
int b = rand.nextInt(10);
boolean flag = rand.nextBoolean();
String message;
if (flag) {
message = String.format("a + b = %d", HelloWorld.add(a, b));
}
else {
message = String.format("a - b = %d", HelloWorld.sub(a, b));
}
System.out.println(message);
TimeUnit.SECONDS.sleep(1);
}
}
}
3.3. 编译和运行
进行编译:
# 进行编译
$ cd application/
$ javac src/sample/*.java -d out/
运行结果:
$ cd out/
$ java sample.Program
5556@LenovoWin7
|000| 5556@LenovoWin7 remains 600 seconds
a - b = 6
|001| 5556@LenovoWin7 remains 599 seconds
a - b = -4
...
4. Java Agent
曾经有一篇文章《Retrieving .class files from a running app》,最初是发表在Sun公司的网站,后来转移到了Oracle的网站,再后来就从Oracle网站消失了。
Sometimes it is better to dump .class
files of generated/modified classes for off-line debugging -
for example, we may want to view such classes using tools like jclasslib.
4.1. 类
4.1.1. ClassDumpAgent
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.util.ArrayList;
import java.util.List;
/**
* This is a java.lang.instrument agent to dump .class files
* from a running Java application.
*/
public class ClassDumpAgent {
public static void premain(String agentArgs, Instrumentation inst) {
agentmain(agentArgs, inst);
}
public static void agentmain(String agentArgs, Instrumentation inst) {
System.out.println("agentArgs: " + agentArgs);
ClassDumpUtils.parseArgs(agentArgs);
inst.addTransformer(new ClassDumpTransformer(), true);
// by the time we are attached, the classes to be
// dumped may have been loaded already.
// So, check for candidates in the loaded classes.
Class[] classes = inst.getAllLoadedClasses();
List<Class> candidates = new ArrayList<>();
for (Class c : classes) {
String className = c.getName();
// 第一步,排除法:不考虑JDK自带的类
if (className.startsWith("java")) continue;
if (className.startsWith("javax")) continue;
if (className.startsWith("jdk")) continue;
if (className.startsWith("sun")) continue;
if (className.startsWith("com.sun")) continue;
// 第二步,筛选法:只留下感兴趣的类(正则表达式匹配)
boolean isModifiable = inst.isModifiableClass(c);
boolean isCandidate = ClassDumpUtils.isCandidate(className);
if (isModifiable && isCandidate) {
candidates.add(c);
}
// 不重要:打印调试信息
String message = String.format("[DEBUG] Loaded Class: %s ---> Modifiable: %s, Candidate: %s", className, isModifiable, isCandidate);
System.out.println(message);
}
try {
// 第三步,将具体的class进行dump操作
// if we have matching candidates, then retransform those classes
// so that we will get callback to transform.
if (!candidates.isEmpty()) {
inst.retransformClasses(candidates.toArray(new Class[0]));
// 不重要:打印调试信息
String message = String.format("[DEBUG] candidates size: %d", candidates.size());
System.out.println(message);
}
}
catch (UnmodifiableClassException ignored) {
}
}
}
4.1.2. ClassDumpTransformer
import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
public class ClassDumpTransformer implements ClassFileTransformer {
public byte[] transform(ClassLoader loader,
String className,
Class redefinedClass,
ProtectionDomain protDomain,
byte[] classBytes) {
// check and dump .class file
if (ClassDumpUtils.isCandidate(className)) {
ClassDumpUtils.dumpClass(className, classBytes);
}
// we don't mess with .class file, just return null
return null;
}
}
4.1.3. ClassDumpUtils
import java.io.File;
import java.io.FileOutputStream;
import java.util.regex.Pattern;
public class ClassDumpUtils {
// directory where we would write .class files
private static String dumpDir;
// classes with name matching this pattern will be dumped
private static Pattern classes;
// parse agent args of the form arg1=value1,arg2=value2
public static void parseArgs(String agentArgs) {
if (agentArgs != null) {
String[] args = agentArgs.split(",");
for (String arg : args) {
String[] tmp = arg.split("=");
if (tmp.length == 2) {
String name = tmp[0];
String value = tmp[1];
if (name.equals("dumpDir")) {
dumpDir = value;
}
else if (name.equals("classes")) {
classes = Pattern.compile(value);
}
}
}
}
if (dumpDir == null) {
dumpDir = ".";
}
if (classes == null) {
classes = Pattern.compile(".*");
}
System.out.println("[DEBUG] dumpDir: " + dumpDir);
System.out.println("[DEBUG] classes: " + classes);
}
public static boolean isCandidate(String className) {
// ignore array classes
if (className.charAt(0) == '[') {
return false;
}
// convert the class name to external name
className = className.replace('/', '.');
// check for name pattern match
return classes.matcher(className).matches();
}
public static void dumpClass(String className, byte[] classBuf) {
try {
// create package directories if needed
className = className.replace("/", File.separator);
StringBuilder buf = new StringBuilder();
buf.append(dumpDir);
buf.append(File.separatorChar);
int index = className.lastIndexOf(File.separatorChar);
if (index != -1) {
String pkgPath = className.substring(0, index);
buf.append(pkgPath);
}
String dir = buf.toString();
new File(dir).mkdirs();
// write .class file
String fileName = dumpDir + File.separator + className + ".class";
FileOutputStream fos = new FileOutputStream(fileName);
fos.write(classBuf);
fos.close();
System.out.println("[DEBUG] FileName: " + fileName);
}
catch (Exception ex) {
ex.printStackTrace();
}
}
}
4.2. manifest.txt
Premain-Class: ClassDumpAgent
Agent-Class: ClassDumpAgent
Can-Redefine-Classes: true
Can-Retransform-Classes: true
注意:在结尾处添加一个空行。
4.3. 编译和打包
第一步,进行编译:
$ javac src/ClassDump*.java -d ./out
在Windows操作系统,如果遇到如下错误:
错误: 编码GBK的不可映射字符
可以添加-encoding
选项:
javac -encoding UTF-8 src/ClassDump*.java -d ./out
第二步,生成Jar文件:
$ cp src/manifest.txt out/
$ cd out/
$ jar -cvfm classdumper.jar manifest.txt ClassDump*.class
5. Tools Attach
将一个Agent Jar与一个正在运行的Application建立联系,需要用到Attach机制:
Agent Jar ---> Tools Attach ---> Application(JVM)
与Attach机制相关的类,定义在tools.jar
文件:
JDK_HOME/lib/tools.jar
5.1. Attach
import com.sun.tools.attach.VirtualMachine;
/**
* Simple attach-on-demand client tool
* that loads the given agent into the given Java process.
*/
public class Attach {
public static void main(String[] args) throws Exception {
if (args.length < 2) {
System.out.println("usage: java Attach <pid> <agent-jar-full-path> [<agent-args>]");
System.exit(1);
}
// JVM is identified by process id (pid).
VirtualMachine vm = VirtualMachine.attach(args[0]);
String agentArgs = (args.length > 2) ? args[2] : null;
// load a specified agent onto the JVM
vm.loadAgent(args[1], agentArgs);
vm.detach();
}
}
5.2. 编译
# 编译(Linux)
$ javac -cp "${JAVA_HOME}/lib/tools.jar":. src/Attach.java -d out/
# 编译(MINGW64)
$ javac -cp "${JAVA_HOME}/lib/tools.jar"\;. src/Attach.java -d out/
# 编译(Windows)
$ javac -cp "%JAVA_HOME%/lib/tools.jar";. src/Attach.java -d out/
5.3. 运行
# 运行(Linux)
java -cp "${JAVA_HOME}/lib/tools.jar":. Attach <pid> <full-path-of-classdumper.jar> dumpDir=<dir>,classes=<name-pattern>
# 运行(MINGW64)
java -cp "${JAVA_HOME}/lib/tools.jar"\;. Attach <pid> <full-path-of-classdumper.jar> dumpDir=<dir>,classes=<name-pattern>
# 运行(Windows)
java -cp "%JAVA_HOME%/lib/tools.jar";. Attach <pid> <full-path-of-classdumper.jar> dumpDir=<dir>,classes=<name-pattern>
java -cp "${JAVA_HOME}/lib/tools.jar"\;. Attach <pid> \
D:/tmp/java-agent-summoning-jutsu/java-agent/out/classdumper.jar \
dumpDir=D:/tmp/java-agent-summoning-jutsu/dump,classes=sample\.HelloWorld
本文内容总结如下:
- 第一点,主要功能。从功能的角度来讲,是如何从一个正在运行的JVM当中将某一个class文件导出的磁盘上。
- 第二点,实现方式。从实现方式上来说,是借助于Java Agent和正则表达式(区配类名)来实现功能。
- 第三点,注意事项。在Java 8的环境下,想要将Agent Jar加载到一个正在运行的JVM当中,需要用到
tools.jar
。
当然,将class文件从运行的JVM当中导出,只是Java Agent功能当中的一个小部分,想要更多的了解Java Agent的内容,可以学习《 Java Agent基础篇》。
Recommend
-
11
左右互搏之术[编程的日常]2gua☑编程 ☑读书 ☑翻译 ☑太极作为程序员出身的人,都要...
-
14
本期我们将剖析刚上新的Shader Analyzer中和Shader变体相关的规则:“Build后生成变体数过多的Shader”、“项目中可能生成变体数过多的Shader”和“项目中全局关...
-
12
ThreadLocal Java多线程下的影分身之术如果写过多线程的代码,你肯定考虑过线程安全问题,更进一步你可能还考虑在在线程安全的前提下性能的问题。大多数情况下大家用来解决线程安全问题都会使用同步,比如用synchron或者concurrent包提供的各种锁,当然这些都能...
-
9
莱绅通灵天价离婚案收上交所监管工作函 实控人地位或不保 Connect with us 若二审维持原判,沈...
-
3
作者:不学无数的程序员 来源:https://my.oschina.net/u/4030990/blog/3211858 在网上关于如何修改Java的抽象语法树的相关API文档并不多,于是本篇记录一下相关的知识点,以便随...
-
3
** 在之前的“Java中的屠龙之术:如何修改语法树”中,我们详细介绍了如何使用Javac源码提供的工具类来修改语法树。** 而在此基础上,有一款开源工具javapoet可以更加快捷地生成字节码,实现原理其实也就是对JavaAPT的封装,然而Javapoet有一个局限性,就...
-
7
当前位置:无极领域 > 随笔 > 正文 怀柔之术消灭一切敌人,承诺一致性原理的妙用
-
5
莱绅通灵首发520NFT数字藏品 开启元宇宙时代告白新方式-品玩 莱绅通灵首发520NFT数字藏品 开启元宇宙时代告白新方式 2小时前 2022年5月20日,比利时王室珠宝供应商莱绅通灵将首次发布品牌蓝火真心钻NFT数字...
-
10
藏民9小时前2940万物皆有周期,加密市场亦然。历经两年资本盛宴后, 2022 年各项链上数据的严重缩水足以可见市场的惨烈,特别是不少巨头的倒塌让这个加密寒冬格外凛冽。...
-
5
听我说# 从 条件渲染 那一篇,我学习到了如何用Vue对d...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK