设计的分层
其实已经看过这篇文章(你还不知道这款Idea插件吗?)的伙伴已经大致可以知道代码的设计长啥样子了。
简单来说,分两层:Idea插件模块和代理模块。
Idea插件模块
这个模块是作为一个成型的Idea插件必备的一个部分。它可以捕获程序在Idea的动作,比如程序在Idea的启动命令、右键菜单的选择事件、工具栏的菜单设置等等。对这些动作做事件的监听,以此来获取并分析程序的Psi结构。
这个模块主要通过继承抽象类JavaProgramPatcher的patchJavaParameters来实现动态添加-javaagent:命令。
@Override
public void patchJavaParameters(Executor executor, RunProfile runProfile, JavaParameters javaParameters) {
RunConfiguration runConfiguration = (RunConfiguration)runProfile;
ParametersList vmParametersList = javaParameters.getVMParametersList();
// -Dmonitor.package=你需要监控的包名
String packageParam = vmParametersList.getParameters().stream().filter(
t -> t.indexOf("-Dmonitor.package=") != -1).findFirst().orElse(null);
if (null == packageParam) {
MessageUtil.warn("vm options is missing, you can add -Dmonitor.package to enable Link Monitoring! ");
return;
}
String packageName = packageParam.substring(packageParam.indexOf("=") + 1);
if (StringUtils.isBlank(packageName)) {
MessageUtil.warn("-Dmonitor.package do not have a correct value ! ");
return;
}
String agentCoreJarPath = PluginUtil.getAgentCoreJarPath();
if (StringUtils.isBlank(agentCoreJarPath)) {
MessageUtil.warn("cannot load agent successfully !");
return;
}
vmParametersList.addParametersString(
"-javaagent:" + agentCoreJarPath + "=" + packageName);
vmParametersList.addNotEmptyProperty("service-invocation-monitor.projectId",
runConfiguration.getProject().getLocationHash());
}
- 先对
-Dmonitor.package命令做校验,提取packageName - 添加
VM options,这一步就是添加接入JavaAgent(也就是下面的代理模块)的命令
代理模块
这里的代理模块,其实就是
JavaAgent,也叫Java探针。使用了字节码增强框架bytebuddy,对调用的链路做了耗时监控。bytebuddy,与ASM、javassist并称为三大字节码增强框架。
- 首先进入
premain入口,注册一个用于监控的切面。
public static void premain(String agentArgs, Instrumentation inst) {
AgentBuilder agentBuilder = new AgentBuilder.Default();
String packageName = agentArgs;
AgentBuilder.Transformer transformer = (builder, typeDescription, classLoader, javaModule) -> {
builder = builder.visit(
Advice.to(TraceAdvice.class)
.on(ElementMatchers.isMethod()
.and(ElementMatchers.any()).and(ElementMatchers.not(ElementMatchers.nameStartsWith("main"))
)));
return builder;
};
agentBuilder.type(ElementMatchers.nameStartsWith(packageName)).transform(transformer).installOn(inst);
}
Advice.to(TraceAdvice.class),使用AgentBuilder.Transformer注册一个切面类TraceAdvice,这个切面会拦截到非main开头的所有方法ElementMatchers.nameStartsWith(packageName),匹配到包名开头的方法transform(transformer),绑定转换类及其配置installOn(inst),创建一个ResettableClassFileTransformer,使用给定的检测类来实现AgentBuilder的一些配置
-
入口的配置做好了以后,包名开头的方法,就会进入这个切面
TraceAdvice。在方法执行前会进入OnMethodEnter,执行结束会执行OnMethodExitOnMethodEnter
@Advice.OnMethodEnter()
public static void enter(@Advice.Origin("#t") String className, @Advice.Origin("#m") String methodName) {
if (filterClassMethods(className, methodName)) {
return;
}
try {
String traceId = TrackManager.getCurrentSpan();
if (null == traceId) {
traceId = UUID.randomUUID().toString();
TrackContext.setTraceId(traceId);
}
TimeCostManager.start(className + "." + methodName);
TrackManager.createEntrySpan();
} catch (Throwable e) {
System.out.println("unknown error has occurred from OnMethodEnter." + e.getMessage());
}
}
-
首先通过
filterClassMethods对方法进行过滤,确定不纳入哪些方法统计 -
使用
threadLocal记录traceId和方法信息 -
使用这个计时器
TimeCostManager,记录开始时间OnMethodExit
@Advice.OnMethodExit()
public static void exit(@Advice.Origin("#t") String className, @Advice.Origin("#m") String methodName) {
if (filterClassMethods(className, methodName)) {
return;
}
try {
TimeCostManager.stop(className + "." + methodName);
TrackManager.getExitSpan();
} catch (Throwable e) {
System.out.println("unknown error has occurred from OnMethodExit." + e.getMessage());
}
}
- 首先通过
filterClassMethods对方法进行过滤,确定不纳入哪些方法统计 - 使用这个计时器
TimeCostManager,计算耗时 - 当前方法从
栈顶弹出,如果此时栈为空,则说明trace链路结束。此时开始统计,各个方法的调用时长,统计完clear掉trace数据
唠嗑唠嗑
具体的细节篇幅原因就不细讲了,这里只是讲了下实现思路。这个插件从设计上来说还有待改造,笔者正在快马加鞭地学习中。