Java Agent(通灵之术)

历览千载书,时时见遗烈。这篇文章主要讲述Java Agent:通灵之术相关的知识,希望能为你提供帮助。
1. 通灵之术在《火影忍者》中,通灵之术,属于时空间忍术的一种。

Java Agent(通灵之术)

文章图片

那么,“通灵之术”,在java领域,代表什么意思呢?就是将正在运行的JVM当中的class进行导出。
Java Agent(通灵之术)

文章图片

本文的主要目的:借助于Java Agent将class文件从JVM当中导出。
2. 准备工作开发环境:
  • JDK版本:Java 8
  • 开发工具:记事本或vi
【Java Agent(通灵之术)】创建文件目录结构:准备一个prepare.sh文件
#!/bin/bashmkdir -p application/src,out/sample/ touch application/src/sample/HelloWorld.java,Program.javamkdir -p java-agent/src,out/ touch java-agent/src/ClassDumpAgent.java,ClassDumpTransformer.java,ClassDumpUtils.java,manifest.txtmkdir -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.
Java Agent(通灵之术)

文章图片

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 = https://www.songbingjia.com/android/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基础篇》。

    推荐阅读