转载

Java基础复习-注解篇

注解(也被称为 元数据 ) 为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据. —— 《Java编程思想》

说到注解可能有人会一脸懵逼,我知道注释,注解又是个什么东西呢?先举个例子,相信下面出现的符号大部分Java程序员都会知道:

  • @ Override //用于标明此方法覆盖了父类的方法
  • @ Deprecated //用于标明已经过时的方法或类
  • @ SuppressWarnings //关闭不当的编译器警告
  • @ Controller @ Service @ Repository //Spring 常用的注解
  • @ Test //Junit 单元测试
  • @ Bind //ButterKnife 绑定控件

注解的应用很广泛,可以使我们能够使用编译器来测试和验证格式,存储有关程序的额外信息。可以用来生成描述符文件,甚至或是新的类定义,并且有助于减轻编写“样板”代码的负担。通过使用注解,我们可以将这些元数据保存在Java源代码中,并利用annotation API为自己的注解构造处理工具。

基本语法

  • 自定义注解

定义一个注解

新建一个 HelloAnnotation.java 文件,然后输入下面的代码,恭喜你一个注解就定义完成了。

import java.lang.annotation.*;
(RetentionPolicy.SOURCE)(ElementType.METHOD)
public HelloAnnotation {
}

注解的定义与接口差不多,关键字是<code> @ interface< /code>记住要加一个<code> @ < /code>,<code> @ Documented< /code>,<code> @ Retention< /code>,<code> @ Target< /code> 是元注解,下面会详细解释什么是元注解。

  • 注解元素

上面的注解像一个空的接口,里面没有内容,我们一般称这样的注解为 标记注解 。我们也可以像普通类添加成员变量一样,给注解添加 注解元素 。 例如,我们要给上面定义的HelloAnnotation添加一个int类型的id元素,和一个String类型的description元素。代码如下:

(RetentionPolicy.SOURCE)(ElementType.METHOD)
public HelloAnnotation {

    int id();
    String description() default "hello world";

}

id和description类似方法的定义,但它们不是方法。元素后面可以加一个<code>default</code>关键字,给这个元素一个默认值。上面例子中,如果在使用注解时没有给description赋值,那么description的值就是“hello world”。

关于default有两个限制需要注意:

  1. 如果一个元素没有默认值,那么在使用的时候必须给这个元素赋值
  2. 对于非基本类型的元素,无论在生命时还是使用时,它的值都不能为null

元素类型必须是以下几种:

  1. 所有基本类型(int, float, boolean等)
  2. String
  3. Class
  4. enum
  5. Annotation
  6. 以上几种类型的数组

有一个特殊的元素叫 value , 不论value的类型是什么,当一个注解里面只有这个叫value的元素需要赋值时(只有一个value元素,或者有其他元素,但是其他的元素都有默认值),可以使用快捷方式。具体怎么使用下面会介绍。

  • 注解的使用

我们在上面定义了一个HelloAnnotation,那么我们如何使用呢?

//第一种使用方式,给每个元素都赋值(id=11,description="如何使用注解")
private void test1(){
}

//第二种使用方式,给没有Default值的元素赋值(id=11)
private void test1(){
}

上面的代码展示了注解的使用方式。我们上面提到的特殊的元素value又是什么呢?我们来重新定义一个注解 ValueAnnotation,这注解有两个元素value 和 id,

(RetentionPolicy.RUNTIME)
public ValueAnnotation {
    int id() default 1;
    int value();
}

看起来跟HelloAnnotation没有什么区别,但是使用的时候会有一点点的骚操作...

//与上面说的使用方式没有不同(value = 1)
private void test1(){
}(value = 1, id = 2)
private void test1(){
}

//可以省略value字段的写法,直接把值放进去就好,不需要指定给value赋值(1)
private void test1(){
}

标准注解&元注解

Java提供了三种标准注解:

  1. @ Override ,表示当前方法是超类的重写,如果方法签名与超类的不一致,编译器会报错,避免拼写错误等失误。
  2. @ Deprecated ,表示当前方法被弃用了,不建议使用。部分IDE会在方法上加中划线,表示弃用, add()
  3. @ SuppressWarnings ,表示关闭某些警告信息,比如List list = new ArrayList(),没有加泛型,会报一个unchecked警告,加上@SuppressWarnings("unchecked")之后,就不会再提示。

同时,Java内置了几种元注解。 元注解 是负责注解其他的注解的注解(怎么断句.....):

|关键字|说明|

|--|--|

|<code> @ Target< /code>|表示该注解用于什么地方,可能的ElementType值:<br/> TYPE : 用于类,接口,注解,enum的声明<br/> FIELD : 用于域声明,包括enum实例<br/> METHOD : 用于修饰方法<br/> PARAMETER : 用于修饰参数<br/> CONSTRUCTOR : 用于修饰构造函数<br/> LOCAL_VARIABLE : 用于修饰局部变量<br/> ANNOTATION_TYPE : 用于修饰注解类型<br/> PACKAGE : 用于修饰包<br/>如果不指定Target,可以用到任何位置|

|<code> @ Retention< /code>|表示在什么级别保存该注解,可能的RetentionPolicy值:<br/> SOURCE : 源代码级别,也就是编译时注解被丢弃。java文件编译成class文件后,注解就拿不到了。<br/> CLASS : class级别,也就是在class文件中可用,但是会被VM丢弃。class文件里面保留注解信息,运行时拿不到<br/> RUNTIME : 运行时级别,也就是在代码运行时也不会被丢弃,因此可以通过反射机制读取注解的信息。<br/>如果不指定Retention,默认是class级别|

|<code> @ Documented< /code>|使用javadoc生成文档时,默认是不包含注解文件中的doc内容的。<br/>加上这个注解之后,javadoc生成时会包含这个注解里面的javadoc内容|

|<code> @ Inherited< /code>|允许子类继承父类的注解。两个类Parent 和 Child,Child继承自Parent, <br/>Parent被一个有@Inherited注解的注解A声明,那么Child类也会有A这个注解|

如何获取注解

上面我们说了怎么定义一个注解,也自定义了两个注解。那么我们如何获取注解和里面的内容呢?

我们先来一种简单的方式:

首先我们修改一下<code>HelloAnnotation</code>,把它的Target变成<code>TYPE</code>,让他可以用在类上面。

(RetentionPolicy.SOURCE)(ElementType.TYPE)
public HelloAnnotation {

    int id();
    String description() default "hello world";

}

接下啦我们编写一个测试类 <code>TestAnnotation</code> :

public class TestAnnotation {

   (12)
    private void test() {

    }

    public static void main(String[] args) {

        //判断TestAnnotation类有没有HelloAnnotation这个注解
        boolean hasValueAnnotation = TestAnnotation.class.isAnnotationPresent(HelloAnnotation.class);
        System.out.println(hasValueAnnotation);
        try {
            //获取test方法中的注解
            Method testMethod = TestAnnotation.class.getDeclaredMethod("test");
            System.out.println(testMethod.isAnnotationPresent(ValueAnnotation.class));
            ValueAnnotation valueAnnotation = testMethod.getAnnotation(ValueAnnotation.class);
            System.out.println("id = " + valueAnnotation.id() + ", value = " + valueAnnotation.value());
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }

}
==================输出结果=====================
false
true
id = 1, value = 12

因为HelloAnnotation定义的Retention是SOURCE,所有我们在运行时这个注解已经被弃用了,<code>TestAnnotation.class.isAnnotationPresent(HelloAnnotation.class)</code> 返回的是false。

ValueAnnotation定义的Retention是RUNTIME,所以<code>test</code>的注解和里面的id,value值我们都可以拿到。

APT处理注解

APT(Annotation Process Tool) 注解处理工具,这是Sun为了帮助注解的处理而提供的工具,包含在javac中,可以在代码编译期解析注解,并且生成新的 Java 文件。

我们不需要关心它是如何工作的,我们只需要编写一个 注解处理器 ,然后编译的时候APT会调用我们的注解处理器,达到我们想要的效果。

如何编写一个注解处理器呢?首先我们要了解一个类叫做<code>AbstractProcessor</code>,很明显这个类是一个Abstract的类,它实现了一个叫<code>Processor</code>的接口,里面有很多方法,我们不做详细的介绍,感兴趣的读者可以找一些资料深入了解一下。我们只需要关心其中几个方法就行。下面我们来自定义一个注解处理器,并重写我们关心的方法:

public class HelloProcessor extends AbstractProcessor {
    //一个很重要的参数ProcessingEnvironment,我们可以拿到一些环境相关的值,和一些有用的工具类
   
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
    }

    //真正的处理过程
   
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }

    //表示这个注解处理器能处理哪些注解
   
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(ValueAnnotation.class.getCanonicalName());
    }

    //支持的版本,建议写成下面的代码形式
   
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}

如何让这个注解处理器生效,有多种方式,请大家自行百度。这里限于文章篇幅以Android工程为例,搭建一个简单的demo。 首先,新建一个普通的gradle Android工程,然后新建一个Java Library: annotation。打开annotation这个module的<code>build.gradle</code>文件,添加依赖<code>compile 'com.google.auto.service:auto-service:1.0-rc2'</code>:

//============ annotation/build.gradle =============
apply plugin: 'java'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    //添加auto-service是为了使用@AutoService注解,把注解处理器添加到jvm
    compile 'com.google.auto.service:auto-service:1.0-rc2'
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"

然后把我们上面写的<code>ValueAnnotation</code> 和<code>HelloProcessor</code>放到annotation工程里面,最后的目录结构如下:

Java基础复习-注解篇

annotation工程的配置,暂时就完成了。接着我们回到app工程里面,打开<code>build.gradle</code>,在dependencies里面添加下面两行:

//========app/build.gradle=======
compile project(":annotation")
//表示我们的工程里面有注解处理器,需要apt处理
annotationProcessor  project(":annotation")

打开<code>MainActivity</code>, 添加一个方法<code>test()</code>,并给这个方法一个注解 :

public class MainActivity extends AppCompatActivity {

   
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
   (12)
    private void test(){

    }

到这里简单的demo环境就打完成了。接着我们介绍一下怎么用注解处理器生成代码。主要逻辑是在<code>HelloProcessor</code>里面:

//@AutoService(Processor.class) 这个注解告诉jvm这是一个注解处理器(Processor.class)
public class HelloProcessor extends AbstractProcessor {
    Messager messager ;
    Filer filer;
   
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        //打印log的工具类
        messager = processingEnv.getMessager();
        //最重要的写生成文件的工具
        filer = processingEnv.getFiler();
    }

   
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        try {
            //获取所有的使用ValueAnnotation注释的Element
            Set<? extends Element> genElements = roundEnv.getElementsAnnotatedWith(ValueAnnotation.class);
            for (Element e : genElements) {
                //获取Annotation的值
                ValueAnnotation valueAnnotation = e.getAnnotation(ValueAnnotation.class);
                int id = valueAnnotation.id();
                int value = valueAnnotation.value();
                //e.getSimpleName() 获得当前作用对象的名称,这里就是test方法
                //e.getEnclosingElement() 获取当前作用对象的上一级对象,也就是MainActivity
                String className = e.getEnclosingElement().getSimpleName().toString();
                messager.printMessage(Diagnostic.Kind.NOTE, "class = " + className + ", method = " + e.getSimpleName());

                //我们生产一个叫MainActivity$$Value的源文件,里面有个方法叫printValue,作用是打印注解的值。
                JavaFileObject jfo = filer.createSourceFile("com.jiang." + className + "$$Value", e);
                Writer writer = jfo.openWriter();
                writer.flush();
                writer.append("package com.jiang;/n" +
                        "/n" +
                        "/n" +
                        "public class " + className + "$$Value {/n" +
                        "    public void printValue(){/n" +
                        "        System.out.println(/"id= " + id + " , value = " + value + "/");/n" +
                        "    };/n" +
                        "}/n");
                writer.flush();
                writer.close();
            }

        } catch (Exception e) {
            messager.printMessage(Diagnostic.Kind.ERROR, e.getMessage());
        }
        return false;
    }

   
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(ValueAnnotation.class.getCanonicalName());
    }

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

上面的一切代码都完成了,Make project,编译一下整个工程,如果一切正常会在gradle的Console里面输出我们打印的log

Java基础复习-注解篇

这时在app工程的build目录下会生成我们想要的java文件

Java基础复习-注解篇

文件的代码就是我们在注解处理器里面添加的:

package com.jiang;


public class MainActivity$$Value {
    public void printValue(){
        System.out.println("id= 1 , value = 12");
    };
}

最后我们可以在<code>MainActivity</code>里面调用这个类:

public class MainActivity extends AppCompatActivity {

   
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new MainActivity$$Value().printValue();
    }
   (12)
    private void test(){

    }
}

运行项目,logcat里面会输出我们的打印结果:

Java基础复习-注解篇

Android开发中经常用到的ButterKnife就是使用了上面的实现方式,根据ResourceId生成java源文件,帮你做findViewById、setOnClickListener这些繁琐的事情。

另一个很火的网络框架Retrofit使用的是注解来定义HttpMethod,它是通过在Builder里面直接调用getAnnotations()这种方式来获取注解信息。

原文  https://xiaozhuanlan.com/topic/5983670241
正文到此结束
Loading...