谈谈这款Idea监控插件的原理

534 阅读3分钟

设计的分层

其实已经看过这篇文章(你还不知道这款Idea插件吗?)的伙伴已经大致可以知道代码的设计长啥样子了。

简单来说,分两层:Idea插件模块和代理模块。

Idea插件模块

这个模块是作为一个成型的Idea插件必备的一个部分。它可以捕获程序在Idea的动作,比如程序在Idea的启动命令、右键菜单的选择事件、工具栏的菜单设置等等。对这些动作做事件的监听,以此来获取并分析程序的Psi结构。

这个模块主要通过继承抽象类JavaProgramPatcherpatchJavaParameters来实现动态添加-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,与ASMjavassist并称为三大字节码增强框架。

  • 首先进入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);

}
  1. Advice.to(TraceAdvice.class),使用AgentBuilder.Transformer注册一个切面类TraceAdvice,这个切面会拦截到非main开头的所有方法
  2. ElementMatchers.nameStartsWith(packageName),匹配到包名开头的方法
  3. transform(transformer),绑定转换类及其配置
  4. installOn(inst),创建一个ResettableClassFileTransformer,使用给定的检测类来实现AgentBuilder的一些配置
  • 入口的配置做好了以后,包名开头的方法,就会进入这个切面TraceAdvice。在方法执行前会进入OnMethodEnter,执行结束会执行OnMethodExit

    • OnMethodEnter
@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());
        }

    }
  1. 首先通过filterClassMethods对方法进行过滤,确定不纳入哪些方法统计

  2. 使用threadLocal记录traceId和方法信息

  3. 使用这个计时器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());
        }
    }
  1. 首先通过filterClassMethods对方法进行过滤,确定不纳入哪些方法统计
  2. 使用这个计时器TimeCostManager,计算耗时
  3. 当前方法从栈顶弹出,如果此时栈为空,则说明trace链路结束。此时开始统计,各个方法的调用时长,统计完clear掉trace数据

唠嗑唠嗑

具体的细节篇幅原因就不细讲了,这里只是讲了下实现思路。这个插件从设计上来说还有待改造,笔者正在快马加鞭地学习中。

image.png