如何获取一次请求流经方法的调用次数和调用耗时

如题,在日常开发中,经常会遇到一些问题的技术选型和方案制定。这个文集的目的就是提供一些问题的技术思考,便于我们在遇到问题的时候,可以更快的应答。当然,有些方案会随着时间变得不再合理,这个时候,也需要我们不断学习新的技术来优化我们的方案。
首先,看到这个问题的时候,我们需要明确我们的目的是什么,只有知道我们的目的,才能更好的去实现。
针对如何获取一次请求流经方法的调用次数和调用耗时,我们可能是想去做统计,也可能是想进行性能优化。
比如针对性能优化,使用AOP的代码开发就过于复杂,开发成本高。但是通过加日志打印调用时间需要上线。这个时候我们可以使用一些调优的工具,比如Arthas。本文我们主要还是介绍使用AOP来解决,关于Arthas如何进行问题定位和性能调优,我们后续在其他文章中介绍。

总体思路

  • 使用AOP思想来解决。增加一个注解,然后增加一个AOP methodAspect ,记录方法的调用次数及耗时。
  • methodAspect 内部含有两个变量 methodCount, methodCost ,都采用了 ConcurrentHashMap 。这是因为方法执行时,可能是多线程写入这两个变量。

过程:
(1) 将需要记录次数和耗时的方法加上 MethodMeasureAnnotation ;
(2) 在 MethodMeasureAspect 的 @PointCut 部分写上需要拦截通知的方法的表达式;
(3) 将 MethodMeasureAspect 作为组件注入到 ServiceAspect 中,并在 ServiceAspect 中打印 MethodMeasureAspect 的内容。

AOP基本概念

  • Aspect:应用程序的某个模块化的关注点;通常是日志、权限、事务、打点、监控等。
  • JointPoint:连接点,程序执行过程中明确的点,一般是方法的调用。
  • Pointcut: 切点。指定施加于满足指定表达式的方法集合。Spring 默认使用 AspectJ pointcut 表达式。
  • Advance: 通知。指定在方法执行的什么时机,不同的Advance对应不同的切面方法;有before,after,afterReturning,afterThrowing,around。同时也可以采用多种策略,具体用法可以参考官方API。
  • TargetObject: 目标对象。通过Pointcut表达式指定的将被通知执行切面逻辑的实际对象。
  • AOP proxy: 代理对象。由AOP框架创建的代理,用于回调实际对象的方法,并执行切面逻辑。Spring实现中,若目标对象实现了至少一个接口,则使用JDK动态代理,否则使用 CGLIB 代理。优先使用JDK动态代理。
  • Weaving:织入。将切面类与应用对象关联起来。Spring使用运行时织入。
    通常 Pointcut 和 Advance 联合使用。即在方法上加上 @Advance(@Pointcut)

哪些场景下,AOP不会生效

  • 被切面方法调用的内部方法;
  • final 方法;
  • private 方法;
  • 静态方法。

如果类 C 有三个公共方法,a,b,c ; a 调用 b ,b 调用 c 。会发现 b,c 是不会执行切面逻辑的。这是因为Spring的AOP主要基于动态代理机制。当调用 a 时,会调用代理的 a 方法,也就进入到切面逻辑,但是当 a 调用 b 时, b 是直接在目标对象上执行,而不是在代理对象上执行,因此,b 是不会进入到切面逻辑的。

代码实现

MethodMeasureAspect.java

package zzz.study.aop;

import zzz.study.util.MapUtil;

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(zzz.study.aop.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();
  }
}

MethodMeasureAnnotation.java

package zzz.study.aop;

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 {

}

MapUtil.java

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;
  }

}

参考连接:https://www.cnblogs.com/lovesqcc/p/9380167.html

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容