com.ideaaedi.component.dump.ExitDumpClassExecutor Maven / Gradle / Ivy
package com.ideaaedi.component.dump;
import com.ideaaedi.commonds.bash.BashUtil;
import com.ideaaedi.commonds.exception.ExceptionUtil;
import org.apache.commons.lang3.StringUtils;
import sun.jvm.hotspot.tools.jcore.ClassDump;
import sun.jvm.hotspot.tools.jcore.ClassFilter;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 扩展封装{@link sun.jvm.hotspot.tools.jcore.ClassDump}
*
* 特别注意:
*
* - 使用此工具类(即{@link sun.jvm.hotspot.tools.jcore.ClassDump})对目标JVM进行dump class时,如果被dump的类
* 中有采用invokedynamic机制的代码(提示:部分lambda表达式就采用了该机制),
* {@link sun.jvm.hotspot.tools.jcore.ClassDump}是无法采集到此处足够的信息。
* 这时,对dump出的.class进行反编译,可以看到源码对应的地方使用了<invokedynamic>占位符代替;
* 此时,可以考虑使用{@link NonExitClassFileTransformerExecutor}来进行dump
* - 使用此工具类(即{@link sun.jvm.hotspot.tools.jcore.ClassDump})对目标JVM进行dump时,dump期间会使目标JVM stop-the-world
* - 使用此工具类(即{@link sun.jvm.hotspot.tools.jcore.ClassDump})进行dump时,dump完成时,会退出当前JVM进程(即:程序停止)
*
*
*
* 其它说明:
* jdk工具sd-jdi.jar里{@link sun.jvm.hotspot.tools.jcore.ClassDump}可以把类的class内容dump到出来. 原生的{@link
* sun.jvm.hotspot.tools.jcore.ClassDump}默认有两个可选参数:
*
* - sun.jvm.hotspot.tools.jcore.filter: 指定{@link ClassFilter}的类名
* - sun.jvm.hotspot.tools.jcore.outputDir: class文件输出的目录
*
*
*
* 所需依赖:
*
* sun.jvm.hotspot
* sa-jdi
* ${java.version}
* system
* ${java.home}/../lib/sa-jdi.jar
*
*
* @author JustryDeng
* @since 2021/9/25 19:40:06
*/
public class ExitDumpClassExecutor {
public static final String ARG_PID = "pid";
public static final String ARG_INCLUDE_PREFIXES = "includePrefixes";
public static final String ARG_EXCLUDE_PREFIXES = "excludePrefixes";
public static final String ARG_OUTPUT_DIR = "outputDir";
public static final String ARG_KEEP_LONG_NAME = "keepLongName";
public static final String ARG_VALUE_SEPARATOR = "=";
public static final boolean DEFAULT_KEEP_LONG_NAME = false;
public static final AtomicInteger TRY_TIMES = new AtomicInteger(1);
/**
* main入口
*/
public static void main(String[] args){
try {
try {
// ClassDump是否能加载到
ExitDumpClassExecutor.class.getClassLoader().loadClass("sun.jvm.hotspot.tools.jcore.ClassDump");
} catch (ClassNotFoundException e) {
int currTime = TRY_TIMES.getAndIncrement();
if (currTime > 3) {
System.out.println("[ERROR] ClassNotFoundException for 'sun.jvm.hotspot.tools.jcore.ClassDump' over 3 times.");
return;
}
tryLoadSaJdiAndExecMain(args);
return;
}
// 参数解析
String pid = parseValueByPrefixFromTail(ARG_PID + ARG_VALUE_SEPARATOR, args);
String includePrefixes = parseValueByPrefixFromTail(ARG_INCLUDE_PREFIXES + ARG_VALUE_SEPARATOR, args);
String excludePrefixes = parseValueByPrefixFromTail(ARG_EXCLUDE_PREFIXES + ARG_VALUE_SEPARATOR, args);
String outputDir = parseValueByPrefixFromTail(ARG_OUTPUT_DIR + ARG_VALUE_SEPARATOR, args);
String keepLongName = parseValueByPrefixFromTail(ARG_KEEP_LONG_NAME + ARG_VALUE_SEPARATOR, args);
boolean keepLongNameBool = DEFAULT_KEEP_LONG_NAME;
try {
keepLongNameBool = Boolean.parseBoolean(keepLongName);
} catch (Exception e) {
// ignore
}
exec(pid, includePrefixes, excludePrefixes, outputDir, keepLongNameBool);
} catch (Throwable e) {
System.out.println("[ERROR] ExitDumpExecutor#main exception" + ExceptionUtil.getStackTraceMessage(e));
}
}
/**
* @see ExitDumpClassExecutor#exec(String, String, String, String, boolean)
*/
public static void exec(String pid, String includePrefixes, String outputDir) {
exec(pid, includePrefixes, null, outputDir, DEFAULT_KEEP_LONG_NAME);
/*
* 说明:其实当执行dump时,内部执行完时,最后一步就会执行sun.jvm.hotspot.tools.Tool#execute,进而退出程序,根本不会执行到这一行。
* 之所以还多余的显示的写个System.exit(0),是为了让人一眼就能看知道这个方法执行完后,程序会退出,慎用
*/
System.exit(0);
}
/**
* @see ExitDumpClassExecutor#exec(String, String, String, String, boolean)
*/
public static void exec(String pid, String includePrefixes, String outputDir, boolean keepLongName) {
exec(pid, includePrefixes, null, outputDir, keepLongName);
/*
* 说明:其实当执行dump时,内部执行完时,最后一步就会执行sun.jvm.hotspot.tools.Tool#execute,进而退出程序,根本不会执行到这一行。
* 之所以还多余的显示的写个System.exit(0),是为了让人一眼就能看知道这个方法执行完后,程序会退出,慎用
*/
System.exit(0);
}
/**
* dump 指定JVM中的class
*
* @param pid
* 要被进行dump操作的JVM进程id
* @param includePrefixes
* 要dump的class的全类名前缀,多个使用逗号分割
* @param excludePrefixes
* 明确排除dump的class的全类名前缀,多个使用逗号分割
* @param outputDir
* 输出目录
* @param keepLongName
* 是否以全类名作为文件名
*
* 假设现有类com.example.HelloWorld,那么:
*
* - 为true时:输出的文件为com.example.HelloWorld.class
* - 为false时::输出的文件为com/example/HelloWorld/class
*
*
*
*/
public static void exec(String pid, String includePrefixes, String excludePrefixes,
String outputDir, boolean keepLongName) {
System.out.printf("pid -> %s\nincludePrefixes -> %s\nexcludePrefixes -> %s\noutputDir -> %s\nkeepLongName -> %s\n",
pid, includePrefixes, excludePrefixes, outputDir, keepLongName);
// ClassDump.main(new String[]{pid});执行到最后一步时,方法内部会主动调用exit退出当前JVM进程(即:结束程序),这里添加钩子,在程序结束前进行统计输出
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
System.out.printf("dumped file count %s, paths:\n", PrefixMatchClassFilter.DUMP_FILE_PATH_LIST.size());
for (int i = 1; i <= PrefixMatchClassFilter.DUMP_FILE_PATH_LIST.size(); i++) {
System.out.println(i + ". " + PrefixMatchClassFilter.DUMP_FILE_PATH_LIST.get(i - 1));
}
}
}));
// step1. 校验并设置相关参数
/*
* 不能dump当前JVM的class, 要不然ClassDump.main时会报错:
* Error attaching to process: Windbg Error: AttachProcess failed!
* sun.jvm.hotspot.debugger.DebuggerException: Windbg Error: AttachProcess failed!
* at sun.jvm.hotspot.debugger.windbg.WindbgDebuggerLocal.attach0(Native Method)
*/
if (BashUtil.currPid().equals(pid)) {
throw new IllegalArgumentException("includePrefixes cannot be blank.");
}
// includePrefixes
if (StringUtils.isBlank(includePrefixes)) {
throw new IllegalArgumentException("includePrefixes cannot be blank.");
}
System.setProperty(PrefixMatchClassFilter.INCLUDE_PREFIXES_KEY, includePrefixes);
// excludePrefixes
if (StringUtils.isNotBlank(excludePrefixes)) {
System.setProperty(PrefixMatchClassFilter.EXCLUDE_PREFIXES_KEY, excludePrefixes);
}
// outputDir
if (StringUtils.isBlank(outputDir)) {
throw new IllegalArgumentException("outputDir cannot be blank.");
}
System.setProperty(PrefixMatchClassFilter.OUTPUT_DIR_KEY, outputDir);
// keepLongName
System.setProperty(PrefixMatchClassFilter.KEEP_LONG_NAME_KEY, String.valueOf(keepLongName));
// step2. 使用自定义的ClassFilter,实现扩展
System.setProperty(PrefixMatchClassFilter.CLASS_DUMP_FILTER_KEY, PrefixMatchClassFilter.class.getTypeName());
/*
* step3. 触发dump
* 特别注意:ClassDump.main(new String[]{pid});的执行逻辑中,在最后一步时,会System.exit()退出程序结束当前进程。
* 所以:你在ClassDump.main(new String[]{pid});后面写的代码是不会执行的。
*/
ClassDump.main(new String[]{pid});
/*
* 说明:其实当执行dump时,内部执行完时,最后一步就会执行sun.jvm.hotspot.tools.Tool#execute,进而退出程序,根本不会执行到这一行。
* 之所以还多余的显示的写个System.exit(0),是为了让人一眼就能看知道这个方法执行完后,程序会退出,慎用
*/
System.exit(0);
}
/**
* 根据前缀解析值
*
* 如: 数组中某元素的为 k1=v1, 此方法传入的前缀为k1=, 那么此方法会返回v1
*
*
* @param prefix
* 前缀
* @param args
* 参数数组
* @return 解析出来的值,若没有则返回null
*/
private static String parseValueByPrefixFromTail(String prefix, String[] args) {
Objects.requireNonNull(prefix, "prefix cannot be null.");
if (args == null) {
return null;
}
// 从后往前找,即: java -jar k1=v1 k2=v2 k3=v3 -xxx.jar中,若k冲突了,那么取后面的k对应的v
for (int i = args.length - 1; i >= 0; i--) {
if (args[i] == null || StringUtils.isBlank(args[i])) {
continue;
}
args[i] = args[i].trim();
if (args[i].startsWith(prefix)) {
return args[i].substring(prefix.length());
}
}
return null;
}
/**
* 试着再加载sa-jdi.jar,并执行{@link ExitDumpClassExecutor#main}
*
* @param mainArgs
* {@link ExitDumpClassExecutor#main}的入参
*/
private static void tryLoadSaJdiAndExecMain(String[] mainArgs) throws
MalformedURLException, ClassNotFoundException, NoSuchMethodException,
IllegalAccessException, InvocationTargetException {
System.out.println("can not find sa-jdi.jar from classpath, try to load it from java.home.");
String javaHome = System.getProperty("java.home");
if (javaHome == null) {
javaHome = System.getenv("JAVA_HOME");
}
if (javaHome == null) {
System.out.println("can not get java.home, can not load sa-jdi.jar.");
System.exit(-1);
}
File saJdiJar = new File(javaHome + "/lib/sa-jdi.jar");
if (!saJdiJar.exists()) {
// java.home maybe jre
saJdiJar = new File(javaHome + "/../lib/sa-jdi.jar");
if (!saJdiJar.exists()) {
System.out.println("can not find lib/sa-jdi.jar from java.home: " + javaHome);
}
}
// build a new classloader, a trick.
List urls = new ArrayList<>(Arrays.asList(((URLClassLoader) ExitDumpClassExecutor.class.getClassLoader()).getURLs()));
urls.add(saJdiJar.toURI().toURL());
URLClassLoader classLoader = new URLClassLoader(urls.toArray(new URL[0]), ClassLoader.getSystemClassLoader().getParent());
Class> startClass = classLoader.loadClass(ExitDumpClassExecutor.class.getName());
final Method exitDumpExecutorMain = startClass.getMethod("main", String[].class);
if (!exitDumpExecutorMain.isAccessible()) {
exitDumpExecutorMain.setAccessible(true);
}
exitDumpExecutorMain.invoke(null, new Object[] {mainArgs});
}
}