从零开始SpringCloud Alibaba实战(79)——Spring-Boot+AOP+统计单次请求方法的执行次数和耗时

2023-11-11

前言

作为工程师,不能仅仅满足于实现了现有的功能逻辑,还必须深入认识系统。一次请求,流经了哪些方法,执行了多少次DB操作,访问了多少次文件操作,调用多少次API操作,总共有多少次IO操作,多少CPU操作,各耗时多少 ? 开发者应当知道这些运行时数据,才能对系统的运行有更深入的理解,更好的提升系统的性能和稳定性。

本文主要介绍使用AOP拦截器来获取一次请求流经方法的调用次数和调用耗时。

总体思路

使用AOP思想来解决。增加一个注解,然后增加一个AOP methodAspect ,记录方法的调用次数及耗时。

methodAspect 内部含有两个变量 methodCount, methodCost ,都采用了 ConcurrentHashMap 。这是因为方法执行时,可能是多线程写入这两个变量。

使用:

(1) 将需要记录次数和耗时的方法加上 MethodMeasureAnnotation 即可;

(2) 将 MethodMeasureAspect 作为组件注入到 ServiceAspect 中,并在 ServiceAspect 中打印 MethodMeasureAspect 的内容。

关注哪些方法

通常重点关注一个任务流程中的如下方法:

IO阻塞操作:文件操作, DB操作,API操作, ES访问;
同步操作:lock, synchronized, 同步工具所施加的代码块等;
CPU耗时:大数据量的format, sort 等。
一般集中在如下包:

service, core , report, sort 等。根据具体项目而定

代码

import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.Signature;

import org.aspectj.lang.annotation.Around;

import org.aspectj.lang.annotation.Aspect;

import org.aspectj.lang.reflect.MethodSignature;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.stereotype.Component;

 

import java.lang.reflect.Method;

import java.util.ArrayList;

import java.util.IntSummaryStatistics;

import java.util.List;

import java.util.Map;

import java.util.concurrent.ConcurrentHashMap;

import java.util.stream.Collectors;

 

/**

 * 记录一次请求中,流经的所有方法的调用耗时及次数

 *

 */

@Component

@Aspect

public class MethodMeasureAspect {
 

  private static final Logger logger = LoggerFactory.getLogger(MethodMeasureAspect.class);

 

  private Map<String, Integer> methodCount = new ConcurrentHashMap();

 

  private Map<String, List<Integer>> methodCost = new ConcurrentHashMap();

 

  @SuppressWarnings(value = "unchecked")

  @Around("@annotation(MethodMeasureAnnotation)")

  public Object process(ProceedingJoinPoint joinPoint) {
    Object obj = null;

    String className = joinPoint.getTarget().getClass().getSimpleName();

    String methodName = className + "_" + getMethodName(joinPoint);

    long startTime = System.currentTimeMillis();

    try {
      obj = joinPoint.proceed();

    } catch (Throwable t) {
      logger.error(t.getMessage(), t);

    } finally {
      long costTime = System.currentTimeMillis() - startTime;

      logger.info("method={}, cost_time={}", methodName, costTime);

      methodCount.put(methodName, methodCount.getOrDefault(methodName, 0) + 1);

      List<Integer> costList = methodCost.getOrDefault(methodName, new ArrayList<>());

      costList.add((int)costTime);

      methodCost.put(methodName, costList);

    }

    return obj;

  }

 

  public String getMethodName(ProceedingJoinPoint joinPoint) {
    Signature signature = joinPoint.getSignature();

    MethodSignature methodSignature = (MethodSignature) signature;

    Method method = methodSignature.getMethod();

    return method.getName();

  }

 

  public String toString() {
 

    StringBuilder sb = new StringBuilder("MethodCount:\n");

    Map<String,Integer> sorted =  MapUtil.sortByValue(methodCount);

    sorted.forEach(

        (method, count) -> {
          sb.append("method=" + method + ", " + "count=" + count+'\n');

        }

    );

    sb.append('\n');

    sb.append("MethodCosts:\n");

    methodCost.forEach(

        (method, costList) -> {
          IntSummaryStatistics stat = costList.stream().collect(Collectors.summarizingInt(x->x));

          String info = String.format("method=%s, sum=%d, avg=%d, max=%d, min=%d, count=%d", method,

                                      (int)stat.getSum(), (int)stat.getAverage(), stat.getMax(), stat.getMin(), (int)stat.getCount());

          sb.append(info+'\n');

        }

    );

 

    sb.append('\n');

    sb.append("DetailOfMethodCosts:\n");

    methodCost.forEach(

        (method, costList) -> {
          String info = String.format("method=%s, cost=%s", method, costList);

          sb.append(info+'\n');

        }

    );

    return sb.toString();

  }

}

import java.lang.annotation.Documented;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

 

/**

 * 记录方法调用

 */

@Retention(RetentionPolicy.RUNTIME)

@Target(ElementType.METHOD)

@Documented

public @interface MethodMeasureAnnotation {
 

}

public class MapUtil {
 

  public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) {
    Map<K, V> result = new LinkedHashMap<>();

    Stream<Map.Entry<K, V>> st = map.entrySet().stream();

 

    st.sorted(Comparator.comparing(e -> e.getValue())).forEach(e -> result.put(e.getKey(), e.getValue()));

 

    return result;

  }

 

}

@Around(“@annotation(MethodMeasureAnnotation)”) 仅仅指定了在携带指定注解的方法上执行。实际上,可以指定多种策略,比如指定类,指定包下。可以使用逻辑运算符 || , && , ! 来指明这些策略的组合。 例如:

@Around("@annotation(MethodMeasureAnnotation) "

      + "|| execution(* BizOrderDataService.*(..))"

      + "|| execution(* core.service.*.*(..)) "

      + "|| execution(* .strategy..*.*(..)) "

      + "|| execution(* *.generate*(..)) "

)

指明了五种策略的组合: 带有 MethodMeasureAnnotation 注解的方法; BizOrderDataService 类的所有方法; zzz.study.core.service 下的所有类的方法; zzz.study.core.strategy 包及其子包下的所有类的方法;zzz.study.core.report 包下所有类的以 generate 开头的方法。

execution表达式

@Pointcut 中, execution 使用最多。 其格式如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)

括号中各个pattern分别表示:

修饰符匹配(modifier-pattern?)
返回值匹配(ret-type-pattern)可以为*表示任何返回值,全路径的类名等
类路径匹配(declaring-type-pattern?)
方法名匹配(name-pattern)可以指定方法名 或者 代表所有, set 代表以set开头的所有方法
参数匹配((param-pattern))可以指定具体的参数类型,多个参数间用“,”隔开,各个参数也可以用“”来表示匹配任意类型的参数,如(String)表示匹配一个String参数的方法;(,String) 表示匹配有两个参数的方法,第一个参数可以是任意类型,而第二个参数是String类型;可以用(…)表示零个或多个任意参数
异常类型匹配(throws-pattern?)其中后面跟着“?”的是可选项。

ThreadLocal方案


import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.Map;

/**
 * @author liu
 * @date 2022年05月26日 10:31
 */
@Aspect
@Component
public class ControllerAspect {

    //定义一个日志的全局的静态
    private static Logger logger = LoggerFactory.getLogger(ControllerAspect.class);

    //ThreadLocal 维护变量 避免同步
    //ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
    ThreadLocal<Long> startTime = new ThreadLocal<>();// 开始时间
    //ThreadLocal 维护变量 避免同步 ThreadLocal是一个关于创建线程局部变量的类、
    //通常情况下,我们创建的变量是可以被任何一个线程访问并且修改的。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程无法访问和修改。
    //ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独以地改变自己的副本,而不会影响其它线程所对应的副本
    //ThreadLocal<Long> startTimes=new ThreadLocal<>();//开始时间

    /**
     * map1存放方法被调用的次数O
     */
    ThreadLocal<Map<String, Long >> map1 = new ThreadLocal<>();

    /**
     * map2存放方法总耗时
     */
    ThreadLocal<Map<String, Long >> map2 = new ThreadLocal<>();


    /**
     * 定义一个切入点. 解释下:
     *
     * ~ 第一个 * 代表任意修饰符及任意返回值. ~ 第二个 * 定义在web包或者子包 ~ 第三个 * 任意方法 ~ .. 匹配任意数量的参数.
     */
    /// static final String pCutStr = "execution(* com.appleyk.*..*(..))";
    // static final String pCutStr = "execution(* com.yanshu.controller..*..*(..))";
    static final String pCutStr = "execution(* com.andon.springbootdistributedlock.controller.*..*(..))";
    @Pointcut(value = pCutStr)
    public void logPointcut() {
    }

    /**
     * Aop:环绕通知 切整个包下面的所有涉及到调用的方法的信息
     * @param joinPoint
     * @return
     * @throws Throwable
     */
    @Around(value = "execution(* com.andon.springbootdistributedlock.controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {


        //初始化 一次
        if(map1.get() ==null ){
            map1.set(new HashMap<>());

        }

        if(map2.get() == null){
            map2.set(new HashMap<>());
        }

        long start = System.currentTimeMillis();
        try {
            Object result = joinPoint.proceed();
            if(result==null){
                //如果切到了 没有返回类型的void方法,这里直接返回
                return null;
            }
            long end = System.currentTimeMillis();

            logger.info("===================");
            String tragetClassName = joinPoint.getSignature().getDeclaringTypeName();
            String MethodName = joinPoint.getSignature().getName();

            Object[] args = joinPoint.getArgs();// 参数
            int argsSize = args.length;
            String argsTypes = "";
            String typeStr = joinPoint.getSignature().getDeclaringType().toString().split(" ")[0];
            String returnType = joinPoint.getSignature().toString().split(" ")[0];
            logger.info("类/接口:" + tragetClassName + "(" + typeStr + ")");
            logger.info("方法:" + MethodName);
            logger.info("参数个数:" + argsSize);
            logger.info("返回类型:" + returnType);
            if (argsSize > 0) {
                // 拿到参数的类型
                for (Object object : args) {
                    argsTypes += object.getClass().getTypeName().toString() + " ";
                }
                logger.info("参数类型:" + argsTypes);
            }

            Long total = end - start;
            logger.info("耗时: " + total + " ms!");

            if(map1.get().containsKey(MethodName)){
                Long count = map1.get().get(MethodName);
                map1.get().remove(MethodName);//先移除,在增加
                map1.get().put(MethodName, count+1);

                count = map2.get().get(MethodName);
                map2.get().remove(MethodName);
                map2.get().put(MethodName, count+total);
            }else{

                map1.get().put(MethodName, 1L);
                map2.get().put(MethodName, total);
            }

            return result;

        } catch (Throwable e) {
            long end = System.currentTimeMillis();
            logger.info("====around " + joinPoint + "\tUse time : " + (end - start) + " ms with exception : "
                    + e.getMessage());
            throw e;
        }

    }

    //对Controller下面的方法执行前进行切入,初始化开始时间
    @Before(value = "execution(* com.andon.springbootdistributedlock.controller.*.*(..))")
    public void beforMehhod(JoinPoint jp) {
        startTime.set(System.currentTimeMillis());
    }

    //对Controller下面的方法执行后进行切入,统计方法执行的次数和耗时情况
    //注意,这里的执行方法统计的数据不止包含Controller下面的方法,也包括环绕切入的所有方法的统计信息
    @AfterReturning(value = "execution(* com.andon.springbootdistributedlock.controller.*.*(..))")
    public void afterMehhod(JoinPoint jp) {
        long end = System.currentTimeMillis();
        long total =  end - startTime.get();
        String methodName = jp.getSignature().getName();
        logger.info("连接点方法为:" + methodName + ",执行总耗时为:" +total+"ms");

        //重新new一个map
        Map<String, Long> map = new HashMap<>();

        //从map2中将最后的 连接点方法给移除了,替换成最终的,避免连接点方法多次进行叠加计算
        //由于map2受ThreadLocal的保护,这里不支持remove,因此,需要单开一个map进行数据交接
        for(Map.Entry<String, Long> entry:map2.get().entrySet()){
            if(entry.getKey().equals(methodName)){
                map.put(methodName, total);

            }else{
                map.put(entry.getKey(), entry.getValue());
            }
        }

        for (Map.Entry<String, Long> entry :map1.get().entrySet()) {
            for(Map.Entry<String, Long> entry2 :map.entrySet()){
                if(entry.getKey().equals(entry2.getKey())){
                    System.err.println(entry.getKey()+",被调用次数:"+entry.getValue()+",综合耗时:"+entry2.getValue()+"ms");
                }
            }

        }
    }

}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

从零开始SpringCloud Alibaba实战(79)——Spring-Boot+AOP+统计单次请求方法的执行次数和耗时 的相关文章

  • 在Windows Server 2003下如何在本地系统帐户下运行jvisualvm.exe?

    我在带有 Java 1 6 u 20 的 Windows Server 2003 下将 GlassFish 3 0 1 作为 Windows 服务运行 总体上我很满意 我希望能够在这个 JVM 上使用 VisualVM 并使用无法在 Tom
  • 连接外部 Accumulo 实例和 java

    我正在尝试使用 Accumulo 连接到虚拟机 问题是 我无法将其连接到 Java 中 我可以看到 Apache 抛出的网页 但我无法让它与代码一起工作 我认为这是缺乏知识的问题而不是真正的问题 但我找不到这方面的文档 所有示例都使用 lo
  • 在不支持 CAS 操作的处理器上进行 CompareAndSet

    今天 我在一次采访中被问到下一个问题 如果您在具有不支持 CAS 操作的处理器的机器上调用 AtomicLong 的compareAndSet 方法 会发生什么情况 您能否帮我解决这个问题 并在可能的情况下提供一些全面描述的链接 From
  • Java 中的 <-- 是什么? [复制]

    这个问题在这里已经有答案了 我遇到了下面的片段 它输出到4 3 2 1 我从来没有遇到过 lt 在爪哇 Is lt 使 var1 的值变为 var2 的运算符 public class Test public static void mai
  • Java LostFocus 和 InputVerifier,按反向制表符顺序移动

    我有一个 GUI 应用程序 它使用 InputVerifier 在产生焦点之前检查文本字段的内容 这都是很正常的 然而 昨天发现了一个问题 这似乎是一个错误 但我在任何地方都找不到任何提及它的地方 在我将其报告为错误之前 我想我应该问 我在
  • 通过Zuul上传大文件

    我在通过 zuul 上传大文件时遇到问题 我正在使用 apache commons 文件上传 https commons apache org proper commons fileupload https commons apache o
  • 我对线程失去了理智

    我想要这个类的对象 public class Chromosome implements Runnable Comparable
  • Java中Gson、JsonElement、String比较

    好吧 我想知道这可能非常简单和愚蠢 但在与这种情况作斗争一段时间后 我不知道发生了什么 我正在使用 Gson 来处理一些 JSON 元素 在我的代码中的某个位置 我将 JsonObject 的 JsonElements 之一作为字符串获取
  • 将类转换为 JSONObject

    我有好几堂这样的课 我想将类转换为 JSONObject 格式 import java io Serializable import com google gson annotations SerializedName public cla
  • 如何在 IntelliJ IDEA 中运行 akka actor

    来自 Akka 网站文档 然后 这个主要方法将创建所需的基础设施 运行演员 启动给定的主要演员并安排 一旦主要参与者终止 整个应用程序就会关闭 因此 您将能够使用类似于以下的命令运行上面的代码 下列的 java classpath akka
  • 数据库中的持久日期不等于检索日期

    我有一个具有 Date 属性的简单实体类 此属性对应于 MySQL 日期时间列 Entity public class Entity Column name start date Temporal TemporalType TIMESTAM
  • 如何向页面添加 HTML 页眉和页脚?

    如何使用 itext 从 html 源添加标题到 pdf 目前 我们已经扩展了 PdfPageEventHelper 并重写了这些方法 工作正常 但当我到达 2 个以上页面时 它会抛出 RuntimeWorkerException Over
  • 如何使用 Jersey 将嵌套列表封送为 JSON?我得到一个空数组或一个包含数组的单元素字典数组

    我正在开发一个使用 Jersey 将对象转换为 JSON 的项目 我希望能够写出嵌套列表 如下所示 data one two three a b c 我想要转换的对象首先将数据表示为 gt gt 我认为 Jersey 会做正确的事情 以上输
  • 文本视图不显示全文

    我正在使用 TableLayout 和 TableRow 创建一个简单的布局 其中包含两个 TextView 这是代码的一部分
  • 我所有的 java 应用程序现在都会抛出 java.awt.headlessException

    所以几天前我有几个工作Java应用程序使用Swing图书馆 JFrame尤其 他们都工作得很好 现在他们都抛出了这个异常 java awt headlessexception 我不知道是什么改变了也许我的Java版本不小心更新了 谢谢你尽你
  • 用于请求带有临时缓存的远程 Observable 的 RxJava 模式

    用例是这样的 我想暂时缓存最新发出的昂贵的Observable响应 但在它过期后 返回到昂贵的源Observable并再次缓存它 等等 一个非常基本的网络缓存场景 但我真的很难让它工作 private Observable
  • Android ScrollView,检查当前是否滚动

    有没有办法检查标准 ScrollView 当前是否正在滚动 方向是向上还是向下并不重要 我只需要检查它当前是否正在滚动 ScrollView当前形式不提供用于检测滚动事件的回调 有两种解决方法可用 1 Use a ListView并实施On
  • 带 getClassLoader 和不带 getClassLoader 的 getResourceAsStream 有什么区别?

    我想知道以下两者之间的区别 MyClass class getClassLoader getResourceAsStream path to my properties and MyClass class getResourceAsStre
  • 如何让 Firebase 与 Java 后端配合使用

    首先 如果这个问题过于抽象或不适合本网站 我想表示歉意 我真的不知道还能去哪里问 目前我已经在 iOS 和 Android 上开发了应用程序 他们将所有状态保存在 Firebase 中 因此所有内容都会立即保存到 Firebase 实时数据
  • 关闭扫描仪是否会影响性能

    我正在解决一个竞争问题 在问题中 我正在使用扫描仪获取用户输入 这是 2 个代码段 一个关闭扫描器 一个不关闭扫描器 关闭扫描仪 import java util Scanner public class JImSelection publ

随机推荐

  • 用Beamer制作幻灯片(卷二 色彩篇)

    在用Beamer类制作幻灯片卷一里讲解了怎么使用Latex的简单的类来制作幻灯片 只是给了基本的怎么制作幻灯片的一个大体框架 但是一个很好的幻灯片远远不止这些功能 beamer的功能还有很多 今天要介绍的内容就是给幻灯片增加一些绚丽的效果
  • 服务器的作用

    服务器的作用 1 服务器就好像是一个电话总台一样 而其他的网络设备就像是公共电话 所有的数据传输都要经过服务器的处理 2 服务器作为一个网络节点 为用户提供数据处理服务 最常见的就是使用服务器为自己搭建一个网站 3 服务器运算能力强 可以长
  • C语言实现一个整型计算器的不同方法

    文章目录 一 实现一个整型计算器 二 运用函数指针数组来实现整型计算器 也就是转移表 三 运用回调函数实现整型计算器 一 实现一个整型计算器 代码如下 include
  • layui显示表格数据的id的两种形式

    1 获取数据库表字段id field id title 用户ID width 100 fixed left align center templet function d return d id 2 templet属性获得id为 title
  • 【大模型】—LangChain开源框架介绍

    大模型 LangChain开源框架介绍 2023年可以说是AI大语言模型发展元年 随着OpenAI的ChatGPT和GPT 4的发布 点燃了人工智能大语言模型的发展浪潮 各大科技公司纷纷推出了自家的大语言模型产品 各国更是将大语言模型的发展
  • springboot 跨域过滤器配置

    添加maven包依赖
  • gbk to utf8 utf8 to gbk

    My Study About My Learn or Study etc GBK和UTF8之间的转换 By Cnangel on October 8 2012 10 10 AM No Comments 关于GBK和UTF 8之间的转换 很多
  • osg学习(七十一)如何给顶点着色器传递顶点数据

    缩放不会影响传递到着色器中顶点坐标缩放 osg会自动向着色器传递osg Vertex osg ModelViewProjectionMatrix等变量 不需要再定义 在着色器中直接使用即可 设置顶点数据 osg Geometry cpp v
  • 2579 启蒙练习-跑步问题

    有二个人在n米的椭圆形的跑道跑步 他们从同一个起点出发 两个人运动方向相同时 每a秒相遇一次 两个人运动方向相反时 每b秒相遇一次 求二人的速度 v1 v2 分别是多少 本题数据保证 n a b v1 v2 都会是整数 收起 输入 三个数
  • SQL Server 基础语法1(超详细!)

    文章目录 创建数据库 增加次要数据库文件 删除次要数据库文件 删除数据库 建立表格 新增列 改变长度 删除表 查询表 删除列 创建数据库 create database school 数据库名 on 数据文件 name school dat
  • SQL Server 基础操作(五)导入和导出数据表

    导入数据表 1 选择需要导数据的数据库右击 任务 导入数据 2 选择数据源 数据源代表数据表从哪里导入到当前的数据库中 填写数据源服务器名称 本地导入 1433 远程导入 IP 1433 3 选择导入的目标数据库 选择导入到那个数据库中 4
  • hive数据仓库课后答案

    第一章 数据仓库的简介 一 填空题 1 数据仓库的目的是构建面向 分析 的集成化数据环境 2 Hive是基于 Hadoop 的一个数据仓库工具 3 数据仓库分为3层 即 源数据层 数据应用层 和数据仓库层 4 数据仓库层可以细分为 明细层
  • k8s部署SpringCloud应用

    一 准备工作 将v2目录上传到 root 目录 下载地址 链接 https pan baidu com s 1oqED4Kew5BeLFqms6U6ISw 提取码 lzx9 springcloud1 项目 用k8s部署 eureka eur
  • (JAVA练习)输入,输出二维数组

    题目 输入 输出二维数组 解答 import java util Scanner public class Erweishuzu public static void main String args 二维数组练习 Scanner sc n
  • element-ui 中dialog居中

    标题element ui 中dialog居中 el dialog display flex flex direction column margin 0 important position absolute top 50 left 50
  • 一款强大的浏览器翻译插件 - 沉浸式的翻译

    起因 前一段时间谷歌翻译宣布跑路 不再对大陆用户提供服务 听闻这一噩耗我不由得心里一惊 燕子 啊不是 谷歌没有你我可咋活呀 对于没太大工作需求 顶多遇上几个不认识单词或需要翻译网页的我来说 Chrome 自带的谷歌翻译可以说是我最常用的翻译
  • micropython源码分析之qstr

    前言 最近在研究micropython的源码编译过程 简单记录下关于qstr部分内容 本篇文章基于micropython1 18版本源码 1 19版本及之后可能会略有差异 标识符与相应对象的联系 Micropython中有很多标识符 例如l
  • 工作笔记:TrueCrypt编译记录

    工作笔记 TrueCrypt编译记录 TrueCrypt的最新版本6 2可以从官方网站上下载 我从这里下载了一个6 1的 http freedos pri ee truecrypt 在TrueCrypt官方网站上很多旧版本都没了 这里却很全
  • 关于Python中中文文本文件使用二进制方式读取后的解码UnicodeDecodeError问题

    最近老猿在进行文件操作的验证测试 发现对于中文文本文件如果使用二进制方式打开 返回的类型是bytes 如果要转换成可读的字符串信息需要进行解码 可是老猿使用decode 或decode UTF 8 解码后报错 Traceback most
  • 从零开始SpringCloud Alibaba实战(79)——Spring-Boot+AOP+统计单次请求方法的执行次数和耗时

    文章目录 前言 代码 ThreadLocal方案 前言 作为工程师 不能仅仅满足于实现了现有的功能逻辑 还必须深入认识系统 一次请求 流经了哪些方法 执行了多少次DB操作 访问了多少次文件操作 调用多少次API操作 总共有多少次IO操作 多