Arthas 之源码简要分析

启动模块(boot)

启动 Arthas

wget https://alibaba.github.io/arthas/arthas-boot.jar
java -jar arthas-boot.jar

模块代码

GitHub Code by arthas-boot

模块入口

在 arthas-boot 模块的 pom.xml 文件中,找到启动类:

<manifest>
	<mainClass>com.taobao.arthas.boot.Bootstrap</mainClass>
</manifest>

com.taobao.arthas.boot.Bootstrap.main() 方法:GitHub Code by Bootstrap.java

源码分析

参数解析

使用了阿里开源的组件 cli,对参数进行了解析。

CLI cli = CLIConfigurator.define(Bootstrap.class);
CommandLine commandLine = cli.parse(Arrays.asList(args));

try {
	CLIConfigurator.inject(commandLine, bootstrap);
} catch (Throwable e) {
	e.printStackTrace();
	System.out.println(usage(cli));
	System.exit(1);
}
参数处理

对传入的参数进行处理。

如调整日志级别,设置 RepoMirror 地址,Java 版本,telnet/http 的端口检查。

检查 pid

如果在传入参数中没有 pid,则会调用本地 jps 命令,列出 java 进程(当然会排除本身)

long pid = bootstrap.getPid();
// select pid
if (pid < 0) {
	try {
		pid = ProcessUtils.select(bootstrap.isVerbose(), telnetPortPid, bootstrap.getSelect());
	} catch (InputMismatchException e) {
		System.out.println("Please input an integer to select pid.");
		System.exit(1);
	}
	if (pid < 0) {
		System.out.println("Please select an available pid.");
		System.exit(1);
	}
}

GitHub Code by ProcessUtils.java

private static Map<Long, String> listProcessByJps(boolean v) {
	Map<Long, String> result = new LinkedHashMap<Long, String>();

	String jps = "jps";
	File jpsFile = findJps();
	if (jpsFile != null) {
		jps = jpsFile.getAbsolutePath();
	}

	AnsiLog.debug("Try use jps to lis java process, jps: " + jps);

	String[] command = null;
	if (v) {
		command = new String[] { jps, "-v", "-l" };
	} else {
		command = new String[] { jps, "-l" };
	}

	List<String> lines = ExecutingCommand.runNative(command);

	AnsiLog.debug("jps result: " + lines);

	long currentPid = Long.parseLong(PidUtils.currentPid());
	for (String line : lines) {
		String[] strings = line.trim().split("\\s+");
		if (strings.length < 1) {
			continue;
		}
		try {
			long pid = Long.parseLong(strings[0]);
			if (pid == currentPid) {
				continue;
			}
			if (strings.length >= 2 && isJpsProcess(strings[1])) { // skip jps
				continue;
			}

			result.put(pid, line);
		} catch (Throwable e) {
			// https://github.com/alibaba/arthas/issues/970
			// ignore
		}
	}

	return result;
}
启动服务端

进入主逻辑,会在用户目录下建立 .arthas 目录,同时下载 arthas-corearthas-agent 等 lib 文件,然后启动客户端和服务端。

if (telnetPortPid > 0 && pid == telnetPortPid) {
	AnsiLog.info("The target process already listen port {}, skip attach.", bootstrap.getTelnetPort());
} else {
	//double check telnet port and pid before attach
	telnetPortPid = findProcessByTelnetClient(arthasHomeDir.getAbsolutePath(), bootstrap.getTelnetPort());
	checkTelnetPortPid(bootstrap, telnetPortPid, pid);

	// start arthas-core.jar
	List<String> attachArgs = new ArrayList<String>();
	attachArgs.add("-jar");
	attachArgs.add(new File(arthasHomeDir, "arthas-core.jar").getAbsolutePath());
	attachArgs.add("-pid");
	attachArgs.add("" + pid);
	attachArgs.add("-target-ip");
	attachArgs.add(bootstrap.getTargetIp());
	attachArgs.add("-telnet-port");
	attachArgs.add("" + bootstrap.getTelnetPort());
	attachArgs.add("-http-port");
	attachArgs.add("" + bootstrap.getHttpPort());
	attachArgs.add("-core");
	attachArgs.add(new File(arthasHomeDir, "arthas-core.jar").getAbsolutePath());
	attachArgs.add("-agent");
	attachArgs.add(new File(arthasHomeDir, "arthas-agent.jar").getAbsolutePath());
	if (bootstrap.getSessionTimeout() != null) {
		attachArgs.add("-session-timeout");
		attachArgs.add("" + bootstrap.getSessionTimeout());
	}

	if (bootstrap.getTunnelServer() != null) {
		attachArgs.add("-tunnel-server");
		attachArgs.add(bootstrap.getTunnelServer());
	}
	if (bootstrap.getAgentId() != null) {
		attachArgs.add("-agent-id");
		attachArgs.add(bootstrap.getAgentId());
	}
	if (bootstrap.getStatUrl() != null) {
		attachArgs.add("-stat-url");
		attachArgs.add(bootstrap.getStatUrl());
	}

	AnsiLog.info("Try to attach process " + pid);
	AnsiLog.debug("Start arthas-core.jar args: " + attachArgs);
	// 启动服务端
	ProcessUtils.startArthasCore(pid, attachArgs);

	AnsiLog.info("Attach process {} success.", pid);
}
启动客户端

最后通过反射的方式来启动字符客户端,等待用户输入指令。

URLClassLoader classLoader = new URLClassLoader(
				new URL[] { new File(arthasHomeDir, "arthas-client.jar").toURI().toURL() });
Class<?> telnetConsoleClas = classLoader.loadClass("com.taobao.arthas.client.TelnetConsole");
Method mainMethod = telnetConsoleClas.getMethod("main", String[].class);

服务端模块(core)

模块代码

GitHub Code by arthas-core

模块入口

在 arthas-core 模块的 pom.xml 中,找到启动类:

<transformers>
	<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
		<mainClass>com.taobao.arthas.core.Arthas</mainClass>
	</transformer>
</transformers>

com.taobao.arthas.core.Arthas.main() 方法:GitHub Code by Arthas.java

源码分析

连接进程

使用 VirutalMachine.attach(pid) 来连接进程,同时使用 virtualMachine.loadAgent 加载自定义的 agent.

private void attachAgent(Configure configure) throws Exception {
	// 省略部分代码
	// 连接进程
			virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());
    
	// 省略部分代码
	// 动态加载Agent
		virtualMachine.loadAgent(arthasAgentPath,
				configure.getArthasCore() + ";" + configure.toString());
}

JavaAgent 代理(agent)

模块代码

GitHub Code by arthas-agent

模块入口

在 arthas-core 模块的 pom.xml 中,找到启动类:

<manifestEntries>
	<Premain-Class>com.taobao.arthas.agent334.AgentBootstrap</Premain-Class>
	<Agent-Class>com.taobao.arthas.agent334.AgentBootstrap</Agent-Class>
</manifestEntries>

com.taobao.arthas.agent334.AgentBootstrap.main() 方法:GitHub Code by AgentBootstrap.java

源码分析

main() 方法中对于 arthas-spy(简单理解为勾子类,类似于 spring aop 的前置方法,后置方法) 进行了加载。

final ClassLoader agentLoader = getClassLoader(inst, arthasCoreJarFile);

将 spyJar 添加到了 BootstrapClassLoader (启动类加载器),优先加载启动类加载器,spy 可以在各个 ClassLoader 中使用。

private static ClassLoader getClassLoader(Instrumentation inst, File arthasCoreJarFile) throws Throwable {
	// 构造自定义的类加载器,尽量减少Arthas对现有工程的侵蚀
	return loadOrDefineClassLoader(arthasCoreJarFile);
}

异步调用 bind () 方法,启动服务端,监听端口,和客户端进行通讯。

Thread bindingThread = new Thread() {
	@Override
	public void run() {
		try {
			bind(inst, agentLoader, agentArgs);
		} catch (Throwable throwable) {
			throwable.printStackTrace(ps);
		}
	}
};