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

如何统计特定包下方法的运行时调用次数(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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.23 05:09:50