From eb61270b01dd6d521ecbce2aabd44fb997f5fe4e Mon Sep 17 00:00:00 2001 From: bubu <1654388696@qq.com> Date: Sat, 6 Jun 2026 11:06:33 +0800 Subject: [PATCH 1/3] fix: escape multiline classloader names in view --- .../core/command/view/ClassLoaderView.java | 24 +++- .../command/view/ClassLoaderViewTest.java | 124 ++++++++++++++++++ 2 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 core/src/test/java/com/taobao/arthas/core/command/view/ClassLoaderViewTest.java 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 416b2876d9c..9421e86e82d 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 @@ -61,7 +61,7 @@ public void draw(CommandProcess process, ClassLoaderModel result) { private void drawUrlClassStats(CommandProcess process, ClassLoaderVO classLoader, List urlClassStats, boolean detail) { if (classLoader != null) { - process.write(classLoader.getName() + ", hash:" + classLoader.getHash() + "\n"); + process.write(formatClassLoaderText(classLoader.getName()) + ", hash:" + classLoader.getHash() + "\n"); } boolean hasMatched = false; @@ -135,7 +135,7 @@ private void drawUrlStats(CommandProcess process, Map usedUrls = urlStat.getUsedUrls(); table.row(new LabelElement("Used URLs:").style(Decoration.bold.bold())); @@ -200,7 +200,8 @@ private void drawResources(CommandProcess process, List resources) { private Element renderClasses(ClassSetVO classSetVO) { TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); if (classSetVO.getSegment() == 0) { - table.row(new LabelElement("hash:" + classSetVO.getClassloader().getHash() + ", " + classSetVO.getClassloader().getName()) + table.row(new LabelElement("hash:" + classSetVO.getClassloader().getHash() + ", " + + formatClassLoaderText(classSetVO.getClassloader().getName())) .style(Decoration.bold.bold())); } for (String className : classSetVO.getClasses()) { @@ -222,7 +223,8 @@ private static TableElement renderTable(Collection classLoaderInf TableElement table = new TableElement().leftCellPadding(1).rightCellPadding(1); table.add(new RowElement().style(Decoration.bold.bold()).add("name", "loadedCount", "hash", "parent")); for (ClassLoaderVO classLoaderVO : classLoaderInfos) { - table.row(classLoaderVO.getName(), "" + classLoaderVO.getLoadedCount(), classLoaderVO.getHash(), classLoaderVO.getParent()); + table.row(formatClassLoaderText(classLoaderVO.getName()), "" + classLoaderVO.getLoadedCount(), + classLoaderVO.getHash(), formatClassLoaderText(classLoaderVO.getParent())); } return table; } @@ -231,7 +233,7 @@ private static TableElement renderTable(Collection classLoaderInf private static Element renderTree(Collection classLoaderInfos) { TreeElement root = new TreeElement(); for (ClassLoaderVO classLoader : classLoaderInfos) { - TreeElement child = new TreeElement(classLoader.getName()); + TreeElement child = new TreeElement(formatClassLoaderText(classLoader.getName())); root.addChild(child); renderSubtree(child, classLoader); } @@ -243,9 +245,19 @@ private static void renderSubtree(TreeElement parent, ClassLoaderVO parentClassL return; } for (ClassLoaderVO childClassLoader : parentClassLoader.getChildren()) { - TreeElement child = new TreeElement(childClassLoader.getName()); + TreeElement child = new TreeElement(formatClassLoaderText(childClassLoader.getName())); parent.addChild(child); renderSubtree(child, childClassLoader); } } + + private static String formatClassLoaderText(String value) { + if (value == null) { + return null; + } + return value.replace("\r\n", "\\n") + .replace("\r", "\\n") + .replace("\n", "\\n") + .replace("\t", " "); + } } diff --git a/core/src/test/java/com/taobao/arthas/core/command/view/ClassLoaderViewTest.java b/core/src/test/java/com/taobao/arthas/core/command/view/ClassLoaderViewTest.java new file mode 100644 index 00000000000..6c7edeca03b --- /dev/null +++ b/core/src/test/java/com/taobao/arthas/core/command/view/ClassLoaderViewTest.java @@ -0,0 +1,124 @@ +package com.taobao.arthas.core.command.view; + +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.ClassLoaderModel; +import com.taobao.arthas.core.command.model.ClassLoaderVO; +import com.taobao.arthas.core.command.model.ClassSetVO; +import com.taobao.arthas.core.shell.command.CommandProcess; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.Collections; + +public class ClassLoaderViewTest { + + @Test + public void shouldEscapeLineBreaksInClassLoaderTable() { + ClassLoaderVO classLoader = new ClassLoaderVO(); + classLoader.setName("TomcatEmbeddedWebappClassLoader\r\n context: /demo"); + classLoader.setParent("jdk.internal.loader.ClassLoaders$AppClassLoader\n parent detail"); + classLoader.setLoadedCount(12); + classLoader.setHash("1a2b3c"); + + String output = renderClassLoaders(classLoader, false); + + Assert.assertTrue(output.contains("TomcatEmbeddedWebappClassLoader\\n context: /demo")); + Assert.assertTrue(output.contains("jdk.internal.loader.ClassLoaders$AppClassLoader\\n parent detail")); + } + + @Test + public void shouldEscapeLineBreaksInClassLoaderTree() { + ClassLoaderVO root = new ClassLoaderVO(); + root.setName("RootClassLoader\r\n root detail"); + root.setHash("root"); + + ClassLoaderVO child = new ClassLoaderVO(); + child.setName("ChildClassLoader\n child detail"); + child.setHash("child"); + root.addChild(child); + + String output = renderClassLoaders(root, true); + + Assert.assertTrue(output.contains("RootClassLoader\\n root detail")); + Assert.assertTrue(output.contains("ChildClassLoader\\n child detail")); + } + + @Test + public void shouldEscapeLineBreaksInUrlStatsHeader() { + ClassLoaderVO classLoader = new ClassLoaderVO(); + classLoader.setName("TomcatEmbeddedWebappClassLoader\r\n context: /demo"); + classLoader.setHash("urlstat"); + + ClassLoaderUrlStat urlStat = new ClassLoaderUrlStat(Collections.singletonList("file:/tmp/demo.jar"), + Collections.emptyList()); + + ClassLoaderModel model = new ClassLoaderModel(); + model.setUrlStats(Collections.singletonMap(classLoader, urlStat)); + + String output = renderView(model); + + Assert.assertTrue(output.contains("TomcatEmbeddedWebappClassLoader\\n context: /demo, hash:urlstat")); + } + + @Test + public void shouldEscapeLineBreaksInUrlClassStatsHeader() { + ClassLoaderVO classLoader = new ClassLoaderVO(); + classLoader.setName("TomcatEmbeddedWebappClassLoader\r\n context: /demo"); + classLoader.setHash("urlclasses"); + + UrlClassStat stat = new UrlClassStat(); + stat.setUrl("file:/tmp/demo.jar"); + stat.setLoadedClassCount(3); + + ClassLoaderModel model = new ClassLoaderModel() + .setClassLoader(classLoader) + .setUrlClassStats(Collections.singletonList(stat)) + .setUrlClassStatsDetail(false); + + String output = renderView(model); + + Assert.assertTrue(output.contains("TomcatEmbeddedWebappClassLoader\\n context: /demo, hash:urlclasses")); + } + + @Test + public void shouldEscapeLineBreaksInAllClassesHeader() { + ClassLoaderVO classLoader = new ClassLoaderVO(); + classLoader.setName("TomcatEmbeddedWebappClassLoader\r\n context: /demo"); + classLoader.setHash("allclasses"); + + ClassSetVO classSet = new ClassSetVO(classLoader, Collections.singletonList("demo.SampleClass")); + + ClassLoaderModel model = new ClassLoaderModel().setClassSet(classSet); + + String output = renderView(model); + + Assert.assertTrue(output.contains("hash:allclasses, TomcatEmbeddedWebappClassLoader\\n context: /demo")); + } + + private String renderClassLoaders(ClassLoaderVO classLoader, boolean tree) { + return render(process -> ClassLoaderView.drawClassLoaders(process, Collections.singletonList(classLoader), tree)); + } + + private String renderView(ClassLoaderModel model) { + return render(process -> new ClassLoaderView().draw(process, model)); + } + + private String render(RenderAction action) { + CommandProcess process = Mockito.mock(CommandProcess.class); + StringBuilder output = new StringBuilder(); + Mockito.when(process.width()).thenReturn(200); + Mockito.when(process.write(Mockito.anyString())).thenAnswer(invocation -> { + output.append(invocation.getArgument(0, String.class)); + return process; + }); + + action.render(process); + return output.toString(); + } + + private interface RenderAction { + void render(CommandProcess process); + } +} From 74d1dd37f832ae6f2fc2d387a59069fa3cb71736 Mon Sep 17 00:00:00 2001 From: hengyunabc Date: Thu, 11 Jun 2026 11:03:16 +0800 Subject: [PATCH 2/3] fix: escape classloader text in detail renderers --- .../core/command/view/ClassLoaderView.java | 12 ++---------- .../com/taobao/arthas/core/util/ClassUtils.java | 10 ++++++++++ .../arthas/core/util/TypeRenderUtils.java | 6 +++++- .../core/command/view/ClassLoaderViewTest.java | 17 +++++++++++++++++ 4 files changed, 34 insertions(+), 11 deletions(-) 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 9421e86e82d..d95d34ff272 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 @@ -18,6 +18,8 @@ import java.util.Map; import java.util.Map.Entry; +import static com.taobao.arthas.core.util.ClassUtils.formatClassLoaderText; + /** * @author gongdewei 2020/4/21 */ @@ -250,14 +252,4 @@ private static void renderSubtree(TreeElement parent, ClassLoaderVO parentClassL renderSubtree(child, childClassLoader); } } - - private static String formatClassLoaderText(String value) { - if (value == null) { - return null; - } - return value.replace("\r\n", "\\n") - .replace("\r", "\\n") - .replace("\n", "\\n") - .replace("\t", " "); - } } diff --git a/core/src/main/java/com/taobao/arthas/core/util/ClassUtils.java b/core/src/main/java/com/taobao/arthas/core/util/ClassUtils.java index f28c0729883..113d74f95d6 100644 --- a/core/src/main/java/com/taobao/arthas/core/util/ClassUtils.java +++ b/core/src/main/java/com/taobao/arthas/core/util/ClassUtils.java @@ -198,6 +198,16 @@ public static ClassLoaderVO createClassLoaderVO(ClassLoader classLoader) { return classLoaderVO; } + public static String formatClassLoaderText(String value) { + if (value == null) { + return null; + } + return value.replace("\r\n", "\\n") + .replace("\r", "\\n") + .replace("\n", "\\n") + .replace("\t", " "); + } + public static List createClassLoaderVOList(Collection classLoaders) { List classLoaderVOList = new ArrayList(); for (ClassLoader classLoader : classLoaders) { diff --git a/core/src/main/java/com/taobao/arthas/core/util/TypeRenderUtils.java b/core/src/main/java/com/taobao/arthas/core/util/TypeRenderUtils.java index 6189f8699f8..853946169b4 100644 --- a/core/src/main/java/com/taobao/arthas/core/util/TypeRenderUtils.java +++ b/core/src/main/java/com/taobao/arthas/core/util/TypeRenderUtils.java @@ -62,7 +62,11 @@ public static Element drawSuperClass(ClassDetailVO clazz) { public static Element drawClassLoader(ClassVO clazz) { String[] classloaders = clazz.getClassloader(); - return drawTree(classloaders); + String[] formattedClassloaders = new String[classloaders.length]; + for (int i = 0; i < classloaders.length; i++) { + formattedClassloaders[i] = ClassUtils.formatClassLoaderText(classloaders[i]); + } + return drawTree(formattedClassloaders); } public static Element drawTree(String[] nodes) { diff --git a/core/src/test/java/com/taobao/arthas/core/command/view/ClassLoaderViewTest.java b/core/src/test/java/com/taobao/arthas/core/command/view/ClassLoaderViewTest.java index 6c7edeca03b..f20250cac4e 100644 --- a/core/src/test/java/com/taobao/arthas/core/command/view/ClassLoaderViewTest.java +++ b/core/src/test/java/com/taobao/arthas/core/command/view/ClassLoaderViewTest.java @@ -2,10 +2,13 @@ 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.ClassVO; import com.taobao.arthas.core.command.model.ClassLoaderModel; import com.taobao.arthas.core.command.model.ClassLoaderVO; import com.taobao.arthas.core.command.model.ClassSetVO; import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.TypeRenderUtils; +import com.taobao.text.util.RenderUtil; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; @@ -45,6 +48,20 @@ public void shouldEscapeLineBreaksInClassLoaderTree() { Assert.assertTrue(output.contains("ChildClassLoader\\n child detail")); } + @Test + public void shouldEscapeLineBreaksInClassDetailClassLoaderTree() { + ClassVO classInfo = new ClassVO(); + classInfo.setClassloader(new String[] { + "TomcatEmbeddedWebappClassLoader\r\n context: /demo", + "jdk.internal.loader.ClassLoaders$AppClassLoader\n parent detail" + }); + + String output = RenderUtil.render(TypeRenderUtils.drawClassLoader(classInfo), 200); + + Assert.assertTrue(output.contains("TomcatEmbeddedWebappClassLoader\\n context: /demo")); + Assert.assertTrue(output.contains("jdk.internal.loader.ClassLoaders$AppClassLoader\\n parent detail")); + } + @Test public void shouldEscapeLineBreaksInUrlStatsHeader() { ClassLoaderVO classLoader = new ClassLoaderVO(); From d4119eada28f0af25a34ed6223e797910273dbc5 Mon Sep 17 00:00:00 2001 From: hengyunabc Date: Thu, 11 Jun 2026 11:20:27 +0800 Subject: [PATCH 3/3] test: cover classloader tab and null rendering --- .../taobao/arthas/core/util/TypeRenderUtils.java | 3 +++ .../core/command/view/ClassLoaderViewTest.java | 13 +++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/com/taobao/arthas/core/util/TypeRenderUtils.java b/core/src/main/java/com/taobao/arthas/core/util/TypeRenderUtils.java index 853946169b4..8df65c65699 100644 --- a/core/src/main/java/com/taobao/arthas/core/util/TypeRenderUtils.java +++ b/core/src/main/java/com/taobao/arthas/core/util/TypeRenderUtils.java @@ -62,6 +62,9 @@ public static Element drawSuperClass(ClassDetailVO clazz) { public static Element drawClassLoader(ClassVO clazz) { String[] classloaders = clazz.getClassloader(); + if (classloaders == null) { + return drawTree(new String[0]); + } String[] formattedClassloaders = new String[classloaders.length]; for (int i = 0; i < classloaders.length; i++) { formattedClassloaders[i] = ClassUtils.formatClassLoaderText(classloaders[i]); diff --git a/core/src/test/java/com/taobao/arthas/core/command/view/ClassLoaderViewTest.java b/core/src/test/java/com/taobao/arthas/core/command/view/ClassLoaderViewTest.java index f20250cac4e..ce164cd480d 100644 --- a/core/src/test/java/com/taobao/arthas/core/command/view/ClassLoaderViewTest.java +++ b/core/src/test/java/com/taobao/arthas/core/command/view/ClassLoaderViewTest.java @@ -20,14 +20,14 @@ public class ClassLoaderViewTest { @Test public void shouldEscapeLineBreaksInClassLoaderTable() { ClassLoaderVO classLoader = new ClassLoaderVO(); - classLoader.setName("TomcatEmbeddedWebappClassLoader\r\n context: /demo"); + classLoader.setName("TomcatEmbeddedWebappClassLoader\r\n context:\t/demo"); classLoader.setParent("jdk.internal.loader.ClassLoaders$AppClassLoader\n parent detail"); classLoader.setLoadedCount(12); classLoader.setHash("1a2b3c"); String output = renderClassLoaders(classLoader, false); - Assert.assertTrue(output.contains("TomcatEmbeddedWebappClassLoader\\n context: /demo")); + Assert.assertTrue(output.contains("TomcatEmbeddedWebappClassLoader\\n context: /demo")); Assert.assertTrue(output.contains("jdk.internal.loader.ClassLoaders$AppClassLoader\\n parent detail")); } @@ -62,6 +62,15 @@ public void shouldEscapeLineBreaksInClassDetailClassLoaderTree() { Assert.assertTrue(output.contains("jdk.internal.loader.ClassLoaders$AppClassLoader\\n parent detail")); } + @Test + public void shouldRenderClassDetailWithNullClassLoaderTree() { + ClassVO classInfo = new ClassVO(); + + String output = RenderUtil.render(TypeRenderUtils.drawClassLoader(classInfo), 200); + + Assert.assertNotNull(output); + } + @Test public void shouldEscapeLineBreaksInUrlStatsHeader() { ClassLoaderVO classLoader = new ClassLoaderVO();