You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java反射未按预期工作:NoSuchMethodException问题排查

问题分析与解决方案:@Stats注解统计方法执行时长失败

问题背景

自定义@Stats注解用于统计Java方法执行时长,编写StatsMethodCall工具类的statsMethod2方法时,计划通过反射从调用栈识别带注解的方法,但运行时抛出NoSuchMethodException,JUnit测试test2调用该方法时,无法正常统计带注解的myMethod执行数据。

核心原因

  1. 反射获取方法时忽略参数列表
    statsMethod2中使用getDeclaredMethod(element.getMethodName())仅通过方法名获取方法,但myMethod是带参数的方法,无参的同名方法不存在,直接抛出NoSuchMethodException,且异常被静默吞掉,导致后续无法找到目标方法。

  2. Lambda表达式干扰调用栈
    调用statsMethod2时使用了Lambda表达式() -> myClass.myMethod(...),调用栈中会生成匿名Lambda类的get方法,而非直接指向myMethod,遍历调用栈时无法定位到带注解的目标方法。

  3. 调用栈遍历逻辑不严谨
    遍历调用栈时没有过滤工具类自身的方法(比如statsMethod2),可能先匹配到非目标方法,或者跳过了正确的调用栈元素。

解决方案

方案一:修复反射与调用栈遍历逻辑

修改statsMethod2,遍历类的所有方法,匹配方法名+注解,同时过滤调用栈中的无关元素:

public static <T> T statsMethod2(final Supplier<T> method, final StatsRegistry statsRegistry) {
    String methodName = null;
    StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
    
    // 跳过前2个元素:getStackTrace()、statsMethod2本身
    for (int i = 2; i < stackTrace.length; i++) {
        StackTraceElement element = stackTrace[i];
        try {
            Class<?> clazz = Class.forName(element.getClassName());
            // 遍历类的所有方法,匹配方法名+@Stats注解
            for (Method m : clazz.getDeclaredMethods()) {
                if (m.getName().equals(element.getMethodName()) && m.isAnnotationPresent(Stats.class)) {
                    methodName = m.getName();
                    break;
                }
            }
            if (methodName != null) break;
        } catch (ClassNotFoundException e) {
            // 忽略无法加载的类
        }
    }
    
    if (methodName == null) {
        throw new IllegalStateException("找不到带@Stats注解的方法");
    }

    T result;
    final Timer.Context timerContext = statsRegistry.getTimer(methodName).time();
    try {
        result = method.get();
    } finally {
        timerContext.stop();
    }
    return result;
}

注意:如果存在方法重载,此逻辑仍可能匹配错误,需进一步对比方法参数类型(调用栈无法直接获取参数,需结合其他方式处理)

方案二:直接传递目标方法信息(更可靠)

放弃依赖调用栈,让调用者直接传递带注解的方法引用,通过反射获取方法信息:

// 修改statsMethod2的参数,接受目标Method对象
public static <T> T statsMethod2(final Method targetMethod, final Supplier<T> method, final StatsRegistry statsRegistry) {
    if (!targetMethod.isAnnotationPresent(Stats.class)) {
        throw new IllegalArgumentException("目标方法未标注@Stats注解");
    }
    String methodName = targetMethod.getName();
    
    T result;
    final Timer.Context timerContext = statsRegistry.getTimer(methodName).time();
    try {
        result = method.get();
    } finally {
        timerContext.stop();
    }
    return result;
}

调用方式(JUnit测试test2中):

StatsMethodCall.statsMethod2(
    MyClass.class.getDeclaredMethod("myMethod", String.class, int.class, double.class), 
    () -> myClass.myMethod("foo", 42, 3.14), 
    registry
);

方案三:使用AOP实现(最优雅)

通过Spring AOP或AspectJ直接拦截带@Stats注解的方法,无需手动处理调用栈和反射:

// 定义切面类
@Component
@Aspect
public class StatsAspect {
    private final StatsRegistry statsRegistry;

    public StatsAspect(StatsRegistry statsRegistry) {
        this.statsRegistry = statsRegistry;
    }

    @Around("@annotation(com.example.StatsMethodCall.Stats)")
    public Object aroundStatsMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        Timer.Context timerContext = statsRegistry.getTimer(methodName).time();
        try {
            return joinPoint.proceed();
        } finally {
            timerContext.stop();
        }
    }
}

此时调用方法无需包裹工具类,直接调用myClass.myMethod(...)即可自动统计。

测试验证

修改后运行test2,即可看到myMethod的调用次数、执行时长等统计数据正常输出。

内容的提问来源于stack exchange,提问作者clincks

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 00:02:03