Java注解与自己定义注解处理器

摘要:
在编译期间,JVM会自己主动执行注解处理器(当然。尽管我们写的Java代码被编译成class就不能被修改了,可是注解处理器会又一次生成其它的java代码。我们能够通过反射来调用新生成的java文件中面的类或方法。JavaAPI为我们提供了注解处理器的接口。这个样例我是从Java注解处理器,參考过来的。extendsTypeElement˃annoations,RoundEnvironmentenv){}@OverridepublicSetgetSupportedAnnotationTypes(){}@OverridepublicSourceVersiongetSupportedSourceVersion(){}}定义自己的注解处理器init对一些工具进行初始化。getSupportedAnnotationTypes表示该注解处理器能够出来那些注解。

动机

近期在看ButterKnife源代码的时候。竟然发现有一个类叫做AbstractProcessor,并且ButterKnife的View绑定不是依靠反射来实现的,而是使用了编译时的注解,自己主动生成的.class文件。

真是一个奇妙的东西啊!

所以本文就注解与自己定义的注解处理器来学习注解。项目Github地址

基础知识

大家应该知道元注解@Retention吧,它表示注解在什么时候存在,能够分为3个时期:

  • RetentionPolicy.SOURCE:表示注解仅仅在java文件中面才有,在编译的时候。这部分注解就会被擦出。相似于@Override仅仅是给程序猿看的。
  • RetentionPolicy.CLASS:注解在编译的时候会存在。在执行的时候就会擦除。
  • RetentionPolicy.RUNTIME:在执行的时候会存在。这里就要用到了反射来实现了。

Annotation Processor

注解处理器。在编译期间,JVM会自己主动执行注解处理器(当然。我们须要将其注冊)。尽管我们写的Java代码被编译成class就不能被修改了,可是注解处理器会又一次生成其它的java代码。我们能够通过反射来调用新生成的java文件中面的类或方法。然后JVM再对这些生成的java代码进行编译。

这是一个递归的过程。

Java API为我们提供了注解处理器的接口。我们仅仅要实现它就能够了。这个类就是AbstractProcessor,以下就一起来实现它。这个样例我是从【Java二十周年】Java注解处理器,參考过来的。

实现功能例如以下:对于一个类,仅仅要给它的某个加上一个注解,就会自己主动生成其接口。接口中的方法就是加注解的方法。

public class Man {

    @Interface("ManInterface")
    public void eat() {
        System.out.println("Eat");
    }
}

eat方法加上了@Interface("ManInterface"),上述就会生成:

public interface ManInterface {
  void eat();
}

就是这么粗暴。

然后我们看看注解是怎么定义的:

package wqh.core;

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

/**
 * Created on 2016/8/10.
 *
 *@author 王启航
 *@version 1.0
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Interface {
    String value();
}

注意上面@Target(ElementType.METHOD)表示该方法仅仅能加在方法上面;@Retention(RetentionPolicy.CLASS)表示是在编译时存在的。

好了接下来就是重点了:

public class InterfaceProcessor extends AbstractProcessor {

    @Override
    public synchronized void init(ProcessingEnvironment env){ }

    @Override
    public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }

    @Override
    public Set<String> getSupportedAnnotationTypes() { }

    @Override
    public SourceVersion getSupportedSourceVersion() { }

}

定义自己的注解处理器

  • init对一些工具进行初始化。
  • process就是真正生成java代码的地方。
  • getSupportedAnnotationTypes表示该注解处理器能够出来那些注解。
  • getSupportedSourceVersion能够出来java版本号

我们首先看看除去process的其它三个方法:

public class InterfaceProcessor extends AbstractProcessor {

    private Types typeUtils;
    private Elements elementUtils;
    private Filer filer;
    private Messager messager;


    @Override
    public synchronized void init(ProcessingEnvironment env) {
        super.init(env);
        elementUtils = env.getElementUtils();
        filer = env.getFiler();
        typeUtils = env.getTypeUtils();
        messager = env.getMessager();
    }

    @Override
    public boolean process(Set<?

extends TypeElement> annotations, RoundEnvironment env) { } @Override public Set<String> getSupportedAnnotationTypes() { Set<String> types = new TreeSet<>(); types.add(Interface.class.getCanonicalName()); return types; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); }

getSupportedSourceVersion返回近期的版本号。
getSupportedAnnotationTypes返回了我们定义的Interface注解。
init初始化了了4个工具类。以下略微介绍一哈:

Elements:Element代表程序的元素,比如包、类或者方法。每一个Element代表一个静态的、语言级别的构件。它仅仅是结构化的文本。他不是可执行的.能够理解为一个签名
(public class Main 或者 private void fun(int a) {}),大家能够联系到XML的解析,或者抽象语法树 (AST)。
给出以下样例:

package com.example;    // PackageElement

public class Foo {        // TypeElement

    private int a;      // VariableElement
    private Foo other;  // VariableElement

    public Foo () {}    // ExecuteableElement

    public void setA (  // ExecuteableElement
                     int newA   // TypeElement
                     ) {}
}

并且我们能够通过

aType.getEnclosingElement();

得到其父标签(没有父标签就返回null).能够联系Class.getEnclosingClass这种方法。

上面样例大家都懂了吧!!

本文主要就是对ExecuteableElement进行操作。

以下看看Messager
由于在注解处理器里面不能够抛出Exception!为什么了?由于在编译阶段抛出错误后,注解处理器就不会执行完。也就没什么用了。
所以Message就是为了输出错误信息。

关于FilterTypes本文就不进行解说了。。。。

好!!以下看看process方法

@Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        Map<String, AnnotatedClass> classMap = new HashMap<>();

        // 得到全部注解@Interface的Element集合
        Set<? extends Element> elementSet = env.getElementsAnnotatedWith(Interface.class);

        for (Element e : elementSet) {
            if (e.getKind() != ElementKind.METHOD) {
                error(e, "错误的注解类型。仅仅有函数能够被该 @%s 注解处理", Interface.class.getSimpleName());
                return true;
            }

            ExecutableElement element = (ExecutableElement) e;
            AnnotatedMethod annotatedMethod = new AnnotatedMethod(element);

            String classname = annotatedMethod.getSimpleClassName();
            AnnotatedClass annotatedClass = classMap.get(classname);
            if (annotatedClass == null) {
                PackageElement pkg = elementUtils.getPackageOf(element);
                annotatedClass = new AnnotatedClass(pkg.getQualifiedName().toString(), element.getAnnotation(Interface.class).value());
                annotatedClass.addMethod(annotatedMethod);
                classMap.put(classname, annotatedClass);
            } else
                annotatedClass.addMethod(annotatedMethod);

        }
        // 代码生成
        for (AnnotatedClass annotatedClass : classMap.values()) {
            annotatedClass.generateCode(elementUtils, filer);
        }
        return false;
    }

首先看看第一部分:

 Map<String, AnnotatedClass> classMap = new HashMap<>();

这里就是存储整个project凝视过getSupportedAnnotationTypes返回的注解的Map
在这里我们定义了例如以下的数据结构:

classMap:
key -> 一个类的类名
value -> AnnotatedClass

// 这个等下会说到,这里知道是这种关系就好
AnnotatedClass 
key -> 一个类的类名
value -> 该类全部加上注解的方法。

第二部分:

// 得到全部注解@InterfaceElement集合
        Set<? extends Element> elementSet = env.getElementsAnnotatedWith(Interface.class);

如凝视所说,得到全部注解@InterfaceElement集合。

注意这里得到了是Element的集合,也就生成了一个树结构。

第三部分:

for (Element e : elementSet) {
            if (e.getKind() != ElementKind.METHOD) {
                error(e, "错误的注解类型,仅仅有函数能够被该 @%s 注解处理", Interface.class.getSimpleName());
                return true;
            }

            ExecutableElement element = (ExecutableElement) e;
            AnnotatedMethod annotatedMethod = new AnnotatedMethod(element);

            String classname = annotatedMethod.getSimpleClassName();
            AnnotatedClass annotatedClass = classMap.get(classname);
            if (annotatedClass == null) {
                PackageElement pkg = elementUtils.getPackageOf(element);
                annotatedClass = new AnnotatedClass(pkg.getQualifiedName().toString(), element.getAnnotation(Interface.class).value());
                annotatedClass.addMethod(annotatedMethod);
                classMap.put(classname, annotatedClass);
            } else
                annotatedClass.addMethod(annotatedMethod);

        }

首先全部加上@InterfaceElement进行变量,第一步推断注解是否加在Method之上,假设不是就输出错误信息。
上文说到能够用Messager来输出:

private void error(Element e, String msg, Object... args) {
        messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args), e);
    }

然后

ExecutableElement element =(ExecutableElement) e;
 AnnotatedMethod annotatedMethod = new AnnotatedMethod(element);

将其转型为ExecutableElement。由于在上面推断全部的Element都是加在方法上的。这里不会报错。
然后构造AnnotatedMethod,由于面向对象,AnnotatedMethod表示一个加了注解的方法。

在这里我们看看什么是AnnotatedMethod:

package wqh.core;

import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;

/**
 * Created on 2016/8/10.
 *
 *@author 王启航
 *@version 1.0
 */
public class AnnotatedMethod {
    private ExecutableElement annotatedMethodElement;
    private String simpleMethodName;
    private String simpleClassName;

    public AnnotatedMethod(ExecutableElement annotatedMethodElement) {
        this.annotatedMethodElement = annotatedMethodElement;
        simpleMethodName = annotatedMethodElement.getSimpleName().toString();
        /*
         * getEnclosingElement() 能够理解为该标签的父标签.
         * {@see Class.getEnclosingClass}
         */
        TypeElement parent = (TypeElement) annotatedMethodElement.getEnclosingElement();
        /*
         * Return the fully qualified name of this class or interface.
         * {@code java.util.Set<E>} is "java.util.Set"
         * {@code java.util.Map.Entry}is "java.util.Map.Entry"
         */
        simpleClassName = parent.getQualifiedName().toString();
    }

    public ExecutableElement getAnnotatedMethodElement() {
        return annotatedMethodElement;
    }

    public String getSimpleMethodName() {
        return simpleMethodName;
    }

    public String getSimpleClassName() {
        return simpleClassName;
    }
}

还是比較简单的哈!!

OK,回到 process

String classname = annotatedMethod.getSimpleClassName();
AnnotatedClass annotatedClass = classMap.get(classname);

构造AnnotatedClass。上面也说到过AnnotatedClass,AnnotatedClass能够表示一个类。里面存储了很多的AnnotatedMethod
以下看看其代码:

package wqh.core;


import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;

import javax.annotation.processing.Filer;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.VariableElement;
import javax.lang.model.util.Elements;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

/**
 * Created on 2016/8/10.
 *
 * @author 王启航
 * @version 1.0
 */
public class AnnotatedClass {
    private String className;
    private String packageName;
    private List<AnnotatedMethod> methods = new LinkedList<>();

    /**
     * @param packageName       将要生成的类的包名
     * @param generateClassName 将要生成的类的类名
     */
    public AnnotatedClass(String packageName, String generateClassName) {
        this.className = generateClassName;
        this.packageName = packageName;
    }

   // 生成Java代码部分。如今能够不看
    public void generateCode(Elements elementUtils, Filer filer) {
        TypeSpec.Builder typeBuilder = TypeSpec.interfaceBuilder(className).addModifiers(Modifier.PUBLIC);

        for (AnnotatedMethod m : methods) {
            MethodSpec.Builder methodBuilder =
                    MethodSpec.methodBuilder(m.getSimpleMethodName())
                            .addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
                            .returns(TypeName.get(m.getAnnotatedMethodElement().getReturnType()));

            int i = 1;
            for (VariableElement e : m.getAnnotatedMethodElement().getParameters()) {
                methodBuilder.addParameter(TypeName.get(e.asType()), "param" + String.valueOf(i));
                ++i;
            }
            typeBuilder.addMethod(methodBuilder.build());
        }
        JavaFile javaFile = JavaFile.builder(packageName, typeBuilder.build()).build();

        try {
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void addMethod(AnnotatedMethod annotatedMethod) {
        methods.add(annotatedMethod);
    }
}

OK。在process里面:

 if (annotatedClass == null) {
                PackageElement pkg = elementUtils.getPackageOf(element);
                annotatedClass = new AnnotatedClass(pkg.getQualifiedName().toString(), element.getAnnotation(Interface.class).value());
                annotatedClass.addMethod(annotatedMethod);
                classMap.put(classname, annotatedClass);
            } else
                annotatedClass.addMethod(annotatedMethod);

就是将加了注解的方法,在同一个类的增加到annotatedClass里面去,然后构成classMap

到这里。全部的结构都已生成。也就是:

classMap:
key -> 一个类的类名
value -> AnnotatedClass

// 这个等下会说到。这里知道是这种关系就好
AnnotatedClass 
key -> 一个类的类名
value -> 该类全部加上注解的方法。

下一步就是生成Java代码了。这里用了JavaWriter的改进版本号JavaPoet,能够自己去下这个包

// 代码生成
        for (AnnotatedClass annotatedClass : classMap.values()) {
            annotatedClass.generateCode(elementUtils, filer);
        }
        return false;

这部分不做具体解说。

好了,全部的InterfaceProcessor解说完成。

注冊打包

接下来将其注冊到JVM中去:在Intelilj IDEA,有例如以下的文件夹结构:
Dir

javax.annotation.processing.Processor中定义了注解处理器:

wqh.core.InterfaceProcessor

这些都做了之后。就回去生成jar包!!

!能够用Maven,可是我们这个项目加了依赖项,所以有点麻烦。
这里直接用Intelilj IDEA打包!
File->Project Structure能够配置这样

将右側的文件夹加到左側就好。
Jar

Build一哈,就能够,能够生成jar包了。!

这些步骤。我们也能够直接使用Google的AutoService,直接省去了这一步。

測试

在其它项目中依照以下配置:
Config
Config2

package test;

import wqh.core.Interface;

/**
 * Created on 2016/8/10.
 *
 *@author 王启航
 *@version 1.0
 */
public class Man {

    @Interface("ManInterface")
    public void eat() {
        System.out.println("Eat");
    }
}

执行就能够在generated文件夹看到生成的接口

package test;

public interface ManInterface {
  void eat();
}

那么怎么使用MainInterface呢?

package test;

/**
 * Created on 2016/8/10.
 *
 *@author 王启航
 *@version 1.0
 */
public class Main {

    public static void main(String args[]) throws ClassNotFoundException {
        Class<?

> c = Class.forName("test.ManInterface"); System.out.println(c.getName()); } }

尽管这个功能没什么用啊!

。。!

參考

Java二十周年 Java注解处理器
Java注解处理器

我的邮箱:1906362072@qq.com
大二学生,欢迎联系。

免责声明:文章转载自《Java注解与自己定义注解处理器》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Spring Boot GraphQL 实战 03_分页、全局异常处理和异步加载WPF如何设置启动窗口下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

java 实现基于opencv全景图合成

因项目需要,自己做了demo,从中学习很多,所以分享出来,希望有这方面需求的少走一些弯路,opencv怎么安装网上教程多多,这里不加详细说明,我安装的opencv-3.3.0  如上图所示,找到相应的jar包,这里讲一下如何这个jar如何导入Maven仓库 mvn install:install-file -Dfile=D:opencv-3.0.0ope...

Unity多语言本地化改进版

简介 之前捣鼓过一个通过csv配置游戏多语言支持的小工具,但是发现使用过程中,通过notepad++去进行转码很不方便,并且直接将配置的csv不加密的放在游戏中心里感觉不是很踏实 于是乎~~ 新的方案 1.在PC/MAC平台上解析多语言配置,也就是editor运行环境中解析csv或者excel 2.通过在Editor运行过程中生成多个语言对象,然后序列化并...

将Flash的Sprite导入Unity(解释说明版)

usingUnityEngine; usingUnityEditor; usingSystem.IO; usingSystem.Xml; usingSystem.Reflection; public classAutoSliceSpriteSheetWithXML : AssetPostprocessor { private voidOnPr...

设计模式--观察者模式初探和java Observable模式

初步认识观察者模式  观察者模式又称为发布/订阅(Publish/Subscribe)模式,因此我们可以用报纸期刊的订阅来形象的说明:     报社方负责出版报纸.     你订阅了该报社的报纸,那么只要报社发布了新报纸,就会通知你,或发到你手上.     如果你不想再读报纸,可以取消订阅,这样,报社发布了新报纸就不会再通知你.   理解其实以上的概念,就...

okio:定义简短高效

  欢迎关注公众号,第一时间获取最新文章:   本篇目录 一、前言 okio是大名鼎鼎的square公司开发出来的,其是okhttp的底层io操作库,既然已经有java原生的io库为什么还要自己费尽开发一套呢?java原生的io操作存在很多问题,比如读写阻塞,内存管理并不高效,体系臃肿,api调用不精简,以上我个人认为okio改进最大的地方是内存管理方...

HBase海量数据存储

HBaseHBase是一个基于HDFS的非关系型数据库(海量数据存储) HBase的特点 1.海量数据存储,HBase中的表可以容纳上百亿行x上百万列的数据。 2.列式存储,HBase中的数据是基于列进行存储的,能够动态的增加和删除列。 3.准实时查询,HBase在海量的数据量下能够接近准实时的查询(百毫秒以内) 4.多版本,HBase中每一列的数据都有多...