diff --git a/core/src/main/java/com/taobao/arthas/core/command/klass100/ClassLoaderCommand.java b/core/src/main/java/com/taobao/arthas/core/command/klass100/ClassLoaderCommand.java index f283eff4a9d..a643b8a6718 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/klass100/ClassLoaderCommand.java +++ b/core/src/main/java/com/taobao/arthas/core/command/klass100/ClassLoaderCommand.java @@ -41,6 +41,7 @@ import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; +import java.util.regex.Pattern; @Name("classloader") @Summary("Show classloader info") @@ -54,10 +55,15 @@ " classloader -a -c 327a647b\n" + " classloader -c 659e0bfd --load demo.MathGame\n" + " classloader -u # url statistics\n" + + " classloader -c 659e0bfd --url-classes\n" + + " classloader -c 659e0bfd --url-classes -d\n" + + " classloader -c 659e0bfd --url-classes --jar spring-core --class org.springframework\n" + Constants.WIKI + Constants.WIKI_HOME + "classloader") public class ClassLoaderCommand extends AnnotatedCommand { private static Logger logger = LoggerFactory.getLogger(ClassLoaderCommand.class); + private static final int DEFAULT_URL_CLASSES_LIMIT = 100; + private static final String UNKNOWN_CODE_SOURCE = ""; private boolean isTree = false; private String hashCode; private String classLoaderClass; @@ -68,6 +74,13 @@ public class ClassLoaderCommand extends AnnotatedCommand { private boolean urlStat = false; + private boolean urlClasses = false; + private boolean urlClassesDetail = false; + private boolean urlClassesRegEx = false; + private int urlClassesLimit = DEFAULT_URL_CLASSES_LIMIT; + private String jarFilter; + private String classFilter; + private String loadClass = null; private volatile boolean isInterrupted = false; @@ -126,6 +139,42 @@ public void setUrlStat(boolean urlStat) { this.urlStat = urlStat; } + @Option(longName = "url-classes", flag = true) + @Description("Display relationship between jar(URL) and loaded classes in the specified ClassLoader") + public void setUrlClasses(boolean urlClasses) { + this.urlClasses = urlClasses; + } + + @Option(shortName = "d", longName = "details", flag = true) + @Description("Display class list for each jar(URL), only works with --url-classes") + public void setUrlClassesDetail(boolean urlClassesDetail) { + this.urlClassesDetail = urlClassesDetail; + } + + @Option(shortName = "E", longName = "regex", flag = true) + @Description("Enable regular expression to match for --jar/--class, only works with --url-classes") + public void setUrlClassesRegEx(boolean urlClassesRegEx) { + this.urlClassesRegEx = urlClassesRegEx; + } + + @Option(shortName = "n", longName = "limit") + @Description("Maximum number of classes to display per jar(URL) in details mode (100 by default), only works with --url-classes -d") + public void setUrlClassesLimit(int urlClassesLimit) { + this.urlClassesLimit = urlClassesLimit; + } + + @Option(longName = "jar") + @Description("Filter jar(URL) by keyword (or regex with -E), only works with --url-classes") + public void setJarFilter(String jarFilter) { + this.jarFilter = jarFilter; + } + + @Option(longName = "class") + @Description("Filter classes by keyword/package (or regex with -E), only works with --url-classes") + public void setClassFilter(String classFilter) { + this.classFilter = StringUtils.normalizeClassName(classFilter); + } + @Override public void process(CommandProcess process) { // ctrl-C support @@ -143,6 +192,12 @@ public void process(CommandProcess process) { process.end(); return; } + + if (!urlClasses && (urlClassesDetail || urlClassesRegEx || jarFilter != null || classFilter != null + || urlClassesLimit != DEFAULT_URL_CLASSES_LIMIT)) { + process.end(-1, "Options -d/-E/-n/--jar/--class only work with --url-classes."); + return; + } if (hashCode != null || classLoaderClass != null) { classLoaderSpecified = true; @@ -174,6 +229,19 @@ public void process(CommandProcess process) { } } + if (urlClasses) { + if (!classLoaderSpecified) { + process.end(-1, "Please specify classloader with '-c ' or '--classLoaderClass ' for --url-classes."); + return; + } + if (targetClassLoader == null) { + process.end(-1, "Can not find classloader by hashcode: " + hashCode + "."); + return; + } + processUrlClasses(process, inst, targetClassLoader); + return; + } + if (all) { String hashCode = this.hashCode; if (StringUtils.isBlank(hashCode) && targetClassLoader != null) { @@ -407,6 +475,172 @@ private boolean checkInterrupted(CommandProcess process) { } } + private void processUrlClasses(CommandProcess process, Instrumentation inst, ClassLoader targetClassLoader) { + if (!urlClassesDetail && urlClassesLimit != DEFAULT_URL_CLASSES_LIMIT) { + process.end(-1, "Option -n/--limit only works with --url-classes -d."); + return; + } + if (urlClassesDetail && urlClassesLimit <= 0) { + process.end(-1, "Option -n/--limit must be greater than 0."); + return; + } + + Pattern jarPattern = null; + Pattern classPattern = null; + if (urlClassesRegEx) { + try { + if (jarFilter != null) { + jarPattern = Pattern.compile(jarFilter); + } + if (classFilter != null) { + classPattern = Pattern.compile(classFilter); + } + } catch (Throwable e) { + process.end(-1, "Regex compile error: " + e.getMessage()); + return; + } + } + + Map statsMap = new HashMap(); + Class[] allLoadedClasses = inst.getAllLoadedClasses(); + for (int i = 0; i < allLoadedClasses.length; i++) { + if ((i & 0x3FFF) == 0 && checkInterrupted(process)) { + return; + } + Class clazz = allLoadedClasses[i]; + if (clazz == null) { + continue; + } + if (clazz.getClassLoader() != targetClassLoader) { + continue; + } + + String url = codeSourceLocation(clazz); + if (!matchJarFilter(url, jarPattern)) { + continue; + } + + UrlClassStatBuilder builder = statsMap.get(url); + if (builder == null) { + builder = new UrlClassStatBuilder(url, classFilter != null, urlClassesDetail ? urlClassesLimit : 0); + statsMap.put(url, builder); + } + builder.increaseLoadedCount(); + + if (classFilter != null) { + if (matchClassFilter(clazz.getName(), classPattern)) { + builder.increaseMatchedCount(); + builder.tryAddClass(clazz.getName()); + } + } else { + builder.tryAddClass(clazz.getName()); + } + } + + boolean hasClassFilter = classFilter != null; + List stats = new ArrayList(statsMap.size()); + for (UrlClassStatBuilder builder : statsMap.values()) { + if (hasClassFilter && builder.getMatchedClassCount() == 0) { + continue; + } + stats.add(builder.build()); + } + + Collections.sort(stats, new Comparator() { + @Override + public int compare(UrlClassStat o1, UrlClassStat o2) { + int c1 = hasClassFilter ? safeInt(o1.getMatchedClassCount()) : o1.getLoadedClassCount(); + int c2 = hasClassFilter ? safeInt(o2.getMatchedClassCount()) : o2.getLoadedClassCount(); + int diff = c2 - c1; + if (diff != 0) { + return diff; + } + return o1.getUrl().compareTo(o2.getUrl()); + } + }); + + RowAffect affect = new RowAffect(); + affect.rCnt(stats.size()); + ClassLoaderModel model = new ClassLoaderModel() + .setClassLoader(ClassUtils.createClassLoaderVO(targetClassLoader)) + .setUrlClassStats(stats) + .setUrlClassStatsDetail(urlClassesDetail); + process.appendResult(model); + process.appendResult(new RowAffectModel(affect)); + process.end(); + } + + private static int safeInt(Integer v) { + return v == null ? 0 : v.intValue(); + } + + private boolean matchJarFilter(String url, Pattern jarPattern) { + if (jarFilter == null) { + return true; + } + String jarName = guessJarName(url); + if (urlClassesRegEx) { + return jarPattern != null && (jarPattern.matcher(url).find() || jarPattern.matcher(jarName).find()); + } + return containsIgnoreCase(url, jarFilter) || containsIgnoreCase(jarName, jarFilter); + } + + private boolean matchClassFilter(String className, Pattern classPattern) { + if (classFilter == null) { + return true; + } + if (urlClassesRegEx) { + return classPattern != null && classPattern.matcher(className).find(); + } + return containsIgnoreCase(className, classFilter); + } + + static boolean containsIgnoreCase(String text, String keyword) { + if (text == null || keyword == null) { + return false; + } + return text.toLowerCase().contains(keyword.toLowerCase()); + } + + private static String codeSourceLocation(Class clazz) { + try { + ProtectionDomain protectionDomain = clazz.getProtectionDomain(); + if (protectionDomain == null) { + return UNKNOWN_CODE_SOURCE; + } + CodeSource codeSource = protectionDomain.getCodeSource(); + if (codeSource == null) { + return UNKNOWN_CODE_SOURCE; + } + URL location = codeSource.getLocation(); + if (location == null) { + return UNKNOWN_CODE_SOURCE; + } + return location.toString(); + } catch (Throwable t) { + return UNKNOWN_CODE_SOURCE; + } + } + + static String guessJarName(String url) { + if (url == null) { + return com.taobao.arthas.core.util.Constants.EMPTY_STRING; + } + String s = url; + int bangIndex = s.lastIndexOf('!'); + if (bangIndex >= 0) { + s = s.substring(0, bangIndex); + } + while (s.endsWith("/")) { + s = s.substring(0, s.length() - 1); + } + int slash = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\')); + if (slash >= 0 && slash < s.length() - 1) { + s = s.substring(slash + 1); + } + return s; + } + private Map urlStats(Instrumentation inst) { Map urlStats = new HashMap(); Map> usedUrlsMap = new HashMap>(); @@ -639,6 +873,110 @@ public boolean accept(ClassLoader classLoader) { } } + public static class UrlClassStat { + private String url; + private int loadedClassCount; + private Integer matchedClassCount; + private List classes; + private boolean truncated; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public int getLoadedClassCount() { + return loadedClassCount; + } + + public void setLoadedClassCount(int loadedClassCount) { + this.loadedClassCount = loadedClassCount; + } + + public Integer getMatchedClassCount() { + return matchedClassCount; + } + + public void setMatchedClassCount(Integer matchedClassCount) { + this.matchedClassCount = matchedClassCount; + } + + public List getClasses() { + return classes; + } + + public void setClasses(List classes) { + this.classes = classes; + } + + public boolean isTruncated() { + return truncated; + } + + public void setTruncated(boolean truncated) { + this.truncated = truncated; + } + } + + private static class UrlClassStatBuilder { + private final String url; + private final boolean hasClassFilter; + private final int limit; + private int loadedClassCount; + private int matchedClassCount; + private SortedSet classNames; + private boolean truncated; + + UrlClassStatBuilder(String url, boolean hasClassFilter, int limit) { + this.url = url; + this.hasClassFilter = hasClassFilter; + this.limit = limit; + if (limit > 0) { + this.classNames = new TreeSet(); + } + } + + void increaseLoadedCount() { + loadedClassCount++; + } + + void increaseMatchedCount() { + matchedClassCount++; + } + + int getMatchedClassCount() { + return matchedClassCount; + } + + void tryAddClass(String className) { + if (classNames == null) { + return; + } + if (classNames.size() >= limit) { + truncated = true; + return; + } + classNames.add(className); + } + + UrlClassStat build() { + UrlClassStat stat = new UrlClassStat(); + stat.setUrl(url); + stat.setLoadedClassCount(loadedClassCount); + if (hasClassFilter) { + stat.setMatchedClassCount(matchedClassCount); + } + if (classNames != null) { + stat.setClasses(new ArrayList(classNames)); + } + stat.setTruncated(truncated); + return stat; + } + } + public static class ClassLoaderUrlStat { private Collection usedUrls; private Collection unUsedUrls; diff --git a/core/src/main/java/com/taobao/arthas/core/command/model/ClassLoaderModel.java b/core/src/main/java/com/taobao/arthas/core/command/model/ClassLoaderModel.java index 40bc024922c..8a8b2521fa8 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/model/ClassLoaderModel.java +++ b/core/src/main/java/com/taobao/arthas/core/command/model/ClassLoaderModel.java @@ -2,6 +2,7 @@ import com.taobao.arthas.core.command.klass100.ClassLoaderCommand.ClassLoaderStat; import com.taobao.arthas.core.command.klass100.ClassLoaderCommand.ClassLoaderUrlStat; +import com.taobao.arthas.core.command.klass100.ClassLoaderCommand.UrlClassStat; import java.util.List; import java.util.Map; @@ -28,6 +29,11 @@ public class ClassLoaderModel extends ResultModel { //urls stat private Map urlStats; + // url->classes stat + private ClassLoaderVO classLoader; + private List urlClassStats; + private Boolean urlClassStatsDetail; + public ClassLoaderModel() { } @@ -125,4 +131,31 @@ public void setUrlStats(Map urlStats) { this.urlStats = urlStats; } + public ClassLoaderVO getClassLoader() { + return classLoader; + } + + public ClassLoaderModel setClassLoader(ClassLoaderVO classLoader) { + this.classLoader = classLoader; + return this; + } + + public List getUrlClassStats() { + return urlClassStats; + } + + public ClassLoaderModel setUrlClassStats(List urlClassStats) { + this.urlClassStats = urlClassStats; + return this; + } + + public Boolean getUrlClassStatsDetail() { + return urlClassStatsDetail; + } + + public ClassLoaderModel setUrlClassStatsDetail(Boolean urlClassStatsDetail) { + this.urlClassStatsDetail = urlClassStatsDetail; + return this; + } + } diff --git a/core/src/main/java/com/taobao/arthas/core/command/view/ClassLoaderView.java b/core/src/main/java/com/taobao/arthas/core/command/view/ClassLoaderView.java index 4c59ced74a1..416b2876d9c 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/view/ClassLoaderView.java +++ b/core/src/main/java/com/taobao/arthas/core/command/view/ClassLoaderView.java @@ -2,6 +2,7 @@ import com.taobao.arthas.core.command.klass100.ClassLoaderCommand.ClassLoaderStat; import com.taobao.arthas.core.command.klass100.ClassLoaderCommand.ClassLoaderUrlStat; +import com.taobao.arthas.core.command.klass100.ClassLoaderCommand.UrlClassStat; import com.taobao.arthas.core.command.model.ClassDetailVO; import com.taobao.arthas.core.command.model.ClassLoaderModel; import com.taobao.arthas.core.command.model.ClassLoaderVO; @@ -51,6 +52,76 @@ public void draw(CommandProcess process, ClassLoaderModel result) { if (result.getUrlStats() != null) { drawUrlStats(process, result.getUrlStats()); } + if (result.getUrlClassStats() != null) { + drawUrlClassStats(process, result.getClassLoader(), result.getUrlClassStats(), + Boolean.TRUE.equals(result.getUrlClassStatsDetail())); + } + } + + private void drawUrlClassStats(CommandProcess process, ClassLoaderVO classLoader, List urlClassStats, + boolean detail) { + if (classLoader != null) { + process.write(classLoader.getName() + ", hash:" + classLoader.getHash() + "\n"); + } + + boolean hasMatched = false; + for (UrlClassStat stat : urlClassStats) { + if (stat.getMatchedClassCount() != null) { + hasMatched = true; + break; + } + } + + if (!detail) { + TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); + RowElement header = new RowElement().style(Decoration.bold.bold()); + if (hasMatched) { + header.add("url", "loadedClassCount", "matchedClassCount"); + } else { + header.add("url", "loadedClassCount"); + } + table.add(header); + + for (UrlClassStat stat : urlClassStats) { + if (hasMatched) { + table.row(stat.getUrl(), "" + stat.getLoadedClassCount(), "" + stat.getMatchedClassCount()); + } else { + table.row(stat.getUrl(), "" + stat.getLoadedClassCount()); + } + } + process.write(RenderUtil.render(table, process.width())) + .write(com.taobao.arthas.core.util.Constants.EMPTY_STRING); + return; + } + + for (UrlClassStat stat : urlClassStats) { + TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); + + StringBuilder title = new StringBuilder(); + title.append(stat.getUrl()) + .append(" (loaded: ").append(stat.getLoadedClassCount()); + if (hasMatched) { + title.append(", matched: ").append(stat.getMatchedClassCount()); + } + title.append(")"); + table.row(new LabelElement(title.toString()).style(Decoration.bold.bold())); + + List classes = stat.getClasses(); + if (classes != null) { + for (String className : classes) { + table.row(className); + } + } + + if (stat.isTruncated() && classes != null) { + int total = hasMatched ? stat.getMatchedClassCount() : stat.getLoadedClassCount(); + table.row(new LabelElement("... (showing first " + classes.size() + " of " + total + + ", use -n/--limit to change limit)")); + } + + process.write(RenderUtil.render(table, process.width())) + .write("\n"); + } } private void drawUrlStats(CommandProcess process, Map urlStats) { diff --git a/core/src/main/java/com/taobao/arthas/core/mcp/tool/function/klass100/ClassLoaderTool.java b/core/src/main/java/com/taobao/arthas/core/mcp/tool/function/klass100/ClassLoaderTool.java index 30e115ef8ab..368fc2e28f2 100644 --- a/core/src/main/java/com/taobao/arthas/core/mcp/tool/function/klass100/ClassLoaderTool.java +++ b/core/src/main/java/com/taobao/arthas/core/mcp/tool/function/klass100/ClassLoaderTool.java @@ -12,13 +12,14 @@ public class ClassLoaderTool extends AbstractArthasTool { public static final String MODE_TREE = "tree"; public static final String MODE_ALL_CLASSES = "all-classes"; public static final String MODE_URL_STATS = "url-stats"; + public static final String MODE_URL_CLASSES = "url-classes"; @Tool( name = "classloader", - description = "ClassLoader 诊断工具,可以查看类加载器统计信息、继承树、URLs,以及进行资源查找和类加载操作" + description = "ClassLoader 诊断工具,可以查看类加载器统计信息、继承树、URLs,以及进行资源查找和类加载操作。搜索类的场景优先使用 sc 工具" ) public String classloader( - @ToolParam(description = "显示模式:stats(统计信息,默认), instances(实例详情), tree(继承树), all-classes(所有类,慎用), url-stats(URL统计)", required = false) + @ToolParam(description = "显示模式:stats(统计信息,默认), instances(实例详情), tree(继承树), all-classes(所有类,慎用), url-stats(URL统计), url-classes(URL与类关系)", required = false) String mode, @ToolParam(description = "ClassLoader的hashcode(16进制),用于指定特定的ClassLoader", required = false) @@ -33,6 +34,21 @@ public String classloader( @ToolParam(description = "要加载的类名,支持全限定名", required = false) String loadClass, + @ToolParam(description = "详情模式:列出每个 URL/jar 中的类名(等价于 -d),仅在 mode=url-classes 时生效", required = false) + Boolean details, + + @ToolParam(description = "按 jar 包名/URL 关键字过滤,仅在 mode=url-classes 时生效", required = false) + String jar, + + @ToolParam(description = "按类名/包名关键字过滤,仅在 mode=url-classes 时生效", required = false) + String classFilter, + + @ToolParam(description = "是否使用正则匹配 jar/class(等价于 -E),仅在 mode=url-classes 时生效", required = false) + Boolean regex, + + @ToolParam(description = "详情模式下每个 URL/jar 最多展示类数量(等价于 -n),默认 100,仅在 mode=url-classes 时生效", required = false) + Integer limit, + ToolContext toolContext) { StringBuilder cmd = buildCommand("classloader"); @@ -50,6 +66,9 @@ public String classloader( case MODE_URL_STATS: cmd.append(" --url-stat"); break; + case MODE_URL_CLASSES: + cmd.append(" --url-classes"); + break; case MODE_STATS: default: break; @@ -66,6 +85,16 @@ public String classloader( addParameter(cmd, "--load", loadClass); + if (mode != null && MODE_URL_CLASSES.equalsIgnoreCase(mode)) { + addFlag(cmd, "-d", details); + addFlag(cmd, "-E", regex); + if (limit != null && limit > 0) { + addParameter(cmd, "-n", String.valueOf(limit)); + } + addParameter(cmd, "--jar", jar); + addParameter(cmd, "--class", classFilter); + } + return executeSync(toolContext, cmd.toString()); } } diff --git a/core/src/main/java/com/taobao/arthas/core/mcp/tool/function/klass100/SearchClassTool.java b/core/src/main/java/com/taobao/arthas/core/mcp/tool/function/klass100/SearchClassTool.java index 5ccd37a8b67..391a14a0efa 100644 --- a/core/src/main/java/com/taobao/arthas/core/mcp/tool/function/klass100/SearchClassTool.java +++ b/core/src/main/java/com/taobao/arthas/core/mcp/tool/function/klass100/SearchClassTool.java @@ -5,17 +5,80 @@ import com.taobao.arthas.mcp.server.tool.annotation.Tool; import com.taobao.arthas.mcp.server.tool.annotation.ToolParam; +/** + * 类搜索工具,对应 Arthas 的 sc 命令 + * 用于搜索 JVM 中已加载的类,支持通配符和正则表达式匹配 + */ public class SearchClassTool extends AbstractArthasTool { @Tool( - name = "getClassInfo", - description = "获取指定类的详细信息,包括类加载器、方法、字段等信息" + name = "sc", + description = "搜索 JVM 中已加载的类。支持通配符(*)和正则表达式匹配,可查看类的详细信息(类加载器、接口、父类、注解等)和字段信息" ) public String sc( - @ToolParam(description = "要查询的类名,支持全限定名") String className, + @ToolParam(description = "类名模式,支持全限定名。可使用通配符如 *StringUtils 或 org.apache.commons.lang.*,类名分隔符支持 '.' 或 '/'") + String classPattern, + + @ToolParam(description = "是否显示类的详细信息,包括类加载器、代码来源、接口、父类、注解等。默认为 true", required = false) + Boolean detail, + + @ToolParam(description = "是否显示类的所有成员变量(字段)信息。需要 detail 为 true 时才生效", required = false) + Boolean field, + + @ToolParam(description = "是否使用正则表达式匹配类名。默认为 false(使用通配符匹配)", required = false) + Boolean regex, + + @ToolParam(description = "指定 ClassLoader 的 hashcode(16进制),用于在多个 ClassLoader 加载同名类时精确定位", required = false) + String classLoaderHash, + + @ToolParam(description = "指定 ClassLoader 的完整类名,如 sun.misc.Launcher$AppClassLoader,可替代 hashcode", required = false) + String classLoaderClass, + + @ToolParam(description = "指定 ClassLoader 的 toString() 返回值,用于匹配特定的类加载器实例", required = false) + String classLoaderStr, + + @ToolParam(description = "对象展开层级,用于展示更详细的对象结构。默认为 0", required = false) + Integer expand, + + @ToolParam(description = "最大匹配类数量限制(仅在显示详细信息时生效)。默认为 100,防止返回过多结果", required = false) + Integer limit, + ToolContext toolContext) { - StringBuilder cmd = buildCommand("sc -d"); - addParameter(cmd, className); + + StringBuilder cmd = buildCommand("sc"); + + // 默认显示详细信息 + boolean showDetail = (detail == null || detail); + addFlag(cmd, "-d", showDetail); + + // 显示字段信息 + addFlag(cmd, "-f", field); + + // 使用正则表达式匹配 + addFlag(cmd, "-E", regex); + + // 指定类加载器(三种方式,优先使用 hashcode) + if (classLoaderHash != null && !classLoaderHash.trim().isEmpty()) { + addParameter(cmd, "-c", classLoaderHash); + } else if (classLoaderClass != null && !classLoaderClass.trim().isEmpty()) { + addParameter(cmd, "--classLoaderClass", classLoaderClass); + } else if (classLoaderStr != null && !classLoaderStr.trim().isEmpty()) { + addParameter(cmd, "-cs", classLoaderStr); + } + + // 对象展开层级 + if (expand != null && expand > 0) { + addParameter(cmd, "-x", String.valueOf(expand)); + } + + // 最大匹配数量限制 + if (limit != null && limit > 0) { + addParameter(cmd, "-n", String.valueOf(limit)); + } + + // 类名模式(必需参数) + addParameter(cmd, classPattern); + return executeSync(toolContext, cmd.toString()); } } diff --git a/core/src/main/java/com/taobao/arthas/core/mcp/tool/function/klass100/SearchMethodTool.java b/core/src/main/java/com/taobao/arthas/core/mcp/tool/function/klass100/SearchMethodTool.java index 269953c9651..417e137c562 100644 --- a/core/src/main/java/com/taobao/arthas/core/mcp/tool/function/klass100/SearchMethodTool.java +++ b/core/src/main/java/com/taobao/arthas/core/mcp/tool/function/klass100/SearchMethodTool.java @@ -5,18 +5,69 @@ import com.taobao.arthas.mcp.server.tool.annotation.Tool; import com.taobao.arthas.mcp.server.tool.annotation.ToolParam; +/** + * 方法搜索工具,对应 Arthas 的 sm 命令 + * 用于搜索 JVM 中已加载类的方法,支持通配符和正则表达式匹配 + */ public class SearchMethodTool extends AbstractArthasTool { @Tool( - name = "getClassMethods", - description = "获取指定类的所有方法信息" + name = "sm", + description = "搜索 JVM 中已加载类的方法。支持通配符(*)和正则表达式匹配,可查看方法的详细信息(返回类型、参数类型、异常类型、注解等)" ) public String sm( - @ToolParam(description = "要查询的类名,支持全限定名") String className, + @ToolParam(description = "类名模式,支持全限定名。可使用通配符如 *StringUtils 或 org.apache.commons.lang.*,类名分隔符支持 '.' 或 '/'") + String classPattern, + + @ToolParam(description = "方法名模式。可使用通配符如 get* 或 *Name。不指定时匹配所有方法", required = false) + String methodPattern, + + @ToolParam(description = "是否显示方法的详细信息,包括返回类型、参数类型、异常类型、注解、类加载器等。默认为 true", required = false) + Boolean detail, + + @ToolParam(description = "是否使用正则表达式匹配类名和方法名。默认为 false(使用通配符匹配)", required = false) + Boolean regex, + + @ToolParam(description = "指定 ClassLoader 的 hashcode(16进制),用于在多个 ClassLoader 加载同名类时精确定位", required = false) + String classLoaderHash, + + @ToolParam(description = "指定 ClassLoader 的完整类名,如 sun.misc.Launcher$AppClassLoader,可替代 hashcode", required = false) + String classLoaderClass, + + @ToolParam(description = "最大匹配类数量限制。默认为 100,防止返回过多结果", required = false) + Integer limit, + ToolContext toolContext) { + StringBuilder cmd = buildCommand("sm"); - addParameter(cmd, className); + + // 默认显示详细信息 + boolean showDetail = (detail == null || detail); + addFlag(cmd, "-d", showDetail); + + // 使用正则表达式匹配 + addFlag(cmd, "-E", regex); + + // 指定类加载器(两种方式,优先使用 hashcode) + if (classLoaderHash != null && !classLoaderHash.trim().isEmpty()) { + addParameter(cmd, "-c", classLoaderHash); + } else if (classLoaderClass != null && !classLoaderClass.trim().isEmpty()) { + addParameter(cmd, "--classLoaderClass", classLoaderClass); + } + + // 最大匹配类数量限制 + if (limit != null && limit > 0) { + addParameter(cmd, "-n", String.valueOf(limit)); + } + + // 类名模式(必需参数) + addParameter(cmd, classPattern); + + // 方法名模式(可选参数) + if (methodPattern != null && !methodPattern.trim().isEmpty()) { + addParameter(cmd, methodPattern); + } + return executeSync(toolContext, cmd.toString()); } - } diff --git a/core/src/test/java/com/taobao/arthas/core/command/klass100/ClassLoaderCommandUrlClassesTest.java b/core/src/test/java/com/taobao/arthas/core/command/klass100/ClassLoaderCommandUrlClassesTest.java new file mode 100644 index 00000000000..918ae3eee74 --- /dev/null +++ b/core/src/test/java/com/taobao/arthas/core/command/klass100/ClassLoaderCommandUrlClassesTest.java @@ -0,0 +1,33 @@ +package com.taobao.arthas.core.command.klass100; + +import org.junit.Assert; +import org.junit.Test; + +public class ClassLoaderCommandUrlClassesTest { + + @Test + public void testGuessJarNameForNestedJarUrl() { + String url = "jar:file:/app.jar!/BOOT-INF/lib/spring-core-5.3.0.jar!/"; + Assert.assertEquals("spring-core-5.3.0.jar", ClassLoaderCommand.guessJarName(url)); + } + + @Test + public void testGuessJarNameForFileJarUrl() { + String url = "file:/private/tmp/math-game.jar"; + Assert.assertEquals("math-game.jar", ClassLoaderCommand.guessJarName(url)); + } + + @Test + public void testGuessJarNameForDirectoryUrl() { + String url = "file:/private/tmp/classes/"; + Assert.assertEquals("classes", ClassLoaderCommand.guessJarName(url)); + } + + @Test + public void testContainsIgnoreCase() { + Assert.assertTrue(ClassLoaderCommand.containsIgnoreCase("Spring-Core-5.3.0.jar", "spring-core")); + Assert.assertTrue(ClassLoaderCommand.containsIgnoreCase("org.springframework.web", "SpringFramework")); + Assert.assertFalse(ClassLoaderCommand.containsIgnoreCase("demo.MathGame", "org.springframework")); + } +} + diff --git a/site/docs/doc/classloader.md b/site/docs/doc/classloader.md index 0871a00bcc0..e8ec63a93aa 100644 --- a/site/docs/doc/classloader.md +++ b/site/docs/doc/classloader.md @@ -22,6 +22,17 @@ | `[c: r:]` | 用 ClassLoader 去查找 resource | | `[c: load:]` | 用 ClassLoader 去加载指定的类 | +### `--url-classes` 参数说明 + +| 参数名称 | 参数说明 | +| ----------------: | :------------------------------------------------------------------------ | +| `--url-classes` | 统计指定 ClassLoader 中,已加载类与 `codeSource(URL/jar)` 的关系 | +| `-d, --details` | 详情模式:列出每个 URL/jar 加载的类名(建议配合 `-n/--limit` 控制输出量) | +| `--jar ` | 按 jar 包名/URL 关键字过滤(默认包含匹配) | +| `--class ` | 按类名/包名关键字过滤(默认包含匹配) | +| `-E, --regex` | `--jar/--class` 使用正则匹配(默认关键字包含匹配) | +| `-n, --limit ` | 详情模式下,每个 URL/jar 最多展示 N 个类(100 默认) | + ## 使用参考 ### 按类加载类型查看统计信息 @@ -160,3 +171,28 @@ $ classloader --url-stat file:/tmp/jdk1.8/Contents/Home/jre/lib/ext/jaccess.jar file:/tmp/jdk1.8/Contents/Home/jre/lib/ext/zipfs.jar ``` + +### 查看指定 ClassLoader 的类与 jar(URL) 的关系列表 + +`--url-classes` 用于统计指定 ClassLoader 中,类来自哪个 jar(URL),以及每个 jar(URL) 加载了多少类。 + +```bash +$ classloader -c 3d4eac69 --url-classes +sun.misc.Launcher$AppClassLoader@3d4eac69, hash:3d4eac69 + url loadedClassCount + file:/private/tmp/math-game.jar 42 + file:/Users/hengyunabc/.arthas/lib/arthas-agent.jar 15 +Affect(row-cnt:2) cost in 3 ms. +``` + +按 jar 包名关键字过滤并查看详情(列出类名): + +```bash +$ classloader -c 3d4eac69 --url-classes -d --jar math-game +``` + +进一步按包名/关键字过滤(同时会输出 `matchedClassCount` 便于统计): + +```bash +$ classloader -c 3d4eac69 --url-classes --jar spring-core --class org.springframework +``` diff --git a/site/docs/en/doc/classloader.md b/site/docs/en/doc/classloader.md index 7ba94174aea..405a8260b64 100644 --- a/site/docs/en/doc/classloader.md +++ b/site/docs/en/doc/classloader.md @@ -20,6 +20,17 @@ View hierarchy, urls and classes-loading info for the class-loaders. | `[c: r:]` | using ClassLoader to search resource | | `[c: load:]` | using ClassLoader to load class | +### `--url-classes` options + +| Name | Specification | +| ----------------: | :------------------------------------------------------------------------------------------- | +| `--url-classes` | Show relationship between loaded classes and `codeSource(URL/jar)` in a specific ClassLoader | +| `-d, --details` | Details mode: list class names for each URL/jar (use `-n/--limit` to control output) | +| `--jar ` | Filter jar(URL) by keyword (contains match by default) | +| `--class ` | Filter classes by keyword/package (contains match by default) | +| `-E, --regex` | Treat `--jar/--class` as regular expression (keyword match by default) | +| `-n, --limit ` | In details mode, show at most N classes per URL/jar (100 by default) | + ## Usage ### View statistics categorized by class type @@ -156,5 +167,30 @@ $ classloader --url-stat file:/tmp/jdk1.8/Contents/Home/jre/lib/ext/openjsse.jar file:/tmp/jdk1.8/Contents/Home/jre/lib/ext/sunpkcs11.jar file:/tmp/jdk1.8/Contents/Home/jre/lib/ext/jaccess.jar - file:/tmp/jdk1.8/Contents/Home/jre/lib/ext/zipfs.jar +file:/tmp/jdk1.8/Contents/Home/jre/lib/ext/zipfs.jar +``` + +### Show class-to-jar(URL) relationship for a specific ClassLoader + +`--url-classes` shows which jar(URL) the classes come from, and how many classes are loaded for each jar(URL) in the specified ClassLoader. + +```bash +$ classloader -c 3d4eac69 --url-classes +sun.misc.Launcher$AppClassLoader@3d4eac69, hash:3d4eac69 + url loadedClassCount + file:/private/tmp/math-game.jar 42 + file:/Users/hengyunabc/.arthas/lib/arthas-agent.jar 15 +Affect(row-cnt:2) cost in 3 ms. +``` + +Filter by jar name keyword and show details (list class names): + +```bash +$ classloader -c 3d4eac69 --url-classes -d --jar math-game +``` + +Filter further by package/keyword (will also output `matchedClassCount` for statistics): + +```bash +$ classloader -c 3d4eac69 --url-classes --jar spring-core --class org.springframework ```