Java Agent:通灵之术
Posted lsieun
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java Agent:通灵之术相关的知识,希望对你有一定的参考价值。
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 dont 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
6. 总结
本文内容总结如下:
- 第一点,主要功能。从功能的角度来讲,是如何从一个正在运行的JVM当中将某一个class文件导出的磁盘上。
- 第二点,实现方式。从实现方式上来说,是借助于Java Agent和正则表达式(区配类名)来实现功能。
- 第三点,注意事项。在Java 8的环境下,想要将Agent Jar加载到一个正在运行的JVM当中,需要用到
tools.jar
。
当然,将class文件从运行的JVM当中导出,只是Java Agent功能当中的一个小部分,想要更多的了解Java Agent的内容,可以学习《Java Agent基础篇》。
以上是关于Java Agent:通灵之术的主要内容,如果未能解决你的问题,请参考以下文章