如何调用Java应用中带有指定@Annotation的所有方法?
在Java中实现带指定注解方法的自动枚举与调用
核心需求
我的NetBeans项目中多个类存在带@Whiztactular注解的无参方法,需要完成以下操作:
- 枚举所有类与方法
- 查找带有指定注解的方法
- 实例化方法所属的类
- 调用这些无参方法
实现方案
前提:确保注解支持运行时反射
首先@Whiztactular注解必须声明为运行时保留,否则反射无法读取到它:
import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface Whiztactular { }
方案1:使用ClassGraph第三方库(推荐)
这是最简便的实现方式,无需手动指定包名,可自动扫描整个类路径下的所有类:
- 添加Maven依赖
<dependency> <groupId>io.github.classgraph</groupId> <artifactId>classgraph</artifactId> <version>4.8.163</version> </dependency>
- 核心实现代码
import io.github.classgraph.ClassGraph; import io.github.classgraph.ClassInfo; import io.github.classgraph.MethodInfo; import io.github.classgraph.ScanResult; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; public class WhiztactularRunner { public static void main(String[] args) throws Exception { List<Method> targetMethods = new ArrayList<>(); // 扫描整个类路径 try (ScanResult scanResult = new ClassGraph() .enableAllInfo() .scan()) { // 遍历所有类,筛选带指定注解的无参方法 for (ClassInfo classInfo : scanResult.getAllClasses()) { for (MethodInfo methodInfo : classInfo.getMethodInfo()) { if (methodInfo.hasAnnotation(Whiztactular.class) && methodInfo.getParameterInfo().length == 0) { Class<?> clazz = classInfo.loadClass(); Method method = clazz.getMethod(methodInfo.getName()); targetMethods.add(method); } } } } // 调用所有找到的方法 for (Method method : targetMethods) { Object instance = method.getDeclaringClass().getConstructor().newInstance(); method.invoke(instance); } } }
方案2:手动实现类路径扫描(无第三方依赖)
如果不想引入外部库,可手动遍历类路径下的目录与JAR包,加载类后通过反射筛选方法:
import java.io.File; import java.io.IOException; import java.lang.reflect.Method; import java.net.URL; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Enumeration; import java.util.List; import java.util.jar.JarEntry; import java.util.jar.JarFile; public class ManualWhiztactularRunner { public static void main(String[] args) throws Exception { List<Method> targetMethods = new ArrayList<>(); ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); // 遍历类路径下的所有资源 Enumeration<URL> resources = classLoader.getResources(""); while (resources.hasMoreElements()) { URL resource = resources.nextElement(); String protocol = resource.getProtocol(); if ("file".equals(protocol)) { // 处理目录中的类文件 File dir = new File(URLDecoder.decode(resource.getFile(), "UTF-8")); scanDirectory(dir, "", targetMethods); } else if ("jar".equals(protocol)) { // 处理JAR包中的类文件 String jarPath = resource.getFile().substring(5, resource.getFile().indexOf('!')); JarFile jarFile = new JarFile(URLDecoder.decode(jarPath, "UTF-8")); scanJar(jarFile, targetMethods); jarFile.close(); } } // 调用所有目标方法 for (Method method : targetMethods) { Object instance = method.getDeclaringClass().getConstructor().newInstance(); method.invoke(instance); } } private static void scanDirectory(File dir, String packageName, List<Method> methods) throws Exception { File[] files = dir.listFiles(); if (files == null) return; for (File file : files) { if (file.isDirectory()) { scanDirectory(file, packageName + file.getName() + ".", methods); } else if (file.getName().endsWith(".class")) { String className = packageName + file.getName().substring(0, file.getName().length() - 6); Class<?> clazz = Class.forName(className); findTargetMethods(clazz, methods); } } } private static void scanJar(JarFile jarFile, List<Method> methods) throws Exception { Enumeration<JarEntry> entries = jarFile.entries(); while (entries.hasMoreElements()) { JarEntry entry = entries.nextElement(); if (entry.getName().endsWith(".class") && !entry.isDirectory()) { String className = entry.getName().replace('/', '.').substring(0, entry.getName().length() - 6); try { Class<?> clazz = Class.forName(className); findTargetMethods(clazz, methods); } catch (ClassNotFoundException | NoClassDefFoundError e) { // 跳过无法加载的类 continue; } } } } private static void findTargetMethods(Class<?> clazz, List<Method> methods) { for (Method method : clazz.getDeclaredMethods()) { if (method.isAnnotationPresent(Whiztactular.class) && method.getParameterCount() == 0) { methods.add(method); } } } }
其他语言的实现对比
.NET中的实现逻辑(伪Java代码)
// 收集所有带指定注解的方法 List<MethodInfo> targetMethods = new ArrayList<>(); // 遍历当前应用域的所有程序集 for (Assembly a : AppDomain.currentDomain.getAssemblies()) { // 遍历程序集中的所有类 for (Type t : a.getTypes()) { // 遍历类中的所有方法 for (MethodInfo m : t.getMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly)) { // 筛选带@Whiztactular注解的方法 if (m.IsDefined(typeof(org.contoso.frobber.grob.Whiztactular), true)) targetMethods.add(m); } } }
调用方法:
// 执行所有找到的方法 for (MethodInfo m : targetMethods) { Object o = Activator.CreateInstance(m.DeclaringType); // 实例化类 m.Invoke(o, null); // 调用无参方法 }
Delphi中的实现逻辑
Delphi应用启动时,每个unit的初始化器会自动执行,测试类可自行注册:
initialization WhiztactularRunner.registerWhiztactularClass(TWuzzleWoozle);
但Java既没有文件级初始化机制,也没有类静态构造器,无法直接实现这种自动注册。
探索历程
- 想用JUnit运行测试
→ JUnit要求测试代码放在单独项目中 - 使用反射查找测试方法
→ 反射需要知道所有测试代码的包名 - 使用Reflections库
→ 仍需指定测试代码所在包名 - 自定义测试注解,反射查找带注解的方法
→ 还是需要包名 - 自定义TestCase注解,查找带注解的类
→ 依旧需要包名 - 自定义TestCase接口,查找实现类
→ 还是需要包名 - 自定义TestCase父类,查找子类
→ 还是需要包名 - 尝试用静态类构造器注册类
→ Java没有静态类构造器 - 尝试用包初始化器注册类
→ Java没有包初始化器 - 尝试监听包加载事件
→ Java没有包加载事件 - 尝试枚举所有包
→ 反射无法枚举所有包 - 尝试从类加载器获取已加载类
→ 类加载器仅知晓被引用的类,且可能存在多个实例 - 枚举当前类路径下的所有包 ⇐ 进行中
- 枚举本地所有JAR并加载类 ⇐ 进行中
- 已耗时4天解决问题,而.NET仅需5行代码,Delphi仅需3行代码
- 研究将409个JSP和498个Java文件转为ASP.NET和C# ⇐ 进行中
- 考虑放弃自动化测试 ⇐ 进行中
内容的提问来源于stack exchange,提问作者Ian Boyd
相关产品推荐
相关产品推荐

