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..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 */ @@ -61,7 +63,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 +137,7 @@ private void drawUrlStats(CommandProcess process, Map usedUrls = urlStat.getUsedUrls(); table.row(new LabelElement("Used URLs:").style(Decoration.bold.bold())); @@ -200,7 +202,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 +225,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 +235,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,7 +247,7 @@ 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); } 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..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,7 +62,14 @@ public static Element drawSuperClass(ClassDetailVO clazz) { public static Element drawClassLoader(ClassVO clazz) { String[] classloaders = clazz.getClassloader(); - return drawTree(classloaders); + 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]); + } + 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 new file mode 100644 index 00000000000..ce164cd480d --- /dev/null +++ b/core/src/test/java/com/taobao/arthas/core/command/view/ClassLoaderViewTest.java @@ -0,0 +1,150 @@ +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.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; + +import java.util.Collections; + +public class ClassLoaderViewTest { + + @Test + public void shouldEscapeLineBreaksInClassLoaderTable() { + ClassLoaderVO classLoader = new ClassLoaderVO(); + 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("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 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 shouldRenderClassDetailWithNullClassLoaderTree() { + ClassVO classInfo = new ClassVO(); + + String output = RenderUtil.render(TypeRenderUtils.drawClassLoader(classInfo), 200); + + Assert.assertNotNull(output); + } + + @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); + } +}