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

如何基于Maven Mojo实现构建时扫描注解类并生成Java工具类

哇,你已经搞定了Maven插件最核心的启动部分,接下来咱们把剩下的几个关键环节落地,让你的插件真正完成扫描、生成代码并打包的完整流程:

1. 扫描类路径,定位带特定注解的类

首先得拿到项目的编译类路径,你已经有MavenProject实例了,直接用它来获取类路径元素就行。这里有两种常用的扫描方式,按需选:

方式一:用Reflections库(简单高效)

Reflections能帮你快速扫描类路径上的注解类,省去自己遍历文件的麻烦。先在插件的pom里加依赖:

<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId>
    <version>0.10.2</version>
</dependency>

然后在Mojo里写扫描逻辑:

// 获取编译类路径并构建类加载器
List<String> classpathElements = project.getCompileClasspathElements();
URL[] classpathUrls = new URL[classpathElements.size()];
for (int i = 0; i < classpathElements.size(); i++) {
    classpathUrls[i] = new File(classpathElements.get(i)).toURI().toURL();
}
URLClassLoader projectClassLoader = new URLClassLoader(classpathUrls, Thread.currentThread().getContextClassLoader());

// 初始化Reflections并扫描带目标注解的类
Reflections reflections = new Reflections(new ConfigurationBuilder()
        .setUrls(ClasspathHelper.forClassLoader(projectClassLoader))
        .setScanners(new TypeAnnotationsScanner()));
Set<Class<?>> targetClasses = reflections.getTypesAnnotatedWith(YourSpecificAnnotation.class);

方式二:用ASM(轻量无类加载风险)

如果项目类很多,不想加载所有类到内存,ASM是更好的选择——它直接解析字节码文件,不需要加载类。先加ASM依赖:

<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>9.5</version>
</dependency>

然后遍历类路径上的文件/JAR包:

List<String> classpathElements = project.getCompileClasspathElements();
Set<String> annotatedClassNames = new HashSet<>();

for (String element : classpathElements) {
    File file = new File(element);
    if (file.isDirectory()) {
        traverseDirectory(file, "");
    } else if (file.getName().endsWith(".jar")) {
        try (JarFile jarFile = new JarFile(file)) {
            Enumeration<JarEntry> entries = jarFile.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                if (entry.getName().endsWith(".class") && !entry.isDirectory()) {
                    try (InputStream is = jarFile.getInputStream(entry)) {
                        ClassReader cr = new ClassReader(is);
                        cr.accept(new ClassVisitor(Opcodes.ASM9) {
                            @Override
                            public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
                                // 对比目标注解的字节码描述符
                                if (Type.getDescriptor(YourSpecificAnnotation.class).equals(descriptor)) {
                                    String className = entry.getName().replace(".class", "").replace("/", ".");
                                    annotatedClassNames.add(className);
                                }
                                return super.visitAnnotation(descriptor, visible);
                            }
                        }, 0);
                    }
                }
            }
        } catch (IOException e) {
            getLog().error("Failed to scan JAR: " + file.getName(), e);
        }
    }
}

// 遍历目录的辅助方法
private void traverseDirectory(File dir, String packagePrefix) {
    File[] files = dir.listFiles();
    if (files == null) return;
    for (File file : files) {
        if (file.isDirectory()) {
            traverseDirectory(file, packagePrefix + file.getName() + ".");
        } else if (file.getName().endsWith(".class")) {
            String className = packagePrefix + file.getName().replace(".class", "");
            // 这里同样可以用ASM解析单个.class文件,逻辑和JAR里的一致
            try (InputStream is = new FileInputStream(file)) {
                ClassReader cr = new ClassReader(is);
                cr.accept(new ClassVisitor(Opcodes.ASM9) {
                    @Override
                    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
                        if (Type.getDescriptor(YourSpecificAnnotation.class).equals(descriptor)) {
                            annotatedClassNames.add(className);
                        }
                        return super.visitAnnotation(descriptor, visible);
                    }
                }, 0);
            } catch (IOException e) {
                getLog().error("Failed to scan class file: " + file.getName(), e);
            }
        }
    }
}
2. 生成工具支持的Java代码

推荐用JavaPoet来生成代码,它的语法非常贴近Java语法,可读性强。先加依赖:

<dependency>
    <groupId>com.squareup</groupId>
    <artifactId>javapoet</artifactId>
    <version>1.13.0</version>
</dependency>

然后写生成逻辑,比如给每个注解类生成一个工具类:

// 指定生成目录——Maven会自动识别target/generated-sources/annotations为源码目录
File generatedSourcesDir = new File(project.getBuild().getDirectory(), "generated-sources/annotations");
if (!generatedSourcesDir.exists()) {
    generatedSourcesDir.mkdirs();
}

// 遍历找到的注解类,生成对应的工具类
for (Class<?> clazz : targetClasses) {
    // 构建工具类的代码
    TypeSpec toolClass = TypeSpec.classBuilder(clazz.getSimpleName() + "Tool")
            .addModifiers(Modifier.PUBLIC)
            .addMethod(MethodSpec.methodBuilder("processInstance")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .returns(void.class)
                    .addParameter(TypeName.get(clazz), "instance")
                    .addStatement("System.out.println(\"Processing $L instance: $L\", $S, instance)", 
                            clazz.getSimpleName(), clazz.getSimpleName(), clazz.getSimpleName())
                    .build())
            .build();

    // 生成Java文件
    JavaFile javaFile = JavaFile.builder(clazz.getPackageName(), toolClass)
            .build();
    javaFile.writeTo(generatedSourcesDir);
}

如果用ASM方式拿到的是类名字符串,你可以用Class.forName()加载类(需要用之前的项目类加载器),或者直接用类名字符串来生成代码。

3. 让生成的代码被编译并打包进JAR

这一步很关键,要告诉Maven把生成的目录加入编译源路径,这样生成的代码会和主代码一起编译,最终打包进JAR:

// 把生成目录添加到项目的编译源路径
project.addCompileSourceRoot(generatedSourcesDir.getAbsolutePath());

另外,一定要把你的Mojo绑定到正确的构建阶段,比如generate-sources阶段——这个阶段在编译主代码之前执行,刚好适合生成代码:

@Mojo(name = "generate-tool-classes", defaultPhase = LifecyclePhase.GENERATE_SOURCES)
public class YourAnnotationProcessorMojo extends AbstractMojo {
    // 你的Mojo代码...
}
4. 几个实用小提示
  • 增量构建:如果想让插件只在注解类变化时重新生成代码,可以实现IncrementalMojo接口,通过对比源文件和生成文件的时间戳来判断是否需要执行,提升构建效率。
  • 日志输出:用Mojo的getLog()方法输出日志,方便调试和排查问题,比如getLog().info("Found " + targetClasses.size() + " annotated classes")。
  • 类加载隔离:Maven插件的类加载环境和项目的是隔离的,所以一定要用项目的类加载器来加载项目中的类,避免类找不到的问题。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.21 06:43:49