Java自定义注解处理器

好的,各位观众老爷,今天咱们来聊聊Java里一个既神秘又强大的家伙——自定义注解处理器!😎

别一听“注解处理器”就觉得高深莫测,其实它就像一位隐藏在幕后的魔法师,能帮你自动生成代码、检查错误,甚至还能改变程序的运行方式!是不是瞬间觉得它有点像《哈利波特》里的魔法棒?🪄

一、 啥是注解处理器?(Annotation Processor,简称AP)

首先,我们得搞清楚,注解处理器是啥玩意儿? 简单来说,它就是你在编译期“动手动脚”的工具。 正常情况下,Java代码编译成字节码(.class文件)之后,就等着JVM来执行了。 但有了注解处理器,你就可以在编译阶段对代码进行扫描,找到你感兴趣的注解,然后根据这些注解的信息,生成新的代码、检查代码规范,或者做任何你想做的事情。

你可以把注解处理器想象成一个“代码翻译器”,它能读懂你写在代码里的“魔法咒语”(注解),然后按照你的指示,生成对应的“魔法效果”(代码或操作)。

举个栗子🌰:

假设你想写一个自动生成Getter和Setter方法的工具。 没用注解处理器之前,你得吭哧吭哧地手动写一堆getXXX()setXXX()方法,累得半死。 但有了注解处理器,你只需要在一个字段上加个@Getter@Setter注解,编译的时候,注解处理器就会自动帮你生成对应的Getter和Setter方法,简直不要太爽!

二、 注解处理器能干啥?(用途大盘点)

注解处理器的用途那可是相当广泛,简直是居家旅行、必备良品! 它可以用来:

  1. 自动生成代码: 比如生成Getter/Setter方法、生成ORM框架的Entity类、生成JSON序列化/反序列化代码等等。 就像一个不知疲倦的代码生成器,帮你解放双手。
  2. 代码校验: 比如检查代码是否符合规范、检查是否存在潜在的错误、检查是否使用了过时的API等等。 就像一个严厉的代码审查员,帮你揪出代码里的“小虫子🐛”。
  3. 生成辅助文件: 比如生成配置文件、生成文档、生成SQL脚本等等。 就像一个贴心的助手,帮你整理各种繁琐的文件。
  4. AOP(面向切面编程): 通过注解来定义切面,然后在编译期织入到代码中。 就像一个隐形的魔术师,悄无声息地改变程序的行为。

三、 怎么写一个注解处理器?(手把手教学)

好了,说了这么多,终于到了激动人心的时刻——怎么写一个属于自己的注解处理器? 别怕,其实并不难,只需要掌握几个关键步骤:

1. 创建一个Java项目:

这个不用多说,用你喜欢的IDE(比如IntelliJ IDEA、Eclipse)创建一个新的Java项目就行。

2. 添加依赖:

你需要添加javax.annotation.processingcom.google.auto.service这两个依赖。

  • javax.annotation.processing:提供了注解处理器相关的API。
  • com.google.auto.service:可以自动注册你的注解处理器。
<!-- Maven -->
<dependency>
    <groupId>com.google.auto.service</groupId>
    <artifactId>auto-service</artifactId>
    <version>1.0.1</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.google.auto.service</groupId>
    <artifactId>auto-service-annotations</artifactId>
    <version>1.0.1</version>
</dependency>
<dependency>
    <groupId>com.squareup</groupId>
    <artifactId>javapoet</artifactId>
    <version>1.13.0</version>
</dependency>

<!-- Gradle -->
dependencies {
    implementation 'com.google.auto.service:auto-service-annotations:1.0.1'
    compileOnly 'com.google.auto.service:auto-service:1.0.1'
    annotationProcessor 'com.google.auto.service:auto-service:1.0.1'
    implementation 'com.squareup:javapoet:1.13.0'
}

这里,我们还引入了com.squareup:javapoet依赖,它是一个非常强大的Java代码生成库,可以让你用优雅的方式生成代码,避免手动拼接字符串的痛苦。

3. 创建注解:

首先,你需要定义一个注解,告诉注解处理器你想处理哪些代码。 比如,我们定义一个@Getter注解:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD) // 只能用在字段上
@Retention(RetentionPolicy.SOURCE) // 注解只保留在源码中,编译后会被丢弃
public @interface Getter {
}
  • @Target:指定注解可以使用的目标类型。 这里ElementType.FIELD表示这个注解只能用在字段上。
  • @Retention:指定注解的生命周期。 这里RetentionPolicy.SOURCE表示这个注解只保留在源码中,编译后会被丢弃。 这是因为注解处理器只需要在编译期读取注解信息,不需要在运行时保留。

4. 创建注解处理器类:

接下来,你需要创建一个类,继承javax.annotation.processing.AbstractProcessor,并实现process()方法。 这个process()方法就是注解处理器的核心,它会在编译期被调用,用来处理注解。

import com.google.auto.service.AutoService;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;

@AutoService(Processor.class) // 自动注册注解处理器
@SupportedAnnotationTypes("com.example.Getter") // 指定要处理的注解
@SupportedSourceVersion(SourceVersion.RELEASE_8) // 指定支持的Java版本
public class GetterProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 遍历所有使用了@Getter注解的元素
        for (Element element : roundEnv.getElementsAnnotatedWith(Getter.class)) {
            // 获取字段名
            String fieldName = element.getSimpleName().toString();
            // 获取字段类型
            String fieldType = element.asType().toString();

            // 生成Getter方法
            MethodSpec getterMethod = MethodSpec.methodBuilder("get" + capitalize(fieldName))
                    .addModifiers(Modifier.PUBLIC)
                    .returns(getTypeName(fieldType))
                    .addStatement("return this." + fieldName)
                    .build();

            // 获取类名
            TypeElement classElement = (TypeElement) element.getEnclosingElement();
            String className = classElement.getSimpleName().toString();
            String packageName = processingEnv.getElementUtils().getPackageOf(classElement).getQualifiedName().toString();

            // 生成类
            TypeSpec classBuilder = TypeSpec.classBuilder(className + "Generated")
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(getterMethod)
                    .build();

            // 生成Java文件
            JavaFile javaFile = JavaFile.builder(packageName, classBuilder)
                    .build();

            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Failed to write file: " + e.getMessage());
            }
        }

        return true;
    }

    // 首字母大写
    private String capitalize(String str) {
        return str.substring(0, 1).toUpperCase() + str.substring(1);
    }

    // 获取类型名称
    private Class<?> getTypeName(String typeName) {
        switch (typeName) {
            case "int":
                return int.class;
            case "long":
                return long.class;
            case "float":
                return float.class;
            case "double":
                return double.class;
            case "boolean":
                return boolean.class;
            case "byte":
                return byte.class;
            case "short":
                return short.class;
            case "char":
                return char.class;
            default:
                try {
                    return Class.forName(typeName);
                } catch (ClassNotFoundException e) {
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Class not found: " + typeName);
                    return Object.class;
                }
        }
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(Getter.class.getName());
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

代码解释:

  • @AutoService(Processor.class):使用AutoService注解自动注册注解处理器。
  • @SupportedAnnotationTypes("com.example.Getter"):指定要处理的注解的完整类名。
  • @SupportedSourceVersion(SourceVersion.RELEASE_8):指定支持的Java版本。
  • process()方法:
    • roundEnv.getElementsAnnotatedWith(Getter.class):获取所有使用了@Getter注解的元素。
    • element.getSimpleName().toString():获取字段名。
    • element.asType().toString():获取字段类型。
    • JavaFile.builder():使用JavaPoet库生成Java文件。
    • processingEnv.getFiler().createSourceFile():创建Java源文件。
    • processingEnv.getMessager().printMessage():打印编译信息。

5. 注册注解处理器:

由于我们使用了AutoService注解,所以不需要手动注册注解处理器。 编译的时候,AutoService会自动在META-INF/services目录下生成一个文件,里面包含了注解处理器的完整类名。

6. 使用注解:

现在,你就可以在你的代码中使用@Getter注解了:

package com.example;

public class Person {
    @Getter
    private String name;

    @Getter
    private int age;
}

7. 编译项目:

编译你的项目,注解处理器就会自动运行,生成PersonGenerated.java文件:

package com.example;

public class PersonGenerated {
  public java.lang.String getName() {
    return this.name;
  }

  public int getAge() {
    return this.age;
  }
}

四、 注意事项(踩坑指南)

  1. 注解处理器只在编译期运行: 这意味着你不能在运行时使用注解处理器。
  2. 注解处理器不能修改现有的代码: 它只能生成新的代码或文件。
  3. 编译顺序: 注解处理器会在编译的早期阶段运行,所以你生成的代码可能会影响后续的编译过程。
  4. 异常处理:process()方法中,一定要处理可能出现的异常,否则会导致编译失败。
  5. 性能问题: 如果你的注解处理器逻辑过于复杂,可能会影响编译速度。

五、 进阶技巧(更上一层楼)

  1. 使用JavaPoet: JavaPoet是一个非常强大的Java代码生成库,可以让你用优雅的方式生成代码,避免手动拼接字符串的痛苦。
  2. 使用Filer: Filer接口可以让你创建新的源文件、类文件和其他类型的文件。
  3. 使用Messager: Messager接口可以让你打印编译信息,比如错误、警告和提示。
  4. 使用Elements和Types: ElementsTypes接口可以让你访问程序元素的元数据,比如类名、字段名、方法名和类型信息。

六、 总结(划重点)

注解处理器是一个强大的工具,可以让你在编译期对代码进行处理,自动生成代码、检查代码规范,甚至还能改变程序的运行方式。 掌握了注解处理器,你就可以像一位魔法师一样,轻松地驾驭代码,提高开发效率,减少重复劳动。

希望这篇文章能帮助你入门注解处理器,开启你的代码魔法之旅! 如果你还有什么问题,欢迎在评论区留言,我们一起探讨! 😉

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注