Java基础复习-注解篇

注解 (也被称为元数据) 为我们在代码中添加信息提供了一种形式化的方法,使我们可以在稍后某个时刻非常方便地使用这些数据. —— 《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工程里面,最后的目录结构如下:
这里写图片描述

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
这里写图片描述

这时在app工程的build目录下会生成我们想要的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里面会输出我们的打印结果:
这里写图片描述

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值