如何基于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
相关产品推荐
相关产品推荐

