Java反射未按预期工作:NoSuchMethodException问题排查
问题背景
自定义@Stats注解用于统计Java方法执行时长,编写StatsMethodCall工具类的statsMethod2方法时,计划通过反射从调用栈识别带注解的方法,但运行时抛出NoSuchMethodException,JUnit测试test2调用该方法时,无法正常统计带注解的myMethod执行数据。
核心原因
反射获取方法时忽略参数列表
statsMethod2中使用getDeclaredMethod(element.getMethodName())仅通过方法名获取方法,但myMethod是带参数的方法,无参的同名方法不存在,直接抛出NoSuchMethodException,且异常被静默吞掉,导致后续无法找到目标方法。Lambda表达式干扰调用栈
调用statsMethod2时使用了Lambda表达式() -> myClass.myMethod(...),调用栈中会生成匿名Lambda类的get方法,而非直接指向myMethod,遍历调用栈时无法定位到带注解的目标方法。调用栈遍历逻辑不严谨
遍历调用栈时没有过滤工具类自身的方法(比如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

