diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/TransformerManager.java b/core/src/main/java/com/taobao/arthas/core/advisor/TransformerManager.java index 73bf2c15d..5f0947598 100644 --- a/core/src/main/java/com/taobao/arthas/core/advisor/TransformerManager.java +++ b/core/src/main/java/com/taobao/arthas/core/advisor/TransformerManager.java @@ -23,6 +23,11 @@ public class TransformerManager { private Instrumentation instrumentation; private List watchTransformers = new CopyOnWriteArrayList(); private List traceTransformers = new CopyOnWriteArrayList(); + + /** + * 先于 watch/trace的 Transformer TODO 改进为全部用 order 排序? + */ + private List reTransformers = new CopyOnWriteArrayList(); private ClassFileTransformer classFileTransformer; @@ -34,6 +39,13 @@ public class TransformerManager { @Override public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { + for (ClassFileTransformer classFileTransformer : reTransformers) { + byte[] transformResult = classFileTransformer.transform(loader, className, classBeingRedefined, + protectionDomain, classfileBuffer); + if (transformResult != null) { + classfileBuffer = transformResult; + } + } for (ClassFileTransformer classFileTransformer : watchTransformers) { byte[] transformResult = classFileTransformer.transform(loader, className, classBeingRedefined, @@ -66,12 +78,18 @@ public class TransformerManager { } } + public void addRetransformer(ClassFileTransformer transformer) { + reTransformers.add(transformer); + } + public void removeTransformer(ClassFileTransformer transformer) { + reTransformers.remove(transformer); watchTransformers.remove(transformer); traceTransformers.remove(transformer); } public void destroy() { + reTransformers.clear(); watchTransformers.clear(); traceTransformers.clear(); instrumentation.removeTransformer(classFileTransformer); diff --git a/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java b/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java index db7762d7e..f240e5ac2 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java +++ b/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java @@ -28,6 +28,7 @@ import com.taobao.arthas.core.command.klass100.JadCommand; import com.taobao.arthas.core.command.klass100.MemoryCompilerCommand; import com.taobao.arthas.core.command.klass100.OgnlCommand; import com.taobao.arthas.core.command.klass100.RedefineCommand; +import com.taobao.arthas.core.command.klass100.RetransformCommand; import com.taobao.arthas.core.command.klass100.SearchClassCommand; import com.taobao.arthas.core.command.klass100.SearchMethodCommand; import com.taobao.arthas.core.command.logger.LoggerCommand; @@ -86,6 +87,7 @@ public class BuiltinCommandPack implements CommandResolver { commands.add(Command.create(OgnlCommand.class)); commands.add(Command.create(MemoryCompilerCommand.class)); commands.add(Command.create(RedefineCommand.class)); + commands.add(Command.create(RetransformCommand.class)); commands.add(Command.create(DashboardCommand.class)); commands.add(Command.create(DumpClassCommand.class)); commands.add(Command.create(HeapDumpCommand.class)); diff --git a/core/src/main/java/com/taobao/arthas/core/command/klass100/RetransformCommand.java b/core/src/main/java/com/taobao/arthas/core/command/klass100/RetransformCommand.java new file mode 100644 index 000000000..e2756ea04 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/klass100/RetransformCommand.java @@ -0,0 +1,498 @@ +package com.taobao.arthas.core.command.klass100; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.IllegalClassFormatException; +import java.lang.instrument.Instrumentation; +import java.security.ProtectionDomain; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import com.alibaba.arthas.deps.org.slf4j.Logger; +import com.alibaba.arthas.deps.org.slf4j.LoggerFactory; +import com.alibaba.deps.org.objectweb.asm.ClassReader; +import com.taobao.arthas.core.advisor.TransformerManager; +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.command.model.ClassLoaderVO; +import com.taobao.arthas.core.command.model.RetransformModel; +import com.taobao.arthas.core.server.ArthasBootstrap; +import com.taobao.arthas.core.shell.cli.CliToken; +import com.taobao.arthas.core.shell.cli.Completion; +import com.taobao.arthas.core.shell.cli.CompletionUtils; +import com.taobao.arthas.core.shell.command.AnnotatedCommand; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.ClassLoaderUtils; +import com.taobao.arthas.core.util.ClassUtils; +import com.taobao.arthas.core.util.SearchUtils; +import com.taobao.middleware.cli.annotations.Argument; +import com.taobao.middleware.cli.annotations.DefaultValue; +import com.taobao.middleware.cli.annotations.Description; +import com.taobao.middleware.cli.annotations.Name; +import com.taobao.middleware.cli.annotations.Option; +import com.taobao.middleware.cli.annotations.Summary; + +/** + * + * Retransform Classes. + * + * @author hengyunabc 2021-01-05 + * @see java.lang.instrument.Instrumentation#retransformClasses(Class...) + */ +@Name("retransform") +@Summary("Retransform classes. @see Instrumentation#retransformClasses(Class...)") +@Description(Constants.EXAMPLE + " retransform /tmp/Test.class\n" + + " retransform -l \n" + + " retransform -d 1 # delete retransform entry\n" + + " retransform --deleteAll # delete all retransform entries\n" + + " retransform --classPattern demo.* # triger retransform classes\n" + + " retransform -c 327a647b /tmp/Test.class /tmp/Test\\$Inner.class \n" + + " retransform --classLoaderClass 'sun.misc.Launcher$AppClassLoader' /tmp/Test.class\n" + + Constants.WIKI + Constants.WIKI_HOME + + "retransform") +public class RetransformCommand extends AnnotatedCommand { + private static final Logger logger = LoggerFactory.getLogger(RetransformCommand.class); + private static final int MAX_FILE_SIZE = 10 * 1024 * 1024; + + private static volatile List retransformEntries = new ArrayList(); + private static volatile ClassFileTransformer transformer = null; + + private String hashCode; + private String classLoaderClass; + + private List paths; + + private boolean list; + + private int delete = -1; + + private boolean deleteAll; + + private String classPattern; + + private int limit; + + @Option(shortName = "l", longName = "list", flag = true) + @Description("list all retransform entry.") + public void setList(boolean list) { + this.list = list; + } + + @Option(shortName = "d", longName = "delete") + @Description("delete retransform entry by id.") + public void setDelete(int delete) { + this.delete = delete; + } + + @Option(longName = "deleteAll", flag = true) + @Description("delete all retransform entries.") + public void setDeleteAll(boolean deleteAll) { + this.deleteAll = deleteAll; + } + + @Option(longName = "classPattern") + @Description("trigger retransform matched classes by class pattern.") + public void setClassPattern(String classPattern) { + this.classPattern = classPattern; + } + + @Option(shortName = "c", longName = "classloader") + @Description("classLoader hashcode") + public void setHashCode(String hashCode) { + this.hashCode = hashCode; + } + + @Option(longName = "classLoaderClass") + @Description("The class name of the special class's classLoader.") + public void setClassLoaderClass(String classLoaderClass) { + this.classLoaderClass = classLoaderClass; + } + + @Argument(argName = "classfilePaths", index = 0, required = false) + @Description(".class file paths") + public void setPaths(List paths) { + this.paths = paths; + } + + @Option(longName = "limit") + @Description("The limit of dump classes size, default value is 5") + @DefaultValue("50") + public void setLimit(int limit) { + this.limit = limit; + } + + private static void initTransformer() { + if (transformer != null) { + return; + } else { + synchronized (RetransformCommand.class) { + if (transformer == null) { + transformer = new RetransformClassFileTransformer(); + TransformerManager transformerManager = ArthasBootstrap.getInstance().getTransformerManager(); + transformerManager.addRetransformer(transformer); + } + } + } + } + + @Override + public void process(CommandProcess process) { + initTransformer(); + + RetransformModel retransformModel = new RetransformModel(); + Instrumentation inst = process.session().getInstrumentation(); + + if (this.list) { + List retransformEntryList = allRetransformEntries(); + retransformModel.setRetransformEntries(retransformEntryList); + process.appendResult(retransformModel); + process.end(); + return; + } else if (this.deleteAll) { + deleteAllRetransformEntry(); + process.appendResult(retransformModel); + process.end(); + return; + } else if (this.delete > 0) { + deleteRetransformEntry(this.delete); + process.end(); + return; + } else if (this.classPattern != null) { + Set> searchClass = SearchUtils.searchClass(inst, classPattern, false, this.hashCode); + if (searchClass.isEmpty()) { + process.end(-1, "These classes are not found in the JVM and may not be loaded: " + classPattern); + return; + } + + if (searchClass.size() > limit) { + process.end(-1, "match classes size: " + searchClass.size() + ", more than limit: " + limit + + ", It is recommended to use a more precise class pattern."); + } + try { + inst.retransformClasses(searchClass.toArray(new Class[0])); + for (Class clazz : searchClass) { + retransformModel.addRetransformClass(clazz.getName()); + } + process.appendResult(retransformModel); + process.end(); + return; + } catch (Throwable e) { + String message = "retransform error! " + e.toString(); + logger.error(message, e); + process.end(-1, message); + return; + } + } + + for (String path : paths) { + File file = new File(path); + if (!file.exists()) { + process.end(-1, "file does not exist, path:" + path); + return; + } + if (!file.isFile()) { + process.end(-1, "not a normal file, path:" + path); + return; + } + if (file.length() >= MAX_FILE_SIZE) { + process.end(-1, "file size: " + file.length() + " >= " + MAX_FILE_SIZE + ", path: " + path); + return; + } + } + + Map bytesMap = new HashMap(); + for (String path : paths) { + RandomAccessFile f = null; + try { + f = new RandomAccessFile(path, "r"); + final byte[] bytes = new byte[(int) f.length()]; + f.readFully(bytes); + + final String clazzName = readClassName(bytes); + + bytesMap.put(clazzName, bytes); + + } catch (Exception e) { + logger.warn("load class file failed: " + path, e); + process.end(-1, "load class file failed: " + path + ", error: " + e); + return; + } finally { + if (f != null) { + try { + f.close(); + } catch (IOException e) { + // ignore + } + } + } + } + + if (bytesMap.size() != paths.size()) { + process.end(-1, "paths may contains same class name!"); + return; + } + + List retransformEntryList = new ArrayList(); + + List> classList = new ArrayList>(); + + for (Class clazz : inst.getAllLoadedClasses()) { + if (bytesMap.containsKey(clazz.getName())) { + + if (hashCode == null && classLoaderClass != null) { + List matchedClassLoaders = ClassLoaderUtils.getClassLoaderByClassName(inst, + classLoaderClass); + if (matchedClassLoaders.size() == 1) { + hashCode = Integer.toHexString(matchedClassLoaders.get(0).hashCode()); + } else if (matchedClassLoaders.size() > 1) { + Collection classLoaderVOList = ClassUtils + .createClassLoaderVOList(matchedClassLoaders); + retransformModel.setClassLoaderClass(classLoaderClass) + .setMatchedClassLoaders(classLoaderVOList); + process.appendResult(retransformModel); + process.end(-1, + "Found more than one classloader by class name, please specify classloader with '-c '"); + return; + } else { + process.end(-1, "Can not find classloader by class name: " + classLoaderClass + "."); + return; + } + } + + ClassLoader classLoader = clazz.getClassLoader(); + if (classLoader != null && hashCode != null + && !Integer.toHexString(classLoader.hashCode()).equals(hashCode)) { + continue; + } + + RetransformEntry retransformEntry = new RetransformEntry(clazz.getName(), bytesMap.get(clazz.getName()), + hashCode, classLoaderClass); + retransformEntryList.add(retransformEntry); + classList.add(clazz); + retransformModel.addRetransformClass(clazz.getName()); + + addRetransformEntry(retransformEntry); + + logger.info("Try retransform class name: {}, ClassLoader: {}", clazz.getName(), clazz.getClassLoader()); + } + } + + try { + if (retransformEntryList.isEmpty()) { + process.end(-1, "These classes are not found in the JVM and may not be loaded: " + bytesMap.keySet()); + return; + } + + inst.retransformClasses(classList.toArray(new Class[0])); + + process.appendResult(retransformModel); + process.end(); + } catch (Throwable e) { + String message = "retransform error! " + e.toString(); + logger.error(message, e); + process.end(-1, message); + } + + } + + private static String readClassName(final byte[] bytes) { + return new ClassReader(bytes).getClassName().replace('/', '.'); + } + + @Override + public void complete(Completion completion) { + List tokens = completion.lineTokens(); + + if (CompletionUtils.shouldCompleteOption(completion, "--classPattern")) { + CompletionUtils.completeClassName(completion); + return; + } + + for (CliToken token : tokens) { + String tokenStr = token.value(); + if (tokenStr != null && tokenStr.startsWith("-")) { + super.complete(completion); + return; + } + } + + // 最后,没有有 - 开头的,才尝试补全 file path + if (!CompletionUtils.completeFilePath(completion)) { + super.complete(completion); + } + } + + public static class RetransformEntry { + private static final AtomicInteger counter = new AtomicInteger(0); + private int id; + private String className; + private byte[] bytes; + private String hashCode; + private String classLoaderClass; + + /** + * 被 transform 触发次数 + */ + private int transformCount = 0; + + public RetransformEntry(String className, byte[] bytes, String hashCode, String classLoaderClass) { + id = counter.incrementAndGet(); + this.className = className; + this.bytes = bytes; + this.hashCode = hashCode; + this.classLoaderClass = classLoaderClass; + } + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getTransformCount() { + return transformCount; + } + + public void setTransformCount(int transformCount) { + this.transformCount = transformCount; + } + + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + public byte[] getBytes() { + return bytes; + } + + public void setBytes(byte[] bytes) { + this.bytes = bytes; + } + + public String getHashCode() { + return hashCode; + } + + public void setHashCode(String hashCode) { + this.hashCode = hashCode; + } + + public String getClassLoaderClass() { + return classLoaderClass; + } + + public void setClassLoaderClass(String classLoaderClass) { + this.classLoaderClass = classLoaderClass; + } + } + + public static synchronized void addRetransformEntry(RetransformEntry retransformEntry) { + List tmp = new ArrayList(); + tmp.addAll(retransformEntries); + tmp.add(retransformEntry); + Collections.sort(tmp, new Comparator() { + @Override + public int compare(RetransformEntry entry1, RetransformEntry entry2) { + return entry1.getId() - entry2.getId(); + } + }); + retransformEntries = tmp; + } + + public static synchronized RetransformEntry deleteRetransformEntry(int id) { + RetransformEntry result = null; + List tmp = new ArrayList(); + for (RetransformEntry entry : retransformEntries) { + if (entry.getId() != id) { + tmp.add(entry); + } else { + result = entry; + } + } + retransformEntries = tmp; + return result; + } + + public static List allRetransformEntries() { + return retransformEntries; + } + + public static synchronized void deleteAllRetransformEntry() { + retransformEntries = new ArrayList(); + } + + static class RetransformClassFileTransformer implements ClassFileTransformer { + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, + ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { + + if (className == null) { + return null; + } + + className = className.replace('/', '.'); + + List allRetransformEntries = allRetransformEntries(); + // 倒序,因为要执行的配置生效 + ListIterator listIterator = allRetransformEntries + .listIterator(allRetransformEntries.size()); + while (listIterator.hasPrevious()) { + RetransformEntry retransformEntry = listIterator.previous(); + int id = retransformEntry.getId(); + // 判断类名是否一致 + boolean updateFlag = false; + // 类名一致,则看是否要比较 loader,如果不需要比较 loader,则认为成功 + if (className.equals(retransformEntry.getClassName())) { + if (retransformEntry.getClassLoaderClass() != null || retransformEntry.getHashCode() != null) { + updateFlag = isLoaderMatch(retransformEntry, loader); + } else { + updateFlag = true; + } + } + + if (updateFlag) { + logger.info("RetransformCommand match class: {}, id: {}, classLoaderClass: {}, hashCode: {}", + className, id, retransformEntry.getClassLoaderClass(), retransformEntry.getHashCode()); + return retransformEntry.getBytes(); + } + + } + + return null; + } + + private boolean isLoaderMatch(RetransformEntry retransformEntry, ClassLoader loader) { + if (loader == null) { + return false; + } + if (retransformEntry.getClassLoaderClass() != null) { + if (loader.getClass().getName().equals(retransformEntry.getClassLoaderClass())) { + return true; + } + } + if (retransformEntry.getHashCode() != null) { + String hashCode = Integer.toHexString(loader.hashCode()); + if (hashCode.equals(retransformEntry.getHashCode())) { + return true; + } + } + return false; + } + + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/model/RetransformModel.java b/core/src/main/java/com/taobao/arthas/core/command/model/RetransformModel.java new file mode 100644 index 000000000..c36885931 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/model/RetransformModel.java @@ -0,0 +1,96 @@ +package com.taobao.arthas.core.command.model; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.taobao.arthas.core.command.klass100.RetransformCommand.RetransformEntry; +import com.taobao.arthas.core.util.ClassUtils; + +/** + * + * @author hengyunabc 2021-01-06 + * + */ +public class RetransformModel extends ResultModel { + + private int retransformCount; + + private List retransformClasses; + private Collection matchedClassLoaders; + private String classLoaderClass; + + private List retransformEntries; + + private RetransformEntry deletedRetransformEntry; + +// private List trigger + +// List classVOs = ClassUtils.createClassVOList(matchedClasses); + public RetransformModel() { + } + + public void addRetransformClass(String className) { + if (retransformClasses == null) { + retransformClasses = new ArrayList(); + } + retransformClasses.add(className); + retransformCount++; + } + + public int getRetransformCount() { + return retransformCount; + } + + public void setRetransformCount(int retransformCount) { + this.retransformCount = retransformCount; + } + + public List getRetransformClasses() { + return retransformClasses; + } + + public void setRetransformClasses(List retransformClasses) { + this.retransformClasses = retransformClasses; + } + + public String getClassLoaderClass() { + return classLoaderClass; + } + + public RetransformModel setClassLoaderClass(String classLoaderClass) { + this.classLoaderClass = classLoaderClass; + return this; + } + + public Collection getMatchedClassLoaders() { + return matchedClassLoaders; + } + + public RetransformModel setMatchedClassLoaders(Collection matchedClassLoaders) { + this.matchedClassLoaders = matchedClassLoaders; + return this; + } + + public List getRetransformEntries() { + return retransformEntries; + } + + public void setRetransformEntries(List retransformEntries) { + this.retransformEntries = retransformEntries; + } + + public RetransformEntry getDeletedRetransformEntry() { + return deletedRetransformEntry; + } + + public void setDeletedRetransformEntry(RetransformEntry deletedRetransformEntry) { + this.deletedRetransformEntry = deletedRetransformEntry; + } + + @Override + public String getType() { + return "retransform"; + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java b/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java index e6ded8dcb..912e15c98 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java +++ b/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java @@ -57,6 +57,7 @@ public class ResultViewResolver { registerView(MemoryCompilerView.class); registerView(OgnlView.class); registerView(RedefineView.class); + registerView(RetransformView.class); registerView(SearchClassView.class); registerView(SearchMethodView.class); diff --git a/core/src/main/java/com/taobao/arthas/core/command/view/RetransformView.java b/core/src/main/java/com/taobao/arthas/core/command/view/RetransformView.java new file mode 100644 index 000000000..9da84a8e0 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/view/RetransformView.java @@ -0,0 +1,63 @@ +package com.taobao.arthas.core.command.view; + +import com.taobao.arthas.core.command.klass100.RetransformCommand.RetransformEntry; +import com.taobao.arthas.core.command.model.RetransformModel; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.text.Decoration; +import com.taobao.text.ui.RowElement; +import com.taobao.text.ui.TableElement; +import com.taobao.text.util.RenderUtil; + +/** + * + * @author hengyunabc 2021-01-06 + * + */ +public class RetransformView extends ResultView { + + @Override + public void draw(CommandProcess process, RetransformModel result) { + // 匹配到多个 classloader + if (result.getMatchedClassLoaders() != null) { + process.write("Matched classloaders: \n"); + ClassLoaderView.drawClassLoaders(process, result.getMatchedClassLoaders(), false); + process.write("\n"); + return; + } + + // retransform -d + if (result.getDeletedRetransformEntry() != null) { + process.write("Delete RetransformEntry by id success. id: " + result.getDeletedRetransformEntry().getId()); + process.write("\n"); + return; + } + + // retransform -l + if (result.getRetransformEntries() != null) { + // header + TableElement table = new TableElement(1, 1, 1, 1).rightCellPadding(1); + table.add(new RowElement().style(Decoration.bold.bold()).add("Id", "ClassName", "LoaderHash", + "LoaderClassName")); + + for (RetransformEntry entry : result.getRetransformEntries()) { + table.row("" + entry.getId(), "" + entry.getClassName(), "" + entry.getHashCode(), + "" + entry.getClassLoaderClass()); + } + + process.write(RenderUtil.render(table)); + return; + } + + // retransform /tmp/Demo.class + if (result.getRetransformClasses() != null) { + StringBuilder sb = new StringBuilder(); + for (String aClass : result.getRetransformClasses()) { + sb.append(aClass).append("\n"); + } + process.write("retransform success, size: " + result.getRetransformCount()).write(", classes:\n") + .write(sb.toString()); + } + + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/shell/cli/CompletionUtils.java b/core/src/main/java/com/taobao/arthas/core/shell/cli/CompletionUtils.java index d3e7c666d..b5014a19a 100644 --- a/core/src/main/java/com/taobao/arthas/core/shell/cli/CompletionUtils.java +++ b/core/src/main/java/com/taobao/arthas/core/shell/cli/CompletionUtils.java @@ -320,4 +320,39 @@ public class CompletionUtils { } } } + + /** + *
+     * 检查是否应该补全某个 option。
+     * 比如 option是: --classPattern , tokens可能是:
+     *  2个: '--classPattern' ' '
+     *  3个: '--classPattern' ' ' 'demo.'
+     * 
+ * + * @param option + * @return + */ + public static boolean shouldCompleteOption(Completion completion, String option) { + List tokens = completion.lineTokens(); + // 有两个 tocken, 然后 倒数第一个不是 - 开头的 + if (tokens.size() >= 2) { + CliToken cliToken_1 = tokens.get(tokens.size() - 1); + CliToken cliToken_2 = tokens.get(tokens.size() - 2); + String token_2 = cliToken_2.value(); + if (!cliToken_1.value().startsWith("-") && token_2.equals(option)) { + return CompletionUtils.completeClassName(completion); + } + } + // 有三个 token,然后 倒数第一个不是 - 开头的,倒数第2是空的,倒数第3是 --classPattern + if (tokens.size() >= 3) { + CliToken cliToken_1 = tokens.get(tokens.size() - 1); + CliToken cliToken_2 = tokens.get(tokens.size() - 2); + CliToken cliToken_3 = tokens.get(tokens.size() - 3); + if (!cliToken_1.value().startsWith("-") && cliToken_2.isBlank() + && cliToken_3.value().equals(option)) { + return CompletionUtils.completeClassName(completion); + } + } + return false; + } } diff --git a/site/src/site/sphinx/advanced-use.md b/site/src/site/sphinx/advanced-use.md index 393a2a246..27138c8bc 100644 --- a/site/src/site/sphinx/advanced-use.md +++ b/site/src/site/sphinx/advanced-use.md @@ -43,6 +43,7 @@ * [sm](sm.md)——查看已加载类的方法信息 * [jad](jad.md)——反编译指定已加载类的源码 * [mc](mc.md)——内存编译器,内存编译`.java`文件为`.class`文件 +* [retransform](retransform.md)——加载外部的`.class`文件,retransform到JVM里 * [redefine](redefine.md)——加载外部的`.class`文件,redefine到JVM里 * [dump](dump.md)——dump 已加载类的 byte code 到特定目录 * [classloader](classloader.md)——查看classloader的继承树,urls,类加载信息,使用classloader去getResource diff --git a/site/src/site/sphinx/commands.md b/site/src/site/sphinx/commands.md index 7ee246edd..7077aacfc 100644 --- a/site/src/site/sphinx/commands.md +++ b/site/src/site/sphinx/commands.md @@ -22,6 +22,7 @@ * [jad](jad.md) * [classloader](classloader.md) * [mc](mc.md) +* [retransform](retransform.md) * [redefine](redefine.md) * [monitor](monitor.md) diff --git a/site/src/site/sphinx/en/advanced-use.md b/site/src/site/sphinx/en/advanced-use.md index 1e2b72f62..6464c02c0 100644 --- a/site/src/site/sphinx/en/advanced-use.md +++ b/site/src/site/sphinx/en/advanced-use.md @@ -40,6 +40,7 @@ Advanced Usage * [sm](sm.md) - check methods info for the loaded classes * [jad](jad.md) - decompile the specified loaded classes * [mc](mc.md) - Memory compiler, compiles `.java` files into `.class` files in memory +* [retransform](retransform.md) - load external `*.class` files and retransform it into JVM * [redefine](redefine.md) - load external `*.class` files and re-define it into JVM * [dump](dump.md) - dump the loaded classes in byte code to the specified location * [classloader](classloader.md) - check the inheritance structure, urls, class loading info for the specified class; using classloader to get the url of the resource e.g. `java/lang/String.class` diff --git a/site/src/site/sphinx/en/commands.md b/site/src/site/sphinx/en/commands.md index d61dadc2c..13a35189c 100644 --- a/site/src/site/sphinx/en/commands.md +++ b/site/src/site/sphinx/en/commands.md @@ -22,6 +22,7 @@ All Commands * [jad](jad.md) * [classloader](classloader.md) * [mc](mc.md) +* [retransform](retransform.md) * [redefine](redefine.md) * [monitor](monitor.md) diff --git a/site/src/site/sphinx/en/jad.md b/site/src/site/sphinx/en/jad.md index 4c7347bec..839b61a1b 100644 --- a/site/src/site/sphinx/en/jad.md +++ b/site/src/site/sphinx/en/jad.md @@ -57,7 +57,7 @@ CharSequence { #### Print source only -By default, the decompile result will have the `ClassLoader` information. With the `--source-only` option, you can print only the source code. Conveniently used with the [mc](mc.md)/[redefine](redefine.md) commands. +By default, the decompile result will have the `ClassLoader` information. With the `--source-only` option, you can print only the source code. Conveniently used with the [mc](mc.md)/[retransform](retransform.md) commands. ``` $ jad --source-only demo.MathGame diff --git a/site/src/site/sphinx/en/mc.md b/site/src/site/sphinx/en/mc.md index 455b655f3..834e7163c 100644 --- a/site/src/site/sphinx/en/mc.md +++ b/site/src/site/sphinx/en/mc.md @@ -30,6 +30,6 @@ The output directory can be specified with the `-d` option: mc -d /tmp/output /tmp/ClassA.java /tmp/ClassB.java ``` -After compiling the `.class` file, you can use the [redefine](redefine.md) command to re-define the loaded classes in JVM. +After compiling the `.class` file, you can use the [retransform](retransform.md) command to re-define the loaded classes in JVM. -> Note that the mc command may fail. If the compilation fails, the `.class` file can be compiled locally and uploaded to the server. Refer to the [redefine](redefine.md) command description for details. \ No newline at end of file +> Note that the mc command may fail. If the compilation fails, the `.class` file can be compiled locally and uploaded to the server. Refer to the [retransform](retransform.md) command description for details. \ No newline at end of file diff --git a/site/src/site/sphinx/en/redefine.md b/site/src/site/sphinx/en/redefine.md index e1a36d48a..8573a50bf 100644 --- a/site/src/site/sphinx/en/redefine.md +++ b/site/src/site/sphinx/en/redefine.md @@ -1,6 +1,8 @@ redefine ======== +> Recommend to use the [retransform](retransform.md) command. + [`mc-redefine` online tutorial](https://arthas.aliyun.com/doc/arthas-tutorials?language=en&id=command-mc-redefine) > Load the external `*.class` files to re-define the loaded classes in JVM. @@ -9,6 +11,8 @@ Reference: [Instrumentation#redefineClasses](https://docs.oracle.com/javase/8/do ### Frequently asked questions +> Recommend to use the [retransform](retransform.md) command. + * The class of `redefine` cannot modify, add or delete the field and method of the class, including method parameters, method names and return values. * If `mc` fails, you can compile the class file in the local development environment, upload it to the target system, and use `redefine` to hot load the class. @@ -29,7 +33,6 @@ Reference: [Instrumentation#redefineClasses](https://docs.oracle.com/javase/8/do |---:|:---| |`[c:]`|hashcode of the class loader| |`[classLoaderClass:]`| The class name of the ClassLoader that executes the expression. | -|`[p:]`|absolute path of the external `*.class`, multiple paths are separated with 'space'| ### Usage diff --git a/site/src/site/sphinx/en/retransform.md b/site/src/site/sphinx/en/retransform.md new file mode 100644 index 000000000..80b289ac7 --- /dev/null +++ b/site/src/site/sphinx/en/retransform.md @@ -0,0 +1,137 @@ +retransform +======== + +> Load the external `*.class` files to retransform the loaded classes in JVM. + +Reference: [Instrumentation#retransformClasses](https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html#retransformClasses-java.lang.Class...-) + +### Usage + +```bash + retransform /tmp/Test.class + retransform -l + retransform -d 1 # delete retransform entry + retransform --deleteAll # delete all retransform entries + retransform --classPattern demo.* # triger retransform classes + retransform -c 327a647b /tmp/Test.class /tmp/Test\$Inner.class + retransform --classLoaderClass 'sun.misc.Launcher$AppClassLoader' /tmp/Test.class +``` + +### retransform the specified .class file + +```bash +$ retransform /tmp/MathGame.class +retransform success, size: 1, classes: +demo.MathGame +``` + +Load the specified .class file, then parse out the class name, and then retransform the corresponding class loaded in the jvm. Every time a `.class` file is loaded, a retransform entry is recorded. + +> If retransform is executed multiple times to load the same class file, there will be multiple retransform entries. + +### View retransform entry + +```bash +$ retransform -l +Id ClassName LoaderHash LoaderClassName +1 demo.MathGame null null +``` + +### Delete the specified retransform entry + +Need to specify id: + +```bash +retransform -d 1 +``` + +### Delete all retransform entries + +```bash +retransform --deleteAll +``` + +### Explicitly trigger retransform + +```bash +$ retransform --classPattern demo.MathGame +retransform success, size: 1, classes: +demo.MathGame +``` + +> Note: For the same class, when there are multiple retransform entries, if retransform is explicitly triggered, the entry added last will take effect (the one with the largest id). + +### Eliminate the influence of retransform + +If you want to eliminate the impact after performing retransform on a class, you need to: + +* Delete the retransform entry corresponding to this class +* Re-trigger retransform + +> If you do not clear all retransform entries and trigger retransform again, the retransformed classes will still take effect when arthas stop. + +### Use with the jad/mc command + +```bash +jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java + +mc /tmp/UserController.java -d /tmp + +retransform /tmp/com/example/demo/arthas/user/UserController.class +``` + +* Use `jad` command to decompile bytecode, and then you can use other editors, such as vim to modify the source code. +* `mc` command to compile the modified code +* Load new bytecode with `retransform` command + +### Tips for uploading .class files to the server + +The `mc` command may fail. You can modify the code locally, compile it, and upload it to the server. Some servers do not allow direct uploading files, you can use the `base64` command to bypass. + +1. Convert the `.class` file to base64 first, then save it as result.txt + + ```bash + Base64 < Test.class > result.txt + ``` + +2. Login the server, create and edit `result.txt`, copy the local content, paste and save + +3. Restore `result.txt` on the server to `.class` + + ``` + Base64 -d < result.txt > Test.class + ``` + +4. Use the md5 command to verify that the `.class` files are consistent. + + +### Restrictions of the retransform command + +* New field/method is not allowed +* The function that is running, no exit can not take effect, such as the new `System.out.println` added below, only the `run()` function will take effect. + + ```java + public class MathGame { + public static void main(String[] args) throws InterruptedException { + MathGame game = new MathGame(); + while (true) { + game.run(); + TimeUnit.SECONDS.sleep(1); + // This doesn't work because the code keeps running in while + System.out.println("in loop"); + } + } + + public void run() throws InterruptedException { + // This works because the run() function ends completely every time + System.out.println("call run()"); + try { + int number = random.nextInt(); + List primeFactors = primeFactors(number); + print(number, primeFactors); + + } catch (Exception e) { + System.out.println(String.format("illegalArgumentCount:%3d, ", illegalArgumentCount) + e.getMessage()); + } + } +``` \ No newline at end of file diff --git a/site/src/site/sphinx/jad.md b/site/src/site/sphinx/jad.md index 5fea91ed9..b4efd4a94 100644 --- a/site/src/site/sphinx/jad.md +++ b/site/src/site/sphinx/jad.md @@ -57,7 +57,7 @@ CharSequence { #### 反编译时只显示源代码 -默认情况下,反编译结果里会带有`ClassLoader`信息,通过`--source-only`选项,可以只打印源代码。方便和[mc](mc.md)/[redefine](redefine.md)命令结合使用。 +默认情况下,反编译结果里会带有`ClassLoader`信息,通过`--source-only`选项,可以只打印源代码。方便和[mc](mc.md)/[retransform](retransform.md)命令结合使用。 ``` $ jad --source-only demo.MathGame diff --git a/site/src/site/sphinx/mc.md b/site/src/site/sphinx/mc.md index a50c5b9a0..a479b56b3 100644 --- a/site/src/site/sphinx/mc.md +++ b/site/src/site/sphinx/mc.md @@ -30,6 +30,6 @@ Affect(row-cnt:1) cost in 346 ms mc -d /tmp/output /tmp/ClassA.java /tmp/ClassB.java ``` -编译生成`.class`文件之后,可以结合[redefine](redefine.md)命令实现热更新代码。 +编译生成`.class`文件之后,可以结合[retransform](retransform.md)命令实现热更新代码。 -> 注意,mc命令有可能失败。如果编译失败可以在本地编译好`.class`文件,再上传到服务器。具体参考[redefine](redefine.md)命令说明。 \ No newline at end of file +> 注意,mc命令有可能失败。如果编译失败可以在本地编译好`.class`文件,再上传到服务器。具体参考[retransform](retransform.md)命令说明。 \ No newline at end of file diff --git a/site/src/site/sphinx/redefine.md b/site/src/site/sphinx/redefine.md index 872d74cad..fcfc212d8 100644 --- a/site/src/site/sphinx/redefine.md +++ b/site/src/site/sphinx/redefine.md @@ -1,6 +1,8 @@ redefine === +> 推荐使用 [retransform](retransform.md) 命令 + [`mc-redefine`在线教程](https://arthas.aliyun.com/doc/arthas-tutorials?language=cn&id=command-mc-redefine) > 加载外部的`.class`文件,redefine jvm已加载的类。 @@ -9,6 +11,8 @@ redefine ### 常见问题 +> 推荐使用 [retransform](retransform.md) 命令 + * redefine的class不能修改、添加、删除类的field和method,包括方法参数、方法名称及返回值 * 如果mc失败,可以在本地开发环境编译好class文件,上传到目标系统,使用redefine热加载class @@ -28,9 +32,6 @@ redefine |---:|:---| |[c:]|ClassLoader的hashcode| |`[classLoaderClass:]`|指定执行表达式的 ClassLoader 的 class name| -|[p:]|外部的`.class`文件的完整路径,支持多个| - - ### 使用参考 diff --git a/site/src/site/sphinx/retransform.md b/site/src/site/sphinx/retransform.md new file mode 100644 index 000000000..6bc3d8b58 --- /dev/null +++ b/site/src/site/sphinx/retransform.md @@ -0,0 +1,138 @@ +retransform +=== + +> 加载外部的`.class`文件,retransform jvm已加载的类。 + +参考:[Instrumentation#retransformClasses](https://docs.oracle.com/javase/8/docs/api/java/lang/instrument/Instrumentation.html#retransformClasses-java.lang.Class...-) + + +### 使用参考 + +```bash + retransform /tmp/Test.class + retransform -l + retransform -d 1 # delete retransform entry + retransform --deleteAll # delete all retransform entries + retransform --classPattern demo.* # triger retransform classes + retransform -c 327a647b /tmp/Test.class /tmp/Test\$Inner.class + retransform --classLoaderClass 'sun.misc.Launcher$AppClassLoader' /tmp/Test.class +``` + +### retransform 指定的 .class 文件 + +```bash +$ retransform /tmp/MathGame.class +retransform success, size: 1, classes: +demo.MathGame +``` + +加载指定的 .class 文件,然后解析出class name,再retransform jvm中已加载的对应的类。每加载一个 `.class` 文件,则会记录一个 retransform entry. + +> 如果多次执行 retransform 加载同一个 class 文件,则会有多条 retransform entry. + +### 查看 retransform entry + +```bash +$ retransform -l +Id ClassName LoaderHash LoaderClassName +1 demo.MathGame null null +``` + +### 删除指定 retransform entry + +需要指定 id: + +```bash +retransform -d 1 +``` + +### 删除所有 retransform entry + +```bash +retransform --deleteAll +``` + +### 显式触发 retransform + +```bash +$ retransform --classPattern demo.MathGame +retransform success, size: 1, classes: +demo.MathGame +``` + +> 注意:对于同一个类,当存在多个 retransform entry时,如果显式触发 retransform ,则最后添加的entry生效(id最大的)。 + +### 消除 retransform 的影响 + +如果对某个类执行 retransform 之后,想消除影响,则需要: + +* 删除这个类对应的 retransform entry +* 重新触发 retransform + +> 如果不清除掉所有的 retransform entry,并重新触发 retransform ,则arthas stop时,retransform过的类仍然生效。 + + +### 结合 jad/mc 命令使用 + +```bash +jad --source-only com.example.demo.arthas.user.UserController > /tmp/UserController.java + +mc /tmp/UserController.java -d /tmp + +retransform /tmp/com/example/demo/arthas/user/UserController.class +``` + +* jad命令反编译,然后可以用其它编译器,比如vim来修改源码 +* mc命令来内存编译修改过的代码 +* 用retransform命令加载新的字节码 + +### 上传 .class 文件到服务器的技巧 + +使用`mc`命令来编译`jad`的反编译的代码有可能失败。可以在本地修改代码,编译好后再上传到服务器上。有的服务器不允许直接上传文件,可以使用`base64`命令来绕过。 + +1. 在本地先转换`.class`文件为base64,再保存为result.txt + + ```bash + base64 < Test.class > result.txt + ``` + +2. 到服务器上,新建并编辑`result.txt`,复制本地的内容,粘贴再保存 + +3. 把服务器上的 `result.txt`还原为`.class` + + ``` + base64 -d < result.txt > Test.class + ``` + +4. 用md5命令计算哈希值,校验是否一致 + +### retransform的限制 + +* 不允许新增加field/method +* 正在跑的函数,没有退出不能生效,比如下面新增加的`System.out.println`,只有`run()`函数里的会生效 + + ```java + public class MathGame { + public static void main(String[] args) throws InterruptedException { + MathGame game = new MathGame(); + while (true) { + game.run(); + TimeUnit.SECONDS.sleep(1); + // 这个不生效,因为代码一直跑在 while里 + System.out.println("in loop"); + } + } + + public void run() throws InterruptedException { + // 这个生效,因为run()函数每次都可以完整结束 + System.out.println("call run()"); + try { + int number = random.nextInt(); + List primeFactors = primeFactors(number); + print(number, primeFactors); + + } catch (Exception e) { + System.out.println(String.format("illegalArgumentCount:%3d, ", illegalArgumentCount) + e.getMessage()); + } + } +``` \ No newline at end of file