参考
http://tech.dianwoda.com/2018/12/20/arthasyuan-ma-fen-xi/
https://zhuanlan.zhihu.com/p/53984185
项目结构 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 . ├── CONTRIBUTING.md //贡献者相关信息 ├── Dockerfile //Dockerfile ├── Dockerfile-No-Jdk //Dockerfile ├── LICENSE //LICENSE ├── NOTICE //NOTICE ├── README.md //README ├── README_CN.md //README ├── README_EN.md //README ├── TODO.md //将来要做的 ├── agent //自定义Agent ├── as-package.sh ├── batch.as ├── bin ├── boot ├── client //Telnet客户端实现 ├── common ├── core //Arthas的核心实现 ├── demo ├── memorycompiler ├── mvnw ├── mvnw.cmd ├── packaging ├── pom.xml ├── site //一些站点信息,包含图片等 ├── spy ├── static └── testcase
从Main方法入手 大致扫了一眼,首先看到了core这个包,从名字中可以推测出其应该是核心包,所以先看看它的结构,然后进行有目的的分析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 . ├── Arthas.java //启动代码 ├── GlobalOptions.java //全局配置 ├── Option.java //配置 ├── advisor │ ├── Advice.java │ ├── AdviceListener.java │ ├── AdviceListenerAdapter.java │ ├── AdviceWeaver.java │ ├── ArthasMethod.java │ ├── AsmCodeLock.java │ ├── CodeLock.java │ ├── Enhancer.java │ ├── InvokeTraceable.java │ ├── ReflectAdviceListenerAdapter.java │ └── TracingAsmCodeLock.java ├── command //各种命令 │ ├── BuiltinCommandPack.java │ ├── Constants.java │ ├── ScriptSupportCommand.java │ ├── basic1000 │ │ ├── CatCommand.java │ │ ├── ClsCommand.java │ │ ├── HelpCommand.java │ │ ├── HistoryCommand.java │ │ ├── KeymapCommand.java │ │ ├── PwdCommand.java │ │ ├── ResetCommand.java │ │ ├── SessionCommand.java │ │ ├── ShutdownCommand.java │ │ ├── StopCommand.java │ │ ├── SystemEnvCommand.java │ │ ├── SystemPropertyCommand.java │ │ └── VersionCommand.java │ ├── express //表达式分析 │ │ ├── ClassLoaderClassResolver.java │ │ ├── CustomClassResolver.java │ │ ├── Express.java │ │ ├── ExpressException.java │ │ ├── ExpressFactory.java │ │ └── OgnlExpress.java │ ├── hidden │ │ ├── JulyCommand.java │ │ ├── OptionsCommand.java │ │ └── ThanksCommand.java │ ├── klass100 │ │ ├── ClassDumpTransformer.java │ │ ├── ClassLoaderCommand.java │ │ ├── DumpClassCommand.java │ │ ├── GetStaticCommand.java │ │ ├── JadCommand.java │ │ ├── MemoryCompilerCommand.java │ │ ├── OgnlCommand.java │ │ ├── RedefineCommand.java │ │ ├── SearchClassCommand.java │ │ └── SearchMethodCommand.java │ └── monitor200 │ ├── AbstractTraceAdviceListener.java │ ├── DashboardCommand.java │ ├── DashboardInterruptHandler.java │ ├── EnhancerCommand.java │ ├── GroovyAdviceListener.java │ ├── GroovyScriptCommand.java │ ├── JvmCommand.java │ ├── MBeanCommand.java │ ├── MonitorAdviceListener.java │ ├── MonitorCommand.java │ ├── PathTraceAdviceListener.java │ ├── StackAdviceListener.java │ ├── StackCommand.java │ ├── ThreadCommand.java │ ├── TimeFragment.java │ ├── TimeTunnelAdviceListener.java │ ├── TimeTunnelCommand.java │ ├── TimeTunnelTable.java │ ├── TraceAdviceListener.java │ ├── TraceCommand.java │ ├── TraceEntity.java │ ├── WatchAdviceListener.java │ └── WatchCommand.java ├── config //配置信息 │ ├── Configure.java │ └── FeatureCodec.java ├── server //服务器 │ └── ArthasBootstrap.java ├── shell //shell │ ├── Shell.java │ ├── ShellServer.java │ ├── ShellServerOptions.java │ ├── cli │ │ ├── CliToken.java │ │ ├── CliTokens.java │ │ ├── Completion.java │ │ ├── CompletionUtils.java │ │ └── impl │ │ └── CliTokenImpl.java │ ├── command │ │ ├── AnnotatedCommand.java │ │ ├── Command.java │ │ ├── CommandBuilder.java │ │ ├── CommandProcess.java │ │ ├── CommandRegistry.java │ │ ├── CommandResolver.java │ │ ├── impl │ │ │ ├── AnnotatedCommandImpl.java │ │ │ └── CommandBuilderImpl.java │ │ └── internal │ │ ├── CloseFunction.java │ │ ├── GrepHandler.java │ │ ├── PlainTextHandler.java │ │ ├── RedirectHandler.java │ │ ├── StatisticsFunction.java │ │ ├── StdoutHandler.java │ │ ├── TermHandler.java │ │ └── WordCountHandler.java │ ├── future │ │ └── Future.java │ ├── handlers │ │ ├── BindHandler.java │ │ ├── Handler.java │ │ ├── NoOpHandler.java │ │ ├── command │ │ │ └── CommandInterruptHandler.java │ │ ├── server │ │ │ ├── SessionClosedHandler.java │ │ │ ├── SessionsClosedHandler.java │ │ │ ├── TermServerListenHandler.java │ │ │ └── TermServerTermHandler.java │ │ ├── shell │ │ │ ├── CloseHandler.java │ │ │ ├── CommandManagerCompletionHandler.java │ │ │ ├── FutureHandler.java │ │ │ ├── InterruptHandler.java │ │ │ ├── QExitHandler.java │ │ │ ├── ShellForegroundUpdateHandler.java │ │ │ ├── ShellLineHandler.java │ │ │ └── SuspendHandler.java │ │ └── term │ │ ├── CloseHandlerWrapper.java │ │ ├── DefaultTermStdinHandler.java │ │ ├── EventHandler.java │ │ ├── RequestHandler.java │ │ ├── SizeHandlerWrapper.java │ │ └── StdinHandlerWrapper.java │ ├── impl │ │ ├── BuiltinCommandResolver.java │ │ ├── ShellImpl.java │ │ └── ShellServerImpl.java │ ├── session │ │ ├── Session.java │ │ └── impl │ │ └── SessionImpl.java │ ├── system │ │ ├── ExecStatus.java │ │ ├── Job.java │ │ ├── JobController.java │ │ ├── Process.java │ │ └── impl │ │ ├── CommandCompletion.java │ │ ├── GlobalJobControllerImpl.java │ │ ├── InternalCommandManager.java │ │ ├── JobControllerImpl.java │ │ ├── JobImpl.java │ │ └── ProcessImpl.java │ └── term │ ├── SignalHandler.java │ ├── Term.java │ ├── TermServer.java │ ├── Tty.java │ └── impl │ ├── CompletionAdaptor.java │ ├── CompletionHandler.java │ ├── Helper.java │ ├── HttpTermServer.java │ ├── TelnetTermServer.java │ └── TermImpl.java ├── util │ ├── ApplicationUtils.java │ ├── ArrayUtils.java │ ├── ArthasBanner.java │ ├── ArthasCheckUtils.java │ ├── ClassLoaderUtils.java │ ├── ClassUtils.java │ ├── Constants.java │ ├── DateUtils.java │ ├── Decompiler.java │ ├── FileUtils.java │ ├── IOUtils.java │ ├── IPUtils.java │ ├── LogUtil.java │ ├── NetUtils.java │ ├── ObjectUtils.java │ ├── SearchUtils.java │ ├── StringUtils.java │ ├── ThreadLocalRandom.java │ ├── ThreadLocalWatch.java │ ├── ThreadUtil.java │ ├── TokenUtils.java │ ├── TypeRenderUtils.java │ ├── UserStatUtil.java │ ├── affect │ │ ├── Affect.java │ │ ├── EnhancerAffect.java │ │ └── RowAffect.java │ ├── collection │ │ ├── GaStack.java │ │ ├── ThreadUnsafeFixGaStack.java │ │ └── ThreadUnsafeGaStack.java │ ├── matcher │ │ ├── EqualsMatcher.java │ │ ├── FalseMatcher.java │ │ ├── GroupMatcher.java │ │ ├── Matcher.java │ │ ├── RegexMatcher.java │ │ ├── TrueMatcher.java │ │ └── WildcardMatcher.java │ ├── metrics │ │ ├── RateCounter.java │ │ └── SumRateCounter.java │ ├── reflect │ │ ├── ArthasReflectUtils.java │ │ └── FieldUtils.java │ └── usage │ └── StyledUsageFormatter.java └── view ├── Ansi.java ├── ClassInfoView.java ├── KVView.java ├── LadderView.java ├── MethodInfoView.java ├── ObjectView.java ├── TableView.java ├── TreeView.java └── View.java
Arthas类 这个类有main方法,所以起手从它开始分析,首先是看看这个类的整体构造:
可以看到一共有5个方法(其中一个是main方法),还有两个静态字符串,而这两个静态字符串如下所示:
1 2 private static final String DEFAULT_TELNET_PORT = "3658" ;private static final String DEFAULT_HTTP_PORT = "8563" ;
很显然,这两个静态的字符串是用来指定默认的Telnet端口号和http端口号的。
唯一的一个构造方法 1 2 3 private Arthas (String[] args) throws Exception { attachAgent(parse(args)); }
可以看到通过parse这个函数对命令行参数进行了处理,然后调用了attachAgent方法,然后就没有然后了。说明奥秘肯定是在attachAgent这个方法上面。但是在深入分析下这个方法之前,先把其它的几个方法简单过一遍。
parse() 这个方法内容比较多,所以先暂时从名字中推测是对用户输入的命令行参数进行解析,然后返回一个Configure对象,之后attachAgent()就可以用这个返回的对象作为参数了。
encodeArg() 1 2 3 4 5 6 7 private static String encodeArg (String arg) { try { return URLEncoder.encode(arg, "utf-8" ); } catch (UnsupportedEncodingException e) { return arg; } }
非常简单的一个函数,就是用utf-8对传入的字符串进行编码并进行返回。
main() 1 2 3 public static void main (String[] args) { new Arthas(args); }
这里稍微对main方法进行了删减,把其中的异常处理删了,无伤大雅。可以发现只是调用了一下Arthas的构造方法,而在构造方法中,又只有attachAgent()这一个方法,说明这个类其实最最主要的其实就是这个叫attachAgent()的方法。
attachAgent() 从我真实使用程序来看,arthas首先会列出一个正在运行java程序的PID列表的,然后让你选择你要attach到哪个进程,那么第一个问题就是:它是如何获取到进程的信息的呢?应该是通过jps这种命令,配合信息摘取来获取。这个目前来说不是很重要,不理解可以先跳过。
列出所有的java相关的列表之后,就是如何attach到其上的问题了,相关的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 private void attachAgent (Configure configure) throws Exception { VirtualMachineDescriptor virtualMachineDescriptor = null ; for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) { String pid = descriptor.id(); if (pid.equals(Integer.toString(configure.getJavaPid()))) { virtualMachineDescriptor = descriptor; } } VirtualMachine virtualMachine = null ; try { if (null == virtualMachineDescriptor) { virtualMachine = VirtualMachine.attach("" + configure.getJavaPid()); } else { virtualMachine = VirtualMachine.attach(virtualMachineDescriptor); } Properties targetSystemProperties = virtualMachine.getSystemProperties(); String targetJavaVersion = JavaVersionUtils.javaVersionStr(targetSystemProperties); String currentJavaVersion = JavaVersionUtils.javaVersionStr(); if (targetJavaVersion != null && currentJavaVersion != null ) { if (!targetJavaVersion.equals(currentJavaVersion)) { AnsiLog.warn("Current VM java version: {} do not match target VM java version: {}, attach may fail." , currentJavaVersion, targetJavaVersion); AnsiLog.warn("Target VM JAVA_HOME is {}, arthas-boot JAVA_HOME is {}, try to set the same JAVA_HOME." , targetSystemProperties.getProperty("java.home" ), System.getProperty("java.home" )); } } String arthasAgentPath = configure.getArthasAgent(); configure.setArthasAgent(encodeArg(arthasAgentPath)); configure.setArthasCore(encodeArg(configure.getArthasCore())); virtualMachine.loadAgent(arthasAgentPath, configure.getArthasCore() + ";" + configure.toString()); } finally { if (null != virtualMachine) { virtualMachine.detach(); } } }
整体思路是这样的:通过sun公司提供的包中的虚拟机的描述符这个类,然后attach到指定的java类上。完成这个方法之后就成功attach到指定pid的jvm上面了。
程序先获取所有正在运行java的VirtualMachine(包括自己),然后用已知的pid找到对应的VirtualMachine,最后是用VirtualMachine.loadAgent()方法加载arthas.jar。
agent加载 这部分先跳过,重点看arthas是如何执行用户输入的命令的。
处理用户的命令 1 2 3 4 5 6 ShellImpl session = createShell(term); session.setWelcome(welcomeMessage); session.closedFuture.setHandler(new SessionClosedHandler(this , session)); session.init(); sessions.put(session.id, session); session.readline();
直接通过追踪很容易发现这段代码,然后可以发现是这里完成了欢迎信息的载入,然后可以从这里开始追踪每个功能的实现,最后一句的readLine()明显就是在等待用户的输入。也就是说session会处理用户的输入,然后匹配不同的命令,并且执行。
所以接下来需要到session(即ShellImpl类)里面去找readline这个函数。由于其实是服务器端(即agent,对应的类是ShellServer)在接受用户的命令,执行之后把结果返回给返回给用户客户端,然后用户客户端显示出来就行了。
Shell 接口的结构:
前两个createJob方法用来创建Job,而jobController会返回一个Job的控制器,同时还有一个方法来创建session和关闭这个shell。
ShellImpl 实现了shell接口的一个类。
一个在arthas中真实使用的shell,其具体包含以下成员:
shell的id(随机生成的一个uuid)
一个closedFuture(推测是用来返回结果使用的)
一个commandManager,用来管理命令
一个session用来实现会话。
一个terminal
一个目前正在执行的job。
其中最最最最最重要 的就是下面的这个函数了
1 2 3 4 public void readline () { term.readline(Constants.DEFAULT_PROMPT, new ShellLineHandler(this ), new CommandManagerCompletionHandler(commandManager)); }
可以看到是调用了terminal的readLine方法,并且把传入了一些参数,所以其实需要看的就是哪个类实现了term接口,去到那里分析就可以了。
OK,到目前为止,已经知道了其实就是ShellImpl这个类里的这个叫readline函数在真正处理用户的输入,那么如果我们需要追踪不同的命令是如何执行的,这里肯定是入口。
Handler 应该是整个项目中最简单的部分了,一个接口,里面只有一个函数,用来处理事件。
Tty 这个接口的注释是Provide interactions with the Shell TTY,用来和Shell进行通信。所以就是一个terminal。
具体有以下抽象方法:
String type(); 返回类型
int width(); 返回tty的宽度
int height(); 返回tty的高度
Tty stdinHandler(Handler<String> handler); 返回一个处理命令的tty
Tty write(String data); 向标准输出中输出数据
Tty resizehandler(Handler<Void> handler); 重设tty
CommandProcess 这个仍然是接口。
配置类。主要记录了ip地址,Telnet和http的端口号,java程序的pid,还有核心和agent的代码。一堆可以set和get方法。有一个编码解码器,暂时放一下,然后最后是序列化和反序列化的函数。
ShellServerOptions 看类的名字就知道是ShellServer的选项类。
定义了一些超时的信息、欢迎信息等。
ShellServer具体实现 首先文本的注释中有这么一段话:ShellServer是一系列的Term servers的集合,且由ShellServer来管理这些Term servers。每当Terminal Server收到一个连接的时候,JobController会被创建。
Job Job是执行在JobController中的,它的生命周期包括run,resume,suspend,interrupt,所以这个接口定义了这些方法。
值得注意的Job是属于session的。
Term Terminal的一个抽象接口。定义了一些term应该有的功能,具体实现在TermImpl这个类中。
TermImpl 对term的一个具体实现,但是我发现….里面的东西好难懂。。。 先跳过了。
ShellLineHandler 这个是用来处理shell命令的。一共只有两个私有参数,一个是term另一个是shell。
CLiToken 一个接口,代表了命令行界面中的已解析令牌。
value 返回这个令牌的值。
raw() 返回未经处理的令牌的值,可能包含未转义的字符。
isText() 当令牌是文本的时候返回true
isBlank() 当令牌的值是空的时候返回true
CliTokenImpl 这个类只有三个私有成员,
1 2 3 final boolean text; final String raw; final String value;
但是实现的方法比较多:
构造方法有两个,太简单了就跳过,只需要知道必须要有一个text和一个value才可以构造Cli令牌。
isText()和isBlank()这两个方法就是靠text那个变量完成的。
raw()和value()这两个就是靠成员变量来决定的。
重写了hashcode,比较的是value的hashcode值。
tokenize()这个函数比较重要,它通过传入一个string,构造一个包含一系列令牌的list并返回。它会取出字符串中的每一个字符,如果字符是空格或者制表符,就用blankToken,否则就用textToken来处理。
textToken(),这个方法用到了LineStatus这个类,这个类是阿里自己开发用来开发终端应用的,详情见这里 。
所以猜测就是给定一个字符串,然后根据其中的空格和制表符来生成一个List<CliToken>,比如我有一个命令,是cat file abc,那就会生成一个List,长度是3。
Job 所有的用户命令都会被封装成Job,具体实现见JobImpl类。 然后经过一系列的跟踪可以发现最后在ProcessImpl中有执行命令的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public synchronized void run (boolean fg) { if (processStatus != ExecStatus.READY) { throw new IllegalStateException("Cannot run proces in " + processStatus + " state" ); } processStatus = ExecStatus.RUNNING; processForeground = fg; foreground = fg; startTime = new Date(); final Tty tty = this .tty; if (tty == null ) { throw new IllegalStateException("Cannot execute process without a TTY set" ); } final List<String> args2 = new LinkedList<String>(); for (CliToken arg : args) { if (arg.isText()) { args2.add(arg.value()); } } CommandLine cl = null ; try { if (commandContext.cli() != null ) { if (commandContext.cli().parse(args2, false ).isAskingForHelp()) { UsageMessageFormatter formatter = new StyledUsageFormatter(Color.green); formatter.setWidth(tty.width()); StringBuilder usage = new StringBuilder(); commandContext.cli().usage(usage, formatter); usage.append('\n' ); tty.write(usage.toString()); terminate(); return ; } cl = commandContext.cli().parse(args2); } } catch (CLIException e) { tty.write(e.getMessage() + "\n" ); terminate(); return ; } process = new CommandProcessImpl(args2, tty, cl); if (cacheLocation() != null ) { process.echoTips("job id : " + this .jobId + "\n" ); process.echoTips("cache location : " + cacheLocation() + "\n" ); } Runnable task = new CommandProcessTask(process); ArthasBootstrap.getInstance().execute(task); }
通用命令实现 首先arthas有一个接口叫CommandResolver,很明显是用来解析命令的,可以通过跟踪发现是一个叫BuiltinCommandPack的类实现了它。而在这个类里面我也找到了arthas支持的所有的命令(部分):
watch实现 watch能够观察到方法的入参、返回值和异常信息,非常有用,所以来看看这个是怎么实现的。(上面中的通用命令的WatchCommand.class就是它的实现)。
首先是一堆的继承链 AnnotatedCommand - > EnhancerCommand -> WatchCommand
AnnotatedCommand
第一个返回命令的名字,第二个返回命令行接口(默认是Null),第三个是抽象方法用来处理命令,最后一个是命令处理完了之后应该做什么。
EnhancerCommand 这个类友好很多,大部分中文注释。但是还是太多了…
这里最主要的是enhance这个方法,里面最主要的是这一段:
1 EnhancerAffect effect = Enhancer.enhance(inst, lock, listener instanceof InvokeTraceable,skipJDKTrace, getClassNameMatcher(), getMethodNameMatcher());
跟踪到一个叫enhancer的类,接下来就是字节码的增强技术了,没了解过,只能先这样了。
从JDK 1.5起,有一套ClassFileTransformer的机制,Java Agent通过Instrumentation注册ClassFileTransformer,那么在类加载或者retransform时就可以回调修改字节码。