欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java基础复习-注解篇

程序员文章站 2022-03-14 19:11:20
...

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

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

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

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

基本语法

  • 自定义注解

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

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface HelloAnnotation {
}

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

  • 注解元素

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

@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface HelloAnnotation {

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

}

id和description类似方法的定义,但它们不是方法。元素后面可以加一个default关键字,给这个元素一个默认值。上面例子中,如果在使用注解时没有给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,那么我们如何使用呢?

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

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

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

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

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

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

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

标准注解&元注解

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

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

关键字 说明
@Target 表示该注解用于什么地方,可能的ElementType值:
TYPE: 用于类,接口,注解,enum的声明
FIELD: 用于域声明,包括enum实例
METHOD: 用于修饰方法
PARAMETER: 用于修饰参数
CONSTRUCTOR: 用于修饰构造函数
LOCAL_VARIABLE: 用于修饰局部变量
ANNOTATION_TYPE: 用于修饰注解类型
PACKAGE: 用于修饰包
如果不指定Target,可以用到任何位置
@Retention 表示在什么级别保存该注解,可能的RetentionPolicy值:
SOURCE: 源代码级别,也就是编译时注解被丢弃。java文件编译成class文件后,注解就拿不到了。
CLASS: class级别,也就是在class文件中可用,但是会被VM丢弃。class文件里面保留注解信息,运行时拿不到
RUNTIME: 运行时级别,也就是在代码运行时也不会被丢弃,因此可以通过反射机制读取注解的信息。
如果不指定Retention,默认是class级别
@Documented 使用javadoc生成文档时,默认是不包含注解文件中的doc内容的。
加上这个注解之后,javadoc生成时会包含这个注解里面的javadoc内容
@Inherited 允许子类继承父类的注解。两个类Parent 和 Child,Child继承自Parent,
Parent被一个有@Inherited注解的注解A声明,那么Child类也会有A这个注解

如何获取注解

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

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

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

@Documented
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface HelloAnnotation {

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

}

接下啦我们编写一个测试类 TestAnnotation :

@HelloAnnotation
public class TestAnnotation {

    @ValueAnnotation(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,所有我们在运行时这个注解已经被弃用了,TestAnnotation.class.isAnnotationPresent(HelloAnnotation.class) 返回的是false。

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

APT处理注解

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

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

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

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

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

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

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

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

//============ 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"

然后把我们上面写的ValueAnnotationHelloProcessor放到annotation工程里面,最后的目录结构如下:
Java基础复习-注解篇

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

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

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

public class MainActivity extends AppCompatActivity {

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

    }

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

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

    @Override
    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;
    }

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

    @Override
    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");
    };
}

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


public class MainActivity extends AppCompatActivity {

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

    }
}

运行项目,logcat里面会输出我们的打印结果:
Java基础复习-注解篇

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

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

相关标签: 注解