java注解(Annotation)总结

吐糟时间

想写博客也不是一天两天了,苦于各种原因,从来都没有写过,虽然平常印象笔记里记了一堆东西,但始终没有一个很好的总结,归根到底还是一个字,懒吧 ! 哎,真是怠惰啊~

在这里插入图片描述

作为不接触后台的android程序猿,前几天用了SpringBoot和各种组件后,发现这怎么这么多注解啊,喵喵喵???(不过话说回来,SpringBoot用起来真是爽啊,比ssh好用多了)
在这里插入图片描述
所以就回头看看注解的东西,顺便写上这篇博客~

注解是什么

@Override 这就是最常用的注解了~

 	@Override
	public String toString() {
 	}

注解有什么用

  • 标记,可以为编译器提供一些信息,以便于检测错误,抑制警告等(@Override、@SuppressWarnings)
  • 编译时动态处理,如动态生成代码(android中的Retrofit、DataBinding、Dagger2、ButterKnife、EventBus3)
  • 运行时动态处理,如得到注解信息(SpringBoot中的@AutoWired)

这里的三个作用实际对应着后面自定义 Annotation 时说的 @Retention 三种值分别表示的 Annotation

注解的分类

  • 标准注解:Override, Deprecated, SuppressWarnings
    指 Java 自带的几个 Annotation,上面三个分别表示重写函数,不鼓励使用(有更好方式、使用有风险或已不在维护),忽略某项 Warning
  • 元注解:@Retention, @Target, @Inherited, @Documented
    元注解是定义注解的注解,在自定义注解的时候会用到
  • 自定义注解
    根据自己的需要,使用上面的元注解自定义注解

怎么自定义注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
  1. 取一个你喜欢的名字,并使用@interface 声明

  2. 使用@Target 注解声明它会被使用的地方(类、属性、方法等)

     取值(ElementType)有 
     1.CONSTRUCTOR:构造器
     2.FIELD:成员变量
     3.LOCAL_VARIABLE:局部变量
     4.METHOD:方法
     5.PACKAGE:包
     6.PARAMETER:参数
     7.TYPE:类、接口(包括注解类型) 或enum声明   
     注意,当注解未指定Target值时,则此注解可以用于任何元素之上,多个值使用{}包含并用逗号隔开,如下:
     @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) 
    
  3. 使用@Retention 注解声明生命周期,即被描述的注解在什么范围内有效

     1.SOURCE:在源文件中有效(即源文件保留,注解仅存在代码中,注解会被编译器丢弃)
     2.CLASS:在class文件中有效(对应编译时注解,注解会在class文件中保留,但会被VM丢弃,在运行时期,这类注解是没有的)
     3.RUNTIME:在运行时有效(对应运行时注解,VM运行期间也会保留该注解,因此可以通过反射来获得该注解)
     注意,当注解未定义Retention值时,默认值是CLASS,如Java内置注解,@Override、@Deprecated、@SuppressWarnning等
    

除了这两种元注解外,还有 @Inherited 和 @Documented。

  • @Inherited 可以让注解被继承,但这并不是真的继承,只是通过使用@Inherited,可以让子类Class对象使用getAnnotations()获取父类被@Inherited修饰的注解。
  • @Documented 被修饰的注解会生成到javadoc中。

注意: 所有的Annotation会自动继承java.lang.Annotation这一接口,并且不能再去继承别的类或是接口.

添加属性

自定义注解还会定义一些属性,在解析注解的时候使用到

@Target(ElementType.FIELD)//指定该注解使用的范围是成员变量
@Retention(RetentionPolicy.RUNTIME)
public @interface Player {

    String name();

    String hero() default "";

    int damage() default 0;

    Constraints connstranints() default @Constraints;
}

注解的属性可支持数据类型:

  • 所有基本数据类型(int,float,boolean,byte,double,char,long,short)

  • String类型

  • Class类型

  • enum类型

  • Annotation类型

  • 以上所有类型的数组

      	注意:
      	1. 属性只能用public或默认(default)这两个访问权修饰。例如,String value();这里把方法设为defaul默认类型。
      	2. 如果一个注解的属性只有一个,且属性名是value(),那么使用注解的时候不需要写属性名字,直接赋值。如下面的 @Team注解。
    

注解的使用

@Target(ElementType.TYPE)//指定该注解使用的范围是类或者接口
@Retention(RetentionPolicy.RUNTIME)
public @interface Team {
    String value() default "";
}

@Team("IG")
public class IG {

    @Player(name = "the Shy", hero = "剑魔", damage = 28000, connstranints = @Constraints(isMvp = true))
    private String top;
}   

注意:

  1. 使用注解的时候,注解的每个属性都要赋值,有默认值的属性可以不赋。
  2. 自定义注解后,需要编写对应的解析类。
  3. 解析运行时注解和编译时注解的方法是不一样的。
  4. 解析运行时(RUNTIME)注解,必须通过Java的反射技术来获取 Annotation对象。
  5. 解析编译时(CLASS)注解,需要自定义一个Processor继承注解处理器 ( Annotation Processor )。

解析运行时(RUNTIME)注解

上面说了,我们写的注解都是自动继承Annotation接口的,要获取类方法,字段的注解信息,必须通过反射。在java.lang.reflect 反射包下有一个AnnotatedElement接口,通过这个接口提供的方法可以利用反射获取注解信息,反射包中的Constructor类、Field类、Method类、Package类和Class类都实现了AnnotatedElement接口。
下面是AnnotatedElement中相关的API方法,以上5个类都实现以下的方法

返回值 方法名称 说明
<A extends Annotation> getAnnotation(Class<A> annotationClass) 该元素如果存在指定类型的注解,则返回这些注解,否则返回 null。
Annotation[] getAnnotations() 返回此元素上存在的所有注解,包括从父类继承的
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) 如果指定类型的注解存在于此元素上,则返回 true,否则返回 false。
Annotation[] getDeclaredAnnotations() 返回直接存在于此元素上的所有注解,注意,不包括父类的注解,调用者可以随意修改返回的数组;这不会对其他调用者返回的数组产生任何影响,没有则返回长度为0的数组
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Player {

   String name() default "";

   Constraints connstranints() default @Constraints;
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Team {
   String value() default "";
}

@Target(ElementType.FIELD) 
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {

   boolean isMvp() default false;

   boolean isDirector() default false;

}

@Team("IG")
public class IG {

   @Player(name = "the Shy")
   private String top;

   @Player(name = "ning", connstranints = @Constraints(isMvp = true))
   private String jungle;

   @Player(name = "rookie")
   private String mid;

   @Player(name = "baolan")
   private String support;

   @Player(name = "jackeylove", connstranints = @Constraints(isDirector = true))
   private String botLane;

   @Player(name = "duke")
   private String substitute;

}

public class Parse {
   public static void main(String[] args) throws ClassNotFoundException {

       Class<?> cl = Class.forName("com.test.lol.IG");
       String team = cl.getAnnotation(Team.class).value();
       StringBuilder teamPlayer = new StringBuilder();
       for (int i = 0; i < cl.getDeclaredFields().length; i++) {
           Field[] fields = cl.getDeclaredFields();
           String fieldName = fields[i].getName();
           Annotation[] anns = fields[i].getDeclaredAnnotations();
           if (anns[0] instanceof Player) {
               Player player = (Player) anns[0];
               String name = player.name();
               Constraints connstranints = player.connstranints();
               boolean director = connstranints.isDirector();
               boolean mvp = connstranints.isMvp();
               teamPlayer.append(fieldName).append(":").append(name);
               if (mvp)
                   teamPlayer.append("(MVP)");
               if (director)
                   teamPlayer.append("(指挥)");
               if (i != cl.getDeclaredFields().length - 1)
                   teamPlayer.append(",").append("\n");
           }
       }

       String content = "2018英雄联盟S8总冠军:" + team + "\n"
               + "冠军成员:" + "\n"
               + teamPlayer;

       System.out.println(content);

   }
   ======================= 输出 =======================
2018英雄联盟S8总冠军:IG
冠军成员:
top:the Shy,
jungle:ning(MVP),
mid:rookie,
support:baolan,
botLane:jackeylove(指挥),
substitute:duke

解析编译时(CLASS)注解

自定义一个继承AbstractProcessor 的 Processor

public class Processor 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()
    在这里可以初始化一些工具类。
    ProcessingEnviroment提供很多有用的工具类Elements, Types和Filer。
    Elements:一个用来处理Element的工具类;
    Types:一个用来处理TypeMirror的工具类;
    Filer:正如这个名字所示,使用Filer你可以创建文件。

  • getSupportedAnnotationTypes()
    这里你必须指定,这个注解处理器是注册给哪个注解的,并且是带包名+类名的全称。

  • getSupportedSourceVersion()
    用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。

  • processor()
    输入参数annotations 请求处理的注解类型集合。
    输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素,相当于“有关全局源码的上下文环境”。
    @return 如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们

在Java 7中,你也可以使用注解来代替getSupportedAnnotationTypes()和getSupportedSourceVersion()

@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.example.lib_annotation.BindView"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class MyButterknifeProcessor extends AbstractProcessor {}

自定义Processor执行过程

上述的四个方法,前三个只会被调用一次,processor()方法可能会被调用多次,当没有输出文件也没有输入文件,处理结束。

用一个例子看一下 processor()调用的次数:
在看例子之前,先介绍几个会出现的演员,比较猴急的可以直接跳过

Element接口

Element在逻辑上代表语言元素,比如包,类,方法等
其实Element是定义的一个接口,定义了外部调用暴露出的接口

方法 解释
TypeMirror asType() 返回此元素定义的类型,实际的java类型(eg:String类型)
ElementKind getKind() 返回此元素的种类:包、类、接口、方法、字段…,如下枚举值,方法返回一个枚举值TypeKind
Set getModifiers() 返回此元素的修饰符,如下枚举值
Name getSimpleName() 返回此元素的简单名称,比如activity名,变量就是变量名
Element getEnclosingElement() 返回封装此元素的最里层元素,即最近的外层元素,如果此元素的声明在词法上直接封装在另一个元素的声明中,则返回那个封装元素; 如果此元素是顶层类型,则返回它的包如果此元素是一个包,则返回 null; 如果此元素是一个泛型参数,则返回 null.
List getEnclosedElements() 获取所有的内层元素
< A extends Annotation> A getAnnotation(Class< A> var1) 返回此元素针对指定类型的注解(如果存在这样的注解),否则返回 null。注解可以是继承的,也可以是直接存在于此元素上的

Element子类

Element在逻辑上代表语言元素,比如包,类,方法等,因此也会有五个直接子接口,它们分别代表一种特定类型的元素

子类 解释
PackageElement 一个包程序元素
TypeElement 一个类或接口程序元素
ExecutableElement 某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注解类型元素
TypeParameterElement 一般类、接口、方法或构造方法元素的泛型参数
VariableElement 一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数

TypeElement详解

TypeElement定义的一个类或接口程序元素,相当于当前注解所在的class对象

方法 解释
NestingKind getNestingKind(); 返回此类型元素的嵌套种类
Name getQualifiedName(); 返回此类型元素的完全限定名称。更准确地说,返回规范 名称。对于没有规范名称的局部类和匿名类,返回一个空名称.譬如 Activity就是包名+类名
TypeMirror getSuperclass(); 返回此类型元素的直接超类。如果此类型元素表示一个接口或者类 java.lang.Object,则返回一个种类为 NONE 的 NoType
List getInterfaces(); 返回直接由此类实现或直接由此接口扩展的接口类型
List getTypeParameters(); 按照声明顺序返回此类型元素的形式类型参数

VariableElement详解

VariableElement标示一个字段、enum 常量、方法或构造方法参数、局部变量或异常参数

方法 解释
getConstantValue 变量初始化的值
getEnclosingElement 获取相关类信息
package apt;
//注解
@Retention(RetentionPolicy.SOURCE) // 注解只在源码中保留
@Target(ElementType.TYPE) // 用于修饰类
public @interface Hello {
    String name() default "";
}
//使用注解
package apt;
@Hello(name = "world")
public class Player {
}
//不使用注解
package apt;
public class Ignored {
}
package apt;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.Set;

@SupportedSourceVersion(SourceVersion.RELEASE_8) // 源码级别, 这里的环境是 jdk 1.8
@SupportedAnnotationTypes("apt.Hello") // 处理的注解类型, 这里需要处理的是 apt 包下的 Hello 注解(这里也可以不用注解, 改成重写父类中对应的两个方法)
public class HelloProcessor extends AbstractProcessor {

    // 计数器, 用于计算 process() 方法运行了几次
    private int count = 1;

    // 用于写文件
    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) {
        System.out.println("start process, count = " + count++);
        // 获得所有类
        Set<? extends Element> rootElements = roundEnv.getRootElements();
        System.out.println("all class:");

        for (Element rootElement : rootElements) {
            System.out.println("  " + rootElement.getSimpleName());
        }

        // 获得有注解的元素, 这里 Hello 只能修饰类, 所以只有类
        Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(Hello.class);
        System.out.println("annotated class:");
        for (Element element : elementsAnnotatedWith) {
            String className = element.getSimpleName().toString();
            System.out.println("  " + className);

            String output = element.getAnnotation(Hello.class).name();
            // 产生的动态类的名字
            String newClassName = className + "_New";
            // 写 java 文件
            createFile(newClassName, output);
        }
        return true;
    }

    private void createFile(String className, String output) {
        StringBuilder cls = new StringBuilder();
        cls.append("package apt;\n\npublic class ")
                .append(className)
                .append(" {\n  public static void main(String[] args) {\n")
                .append("    System.out.println(\"")
                .append(output)
                .append("\");\n  }\n}");
        try {
            JavaFileObject sourceFile = filer.createSourceFile("apt." + className);
            Writer writer = sourceFile.openWriter();
            writer.write(cls.toString());
            writer.flush();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在项目根目录新建一个out/production 目录
在这里插入图片描述

代码逻辑:
1、获得所有标有注解的类
2、取出注解中的信息
3、生成新的 java 文件

在idea的terminal里编译注解处理器
W:\workspace3\testAnnotation>javac -encoding UTF-8 -d out\production\ src\apt\HelloProcessor.java src\apt\Hello.java
接着执行注解处理器
W:\workspace3\testAnnotation>javac -encoding UTF-8 -cp out\production\ -processor apt.HelloProcessor -d out\production -s src\ src\apt\*.java

start process, count = 1
all class:
  Hello
  HelloProcessor
  Ignored
  Player
annotated class:
  Player
start process, count = 2
all class:
  Player_New
annotated class:
start process, count = 3
all class:
annotated class:

可以看到processor()的执行的次数为3次,当没有输入和输出后,不会再次执行。

过程 输入 输出
第一次 Hello.java , HelloProcessor.java ,Ignored.java, Player.java Player_New.java
第二次 Player_New.java -
第三次 - -

注意: 运行注解处理器的时候, 会开一个完整的 java 虚拟机执行代码, 所以自定义的注解处理器是可以使用各种类库的。

简单实现一个ButterKnife

好了,乱78遭的说了一堆,不管你懂没懂,DuangDuangDuang的看了一遍,然后自己动手来一遍就能大致理解了。
在这里插入图片描述
这个up的例子写的很好,也比较简单易懂,这里我就不再那啥了,毕竟贴图,贴代码还是很麻烦的…
从0到1:实现 Android 编译时注解 链接的文章结尾也有github的代码可以参考。

这里我还要提的是,关于调试 abstractProcessor的一点体会:

  • debug 自定义的remote – apt的时候,有可能会碰上连接5005端口失败的错误,这个时候可以先执行assembleDebug,然后在debug apt,就可以连上5005端口了,接着执行assembleDebug,你会发现在abstractProcessor中打的断点就能调试了
    在这里插入图片描述
  • 用javaPoet成功生成文件以后,想再次进入abstractProcessor调试, 需要删除生成的MainActivity$$ViewInjector
  • 调试的时候你会发现,processor()方法也一共被执行了3次,通过查看这两个参数的值,再结合上面也执行了3次的那个例子,想必聪明的你一定能懂。
  • public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) 
    

=============================================================================

参考文献

猜你喜欢

转载自blog.csdn.net/u010717084/article/details/83684207