如何统计特定包下方法的运行时调用次数(Java 21+Spring Boot 3.2.4)
问题:统计指定包下方法的运行时调用次数
我需要统计特定包(com.my.project.activities)下任意方法的运行时调用次数,最终要能导出结果(比如setA方法被调用17次、setB方法被调用3次这类形式)。
目前我已经通过代码成功提取了该包下所有以ActivityImpl结尾的实现类及其方法,但不知道怎么在生产代码中统计这些方法的实际调用次数。
提取类的核心代码
public static List<Class<?>> find(String scannedPackage) { String scannedPath = scannedPackage.replace(PKG_SEPARATOR, DIR_SEPARATOR); URL scannedUrl = Thread.currentThread().getContextClassLoader().getResource(scannedPath); if (scannedUrl == null) { throw new IllegalArgumentException(String.format(BAD_PACKAGE_ERROR, scannedPath, scannedPackage)); } File scannedDir = new File(scannedUrl.getFile()); List<Class<?>> classes = new ArrayList<Class<?>>(); for (File file : scannedDir.listFiles()) { classes.addAll(find(file, scannedPackage)); } return classes; } private static List<Class<?>> find(File file, String scannedPackage) { List<Class<?>> classes = new ArrayList<Class<?>>(); String resource = scannedPackage + PKG_SEPARATOR + file.getName(); if (file.isDirectory()) { for (File child : file.listFiles()) { classes.addAll(find(child, resource)); } } else if (resource.endsWith(CLASS_FILE_SUFFIX)) { int endIndex = resource.length() - CLASS_FILE_SUFFIX.length(); String className = resource.substring(0, endIndex); if (className.endsWith("ActivityImpl")) { try { classes.add(Class.forName(className)); } catch (ClassNotFoundException ignore) { } } } return classes; }
调用代码
public static void main(String[] args) throws Exception { List<Class<?>> classes = find("com.my.project.activities"); for (Class c : classes) { System.out.println("Class name: " + c.getName()); System.out.print("Methods: "); Method[] methods = c.getDeclaredMethods(); for (Method method : methods) { System.out.print(method.getName() + ", "); } System.out.println(); System.out.println(); } }
环境信息:Java 21,Spring Boot 3.2.4
解决方案
针对你的需求,结合Spring Boot环境,推荐三种可行的实现方式,覆盖不同场景:
一、Spring AOP(推荐,适用于Spring管理的Bean)
Spring AOP可以通过切面快速拦截指定包下的方法调用,实现无侵入的统计。
1. 依赖准备
Spring Boot 3.x默认包含AOP依赖,无需额外引入;若项目未自动集成,可添加:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
2. 实现调用次数统计类
用线程安全的容器存储方法调用次数,并提供导出功能:
import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.springframework.stereotype.Component; @Component public class MethodCallCounter { private final Map<String, Integer> callCountMap = new ConcurrentHashMap<>(); // 累加调用次数 public void increment(String methodKey) { callCountMap.put(methodKey, callCountMap.getOrDefault(methodKey, 0) + 1); } // 获取当前统计结果 public Map<String, Integer> getCallCounts() { return new ConcurrentHashMap<>(callCountMap); } // 导出统计结果到文件 public void exportToFile(String filePath) throws IOException { try (PrintWriter writer = new PrintWriter(new FileWriter(filePath))) { callCountMap.forEach((method, count) -> writer.println(method + ": " + count + "次")); } } }
3. 定义切面拦截方法
创建切面类,匹配目标包下的所有方法调用:
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Aspect @Component public class MethodCallAspect { @Autowired private MethodCallCounter counter; // 切点:拦截com.my.project.activities包下所有类的所有方法 @Before("execution(* com.my.project.activities..*(..))") public void countMethodCall(JoinPoint joinPoint) { // 生成唯一方法标识:全类名+方法名 String methodKey = joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName(); counter.increment(methodKey); } }
4. 使用方式
在需要导出结果的地方(比如接口、定时任务)注入MethodCallCounter,调用exportToFile即可:
@RestController public class ExportController { @Autowired private MethodCallCounter counter; @GetMapping("/export-call-count") public String export() throws IOException { counter.exportToFile("method-call-counts.txt"); return "导出成功"; } }
注意:Spring AOP仅能拦截Spring容器管理的Bean方法,非Bean类无法覆盖。
二、字节码增强(ASM,适用于所有类)
如果需要统计非Spring Bean的方法,可使用ASM字节码框架,在类加载时插入统计逻辑,实现底层无侵入统计。
1. 引入ASM依赖
<dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm</artifactId> <version>9.6</version> </dependency>
2. 实现ClassVisitor插入统计逻辑
import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; public class CountClassVisitor extends ClassVisitor { private final String targetPackage; private String className; public CountClassVisitor(ClassVisitor classVisitor, String targetPackage) { super(Opcodes.ASM9, classVisitor); this.targetPackage = targetPackage; } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { this.className = name.replace("/", "."); super.visit(version, access, name, signature, superName, interfaces); } @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); // 跳过构造方法,仅处理目标包下的类方法 if (mv != null && !name.equals("<init>") && className.startsWith(targetPackage)) { return new CountMethodVisitor(mv, className + "." + name); } return mv; } } class CountMethodVisitor extends MethodVisitor { private final String methodKey; public CountMethodVisitor(MethodVisitor methodVisitor, String methodKey) { super(Opcodes.ASM9, methodVisitor); this.methodKey = methodKey; } @Override public void visitCode() { // 在方法入口插入统计代码:调用静态工具类的increment方法 super.visitLdcInsn(methodKey); super.visitMethodInsn(Opcodes.INVOKESTATIC, "com/my/project/util/MethodCallCounter", "increment", "(Ljava/lang/String;)V", false); super.visitCode(); } }
3. 实现Java Agent
创建Agent在类加载时应用字节码转换:
import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.Instrumentation; import java.security.ProtectionDomain; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; public class CountAgent { public static void premain(String agentArgs, Instrumentation inst) { String targetPackage = agentArgs != null ? agentArgs : "com.my.project.activities"; inst.addTransformer(new ClassFileTransformer() { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { if (className.startsWith(targetPackage.replace(".", "/"))) { ClassReader cr = new ClassReader(classfileBuffer); ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES); CountClassVisitor cv = new CountClassVisitor(cw, targetPackage); cr.accept(cv, ClassReader.SKIP_DEBUG); return cw.toByteArray(); } return null; } }); } }
4. 打包与运行
- 把Agent代码打包成jar,在MANIFEST.MF中添加
Premain-Class: com.my.project.agent.CountAgent - 运行Spring Boot应用时添加JVM参数:
-javaagent:count-agent.jar=com.my.project.activities MethodCallCounter需改为静态类,方便字节码调用。
三、动态代理(适用于接口实现类)
对于接口驱动的实现类,可使用CGLIB或JDK动态代理包裹实例,统计调用次数。
实现CGLIB代理类
import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; import java.lang.reflect.Method; public class MethodCountProxy implements MethodInterceptor { private final Object target; private final MethodCallCounter counter; public MethodCountProxy(Object target, MethodCallCounter counter) { this.target = target; this.counter = counter; } // 创建代理实例 public static Object createProxy(Object target, MethodCallCounter counter) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(target.getClass()); enhancer.setCallback(new MethodCountProxy(target, counter)); return enhancer.create(); } @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { String methodKey = target.getClass().getName() + "." + method.getName(); counter.increment(methodKey); return proxy.invokeSuper(obj, args); } }
使用代理替换原Bean
在Spring配置中把原Bean替换为代理实例:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ProxyConfig { @Bean public MyActivityImpl myActivityImpl(MethodCallCounter counter) { MyActivityImpl target = new MyActivityImpl(); return (MyActivityImpl) MethodCountProxy.createProxy(target, counter); } }
内容的提问来源于stack exchange,提问作者AsfK
相关产品推荐
相关产品推荐

