All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ideaaedi.component.dump.ExitDumpClassExecutor Maven / Gradle / Ivy

There is a newer version: 2.6.0
Show newest version
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}
*

* 特别注意: *
    *
  1. 使用此工具类(即{@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
  2. *
  3. 使用此工具类(即{@link sun.jvm.hotspot.tools.jcore.ClassDump})对目标JVM进行dump时,dump期间会使目标JVM stop-the-world
  4. *
  5. 使用此工具类(即{@link sun.jvm.hotspot.tools.jcore.ClassDump})进行dump时,dump完成时,会退出当前JVM进程(即:程序停止)
  6. *
* *

* 其它说明:
* 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}); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy