Java 注解:注解处理器获取泛型真实类型

[TOC]

Java 注解:注解处理器获取泛型真实类型

注解 annotation 是 Java 中的一大特性,是插入代码中的元数据。注解的使用能够大大简化代码的编写,所以在很多框架中得到了使用,比如 Web 框架 Spring 中的 @Service、@Resource 注解,比如参数校验框架 hibernate-validator 中的 @NotNull 等注解。

如何定义注解

定义注解需要用到元注解 meta annotation,即描述注解的注解。元注解有以下几类:

  1. @Target:描述了注解所修饰的对象范围,比如:类、方法、字段、以及注解本身等等。
  2. @Retention:定义注解被保留的时间长短,有三种:源文件、class 文件、运行时。
  3. @Documented:表示含有该注解类型的元素(带有注释的)会通过javadoc或类似工具进行文档化。
  4. @Inherited:表示注解类型能被自动继承。

Java 中使用 Class 来定义类,使用 enum 定义枚举,使用 @interface 来定义注解。具体定义格式如下:

  public @interface 注解名 { 定义体 }

其中的定义体里可以写若干方法来表示配置,方法的名称就是参数的名称,返回值类型就是参数的类型,可以通过 default 来声明参数的默认值。下面给出一个注解的示例:

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoParse {
    String key();
    int type() default 0;
}

该注解 AutoParse 可以用于描述 FIELD 即类中字段,在运行时仍然能够获取,不可被继承(没有使用 Inherited 注解描述)。

获取注解信息

注解通常用来描述类、方法等,那如何获取一个方法或类上的注解呢?可以通过 Class 类或 Method 类对应的 getAnnotation 方法获取:

public final class Class<T> implements java.io.Serializable,
                              GenericDeclaration,
                              Type,
                              AnnotatedElement {
    public Annotation[] getAnnotations();
    public <A extends Annotation> A getAnnotation(Class<A> annotationClass);
    public <A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass);
    public Annotation[] getDeclaredAnnotations();
}

public final class Method extends Executable {
    public <T extends Annotation> T getAnnotation(Class<T> annotationClass);
    public Annotation[] getDeclaredAnnotations();
    // 返回方法参数的注解
    public Annotation[][] getParameterAnnotations();
}

getDeclaredAnnotations 方法只返回直接存在于此元素上的注解,不会返回继承的注解;而 getAnnotation 则返回全部注解,包含继承的。

注:Field 也有类似的方法可以获取注解。

注解处理器

注解处理器 annotation processor 是 javac 内置的编译时扫描和处理注解的工具,比较常见的用法是在编译时获取注解信息,动态生成 Java 文件。

AbstractProcessor

想要自定义注解处理器需要继承实现 AbstractProcessor 类:

  1. init 方法入参是环境变量 ProcessingEnvironment,从中可以拿到报告错误信息的 Messager,用于生成代码文件的 Filer。
  2. process 抽象方法,是我们需要实现的,用于处理注解信息的方法,生成类文件的代码放这里。
public abstract class AbstractProcessor implements Processor {
    public synchronized void init(ProcessingEnvironment processingEnv) {
        if (initialized)
            throw new IllegalStateException("Cannot call init more than once.");
        Objects.requireNonNull(processingEnv, "Tool provided null ProcessingEnvironment");

        this.processingEnv = processingEnv;
        initialized = true;
    }
    
    public abstract boolean process(Set<? extends TypeElement> annotations,
                                    RoundEnvironment roundEnv);
}

示例:注解处理器获取泛型信息

Java 中的泛型有一个类型擦除的概念:

Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

那么如何在运行时获取某对象的某个 Field 的真实类型信息呢?既然是在编译期间擦除的,那么我们就可以注解处理器在编译期间获取泛型的真实类型信息。

以下给出一个处理上面的 @AutoParse 注解的注解处理器示例,该注解处理器获取了注解所描述字段的类型信息,并将这些信息写入了一个类文件中:

  1. @SupportedAnnotationTypes:指明要处理的注解。
  2. @SupportedSourceVersion(SourceVersion.RELEASE_7):指明适合的 Java 版本。
  3. 继承 process 方法,做真正的处理工作。
  4. Filer 用于创建新的 Java 类。
  5. RoundEnvironment 类包含了被注解所描述的 Field 的真实类型信息。
@SupportedAnnotationTypes({ "com.albon.arith.annotation.procecssor.AutoParseField" })
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class AutoParseFieldProcessor extends AbstractProcessor {

    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        filer = processingEnv.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Map<String, String> fieldTypeMap = Maps.newHashMap();

        for (Element elem : roundEnv.getElementsAnnotatedWith(AutoParseField.class)) {
            AutoParseField annotation = elem.getAnnotation(AutoParseField.class);
            String message = System.currentTimeMillis() + " - annotation found in " + elem.getSimpleName()
                    + " with key " + annotation.key() + " kind " + elem.getKind() + " class " + elem.getClass()
                    + " asType " + elem.asType() + "\n\tgetEnclosingElement().getSimpleName() "
                    + elem.getEnclosingElement().getSimpleName() + "\n\tgetEnclosingElement().asType() "
                    + elem.getEnclosingElement().asType();
            fieldTypeMap.put(elem.getEnclosingElement().asType().toString() + "#" + elem.getSimpleName(),
                    elem.asType().toString());

            processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message);
        }

        if (fieldTypeMap.isEmpty()) {
            return true;
        }

        Writer writer = null;
        try {
            JavaFileObject jfo = filer
                    .createSourceFile("com.albon.arith.annotation.service.AutoParseFieldInfo");
            writer = jfo.openWriter();
            writer.write("package com.albon.arith.annotation.service;\n" +
                    "\n" +
                    "import com.google.common.collect.Maps;\n" +
                    "\n" +
                    "import java.util.Map;\n" +
                    "\n" +
                    "public class AutoParseFieldInfo {\n" +
                    "\n" +
                    "    // key: classpath#fieldName, value: fieldType\n" +
                    "    public static final Map<String, String> FIELD_TYPE_MAP = Maps.newHashMap();\n" +
                    "\n" +
                    "    static {\n");

            for (Map.Entry<String, String> entry : fieldTypeMap.entrySet()) {
                writer.write("        FIELD_TYPE_MAP.put(\"" +
                        entry.getKey() +
                        "\", \"" +
                        entry.getValue() +
                        "\");\n");
            }

            writer.write("    }\n" +
                    "\n" +
                    "    public static void main(String[] args) {\n" +
                    "        System.out.println(FIELD_TYPE_MAP);\n" +
                    "    }\n" +
                    "}\n");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return true; // no further processing of this annotation type
    }
}

我们写了一个使用 @AutoParse 注解描述的 SimplePoJo 类:

public class SimplePoJo {

    @AutoParse(key = "first")
    private Integer first;
    @AutoParse(key = "second")
    private String second;
    @AutoParse(key = "third")
    private List<String> third;
}

还需要在配置文件中指定使用该注解处理器,请在 resources 文件夹下新建 META-INF/services 文件夹,再新建文件 javax.annotation.processing.Processor,文件内容如下:

com.albon.arith.annotation.procecssor.AutoParseFieldProcessor

代码完成之后,使用 mvn clean package 进行编译,编译后可以看到经由注解处理器生成的 AutoParseFieldInfo.java 文件,其内容如下所示:

public class AutoParseFieldInfo {
    // key: classpath#fieldName, value: fieldType
    public static final Map<String, String> FIELD_TYPE_MAP = Maps.newHashMap();

    static {        FIELD_TYPE_MAP.put("com.albon.arith.annotation.service.SimplePoJo#third", "java.util.List<java.lang.String>");
        FIELD_TYPE_MAP.put("com.albon.arith.annotation.service.SimplePoJo#second", "java.lang.String");
        FIELD_TYPE_MAP.put("com.albon.arith.annotation.service.SimplePoJo#first", "java.lang.Integer");
    }

    public static void main(String[] args) {
        System.out.println(FIELD_TYPE_MAP);
    }
}

示例代码地址

完整的示例代码请看 GitHub: annotation-processor

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容