1. Channeling
In Naruto, the art of channeling belongs to time and space ninja.
So what does "channeling" mean in the Java world? Is to export the class es in the running JVM.
The main purpose of this paper is to export the class file from the JVM with the help of Java Agent.
2. Preparation
Development environment:
- JDK version: Java 8
- Development tools: Notepad or vi
Create file directory structure: prepare a prepare SH file
#!/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
Directory structure: (before compilation)
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
Directory structure: (after compilation)
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. Compile and run
To compile:
# Compile $ cd application/ $ javac src/sample/*.java -d out/
Operation results:
$ 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
There was once an article "retrieving. Class files from a running app", which was first published on the website of Sun company, then transferred to the website of Oracle, and then disappeared from the Oracle website.
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. class
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(); // Step 1: exclude: do not consider the JDK's own classes 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; // The second step, filtering method: leave only the classes of interest (regular expression matching) boolean isModifiable = inst.isModifiableClass(c); boolean isCandidate = ClassDumpUtils.isCandidate(className); if (isModifiable && isCandidate) { candidates.add(c); } // Unimportant: print debugging information String message = String.format("[DEBUG] Loaded Class: %s ---> Modifiable: %s, Candidate: %s", className, isModifiable, isCandidate); System.out.println(message); } try { // Step 3: dump the specific class // 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])); // Unimportant: print debugging information String message = String.format("[DEBUG] candidates size: %d", candidates.size()); System.out.println(message); } } catch (UnmodifiableClassException ignored) { } } }
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; } }
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
Note: add a blank line at the end.
4.3. Compilation and packaging
The first step is to compile:
$ javac src/ClassDump*.java -d ./out
On the Windows operating system, if you encounter the following error:
error: code GBK Unmapped character of
You can add the - encoding option:
javac -encoding UTF-8 src/ClassDump*.java -d ./out
Step 2: generate Jar file:
$ cp src/manifest.txt out/ $ cd out/ $ jar -cvfm classdumper.jar manifest.txt ClassDump*.class
5. Tools Attach
To connect an Agent Jar with a running Application, you need to use the Attach mechanism:
Agent Jar ---> Tools Attach ---> Application(JVM)
Classes related to the Attach mechanism are defined in tools Jar file:
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. compile
# Compile (Linux) $ javac -cp "${JAVA_HOME}/lib/tools.jar":. src/Attach.java -d out/ # Compile (MINGW64) $ javac -cp "${JAVA_HOME}/lib/tools.jar"\;. src/Attach.java -d out/ # Compile (Windows) $ javac -cp "%JAVA_HOME%/lib/tools.jar";. src/Attach.java -d out/
5.3. function
# Run (Linux) java -cp "${JAVA_HOME}/lib/tools.jar":. Attach <pid> <full-path-of-classdumper.jar> dumpDir=<dir>,classes=<name-pattern> # Operation (MINGW64) java -cp "${JAVA_HOME}/lib/tools.jar"\;. Attach <pid> <full-path-of-classdumper.jar> dumpDir=<dir>,classes=<name-pattern> # Run (Windows) java -cp "%JAVA_HOME%/lib/tools.jar";. Attach <pid> <full-path-of-classdumper.jar> dumpDir=<dir>,classes=<name-pattern>
Example:
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. Summary
The contents of this paper are summarized as follows:
- First, the main functions. From the perspective of function, it is how to export a class file from a running JVM to the disk.
- Second, the implementation method. In terms of implementation, it realizes the function with the help of Java Agent and regular expression (distinguishing class name).
- Third, precautions. In the Java 8 environment, tools are needed to load the Agent Jar into a running JVM jar.
Of course, exporting the class file from the running JVM is only a small part of the functions of Java Agent. You can learn more about Java Agent< Java Agent Basics>.