Java注解

在写java代码的过程中,经常会遇到注解,但是没有去理解注解背后的原理,也没有实现过注解。网上关于java注解的文章已经有很多了,参考了一些资料,整理一下注解这方面的知识~

什么是注解

注解其实很常见。比如@override、Deprecated等。在使用Junit或Spring boot等一些框架的时候,注解更是无处不在了。那么,到底什么是注解呢?

Annotations, a form of metadata, provide data about a program that is not part of the program itself. 
Annotations have no direct effect on the operation of the code they annotate.   
>>[https://docs.oracle.com/javase/tutorial/java/annotations/]

注解是元数据的一种,提供了关于程序的一些描述信息,但这些信息并不属于这个程序本身的一部分。注解并不会直接影响到代码的执行。

翻译起来有点拗口。实际上,注解是那些插入到源代码中用于某种工具进行处理的标签。注解不会改变对编写的程序的编译方式,对于包含和不包含注解的代码,java编译器都会生成相同的虚拟机指令。

一句话来说,注解只是描述代码的标签。注解本身不会做什么事情,为了使注解起到作用来实现一些黑科技,我们还需要用于处理注解的工具(编写代码处理这些注解)。

我们使用注解可以:

  • 生成文档

  • 在编译时进行检查。比如@override

  • 替代配置文件,实现自动配置。比如 Springboot

注解Annotation是在jdk1.5之后引进的,jdk1.8之后又增加了一些新的特性。接下来的讨论基于jdk1.7。

注解的使用

在java中,注解是当做一个修饰符(比如public或static之类的关键词)来使用的。注解可以存在于:

包 | 类(包括enum) | 接口(包括注解接口) | 方法 | 构造器 | 成员变量 | 本地变量 | 方法参数

注意:

1.对于包的注解,需要在package-info.java中声明。

2.对于本地变量的注解,只能在源码级别上进行处理。所有的本地变量注解在类编译完之后会被遗弃掉。

假如有这样一个注解:(注解的定义见下文)

@interface Result {
    String name() default "";
    int value() default -1;
    String res() default "";
}

我们可以这样使用它:

@Result(name="res1",value=1)

括号中元素的顺序无关紧要

@Result(value=1,name="res1")等价于@Result(name="res1",value=1)

如果元素值没有指定,则使用默认值:(没有声明默认值时必须指定元素值)

@Result等价于@Result(name="",value=-1,"")

如果元素的名字为特殊值value,那么可以忽略这个元素名和等号:

@Result(1)等价于@Result(name="",value=1,"")

如果元素是数组,那么他的值要用括号括起来:

@Result(res={"a","b"})

如果数组是单值,可以忽略这些括号:

@Result(res="a")

注解分类

根据注解的用途和使用方式,注解可以分为以下几类:

元注解:注解注解的注解。也就是用来描述注解定义的注解

预定义注解:jdk内置的一些注解

自定义注解:我们自己定义的注解

  • 元注解

元注解包含下面几个:

@Target: 指定这个注解可以应用于哪些项

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,

    /** Field declaration (includes enum constants) */
    FIELD,

    /** Method declaration */
    METHOD,

    /** Parameter declaration */
    PARAMETER,

    /** Constructor declaration */
    CONSTRUCTOR,

    /** Local variable declaration */
    LOCAL_VARIABLE,

    /** Annotation type declaration */
    ANNOTATION_TYPE,

    /** Package declaration */
    PACKAGE
}

比如,我们定义了一个注解Bug,该注解只能应用于方法或成员变量:

@Target({ElementType.METHOD,ElementType.FIELD})
@interface Bug{
    int value() default -1;
}

注解Bug则只能用于类方法或成员变量,如果注解了其他项比如类或者包,编译则不会通过。

对于一个没有声明@Target的注解,可以应用到任何项上。

@Retention: 指定这个注解可以保留多久

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

SOURCE:只存在于源代码,编译成.class之后就没了

CLASS: 保留到类文件中,但是虚拟机不会载入

RUNTIME:保留到类文件中,并且虚拟机会载入。这意味着通过反射可以获取到这些注解和注解元素值

默认情况下(没有声明@Retention),注解保留级别为CLASS

@Document:指定这个注解应该包含在文档中

文档化的注解意味着像javadoc这样的工具生成的文档中会包含这些注解。比如:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

@Deprecated是文档化的注解,URLDecoder.decode(String s)方法应用了这个注解:

image.png
image.png

可以在文档中看到Deprecated的出现。

@Inherited: 指定一个注解,当他应用于一个类的时候,能够自动被其子类继承

@Inherited只能应用于对类的注解。如果一个类具有继承注解,那么他的所有子类都自动具有同样的注解。

比如,定义了一个继承注解@Secret表示一个类是隐私的不可被序列化传输的,那么该类的子类会被自动注解为不可序列化传输的。

@Inherited
@interface Secret{
}

@Secret class A{}

class B extends A{} //同样是@Secret的

当注解工具去获取声明了@Secret的对象时,他能够获取到A的对象和B的对象。

  • 预定义注解

常用的有三个:@override、@Deprecated、@SuppressWarnings,具体的作用可以查文档或者源码,不再赘述。

注解的定义

上面的讨论中已经涉及到了注解的定义。一个注解是由一个注解接口来定义的:

@interface Result {
    String name();
    int value() default -1;
}

每个元素的声明有两种形式,有默认值和没有默认值的,就像上面那样。注解的元素可以是下面之一:

基本类型|String|Class类型|enum|注解类型|由前面所述类型构成的数组

@interface BugReport{
    enum Status{FIXED,OPEN,NEW,CLOSE};
    boolean isIgnore() default false;
    String id();
    Class<?> testCase() default Void.class;
    Status status() default Status.NEW;
    Author author() default @Author;
    String[] reportMsg() default "";
}

注意,虽然注解元素可以是另一个注解,但是不能在注解中引入循环依赖,比如@BugReport依赖@Author,而@Author又依赖@BugReport。同时,注解元素也不可以为null,元素的值必须是编译期常量。

我们可以通过在注解的定义前声明之前提到的元注解来定制我们的注解,比如:

@Target({ ElementType.METHOD, ElementType.FIELD })
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface Result {
    String name() default "";
    int value() default -1;
    String[] reportMsg() default "";
}

所有的注解接口隐式的继承自java.lang.annotation.Annotation接口。这是一个正常的接口,而不是注解接口。

public interface Annotation {
    boolean equals(Object obj);
    int hashCode();
    String toString();
    Class<? extends Annotation> annotationType();
}

也就是说,注解接口也是普通接口的一种。注解接口中的元素实际上也是方法的声明,这些方法类似于bean的get、set,我们使用@Result(name="A")的形式实际上是调用了set方法给某个变量赋值。

既然是接口,那么就应该有实现(不然怎么用呢?)。我们不需要主动提供实现了注解接口的类,虚拟机会在需要的时候产生一些代理类和对象。下文会提到。

既然可以为注解元素赋值,那么必定有方法去获得这些值。也就是注解的解析。

注解的解析

我们定义了注解并且应用了注解,但是仅仅这样的话注解并不会起到什么作用。需要我们提供一种工具去解析声明的注解,然后实现一些自动配置或者生成报告的功能。这就是注解的解析。

  • 源代码中的注解

注解的用处之一就是自动生成一些包含程序额外信息的文件。比如,根据注解生成代码进度报告,或者bug修复报告等。生成的文件可以是属性文件、xml文件、html文档或者shell脚本。也可以生成java源文件。

注解处理器通常通过集成AbstractProcessor类实现了Processor接口,通过process方法实现处理源码中注解的逻辑。通过声明具体的注解类型来指定该处理器处理哪些注解。

@SupportedAnnotationTypes("space.yukai.annotations.BUG")
class AnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // TODO Auto-generated method stub
        return false;
    }
    
}

process的两个参数:annotations代表了要处理的注解集,roundEnv是包含有关当前处理循环信息的RoundEnv引用。

Java注解处理器这篇文章以通过注解自动生成工厂类文件为例,详细介绍了如何处理源码级别的注解。(英文原文在这:http://hannesdorfmann.com/annotation-processing/annotationprocessing101

注意,我们虽然可以通过源码级别的注解处理器生成新的文件,却很难编辑源文件,比如,通过处理注解自动生成get、set方法。字节码级别的处理器是可以的。

  • 字节码中的注解

字节码级别的注解,即存在于class文件中的注解。我们还可以通过BCEL这样的字节码工程类库修改或插入字节码来改变类文件。比如在声明了@LogEntity的方法开始部分插入打印日志信息的字节码。

涉及的不多,不再赘述。

  • 运行时的注解

在运行时处理注解是比较常见的注解处理手段。一般是通过反射API获取到我们的注解信息,从而实现一些功能。

下面是自己写的一个例子,通过解析BugReport注解得到一些测试信息,然后通过动态代理的方式生成代理测试类,最后运行测试自动生成测试报告。(不要在意代码有什么缺陷或者其他问题,仅仅是一个例子而已~)

package space.kyu.proxy;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.List;

public class AnnotationsTest {
    public static void main(String[] args) {
        TestBug testBug = new TestBug(true);
        TestBug testBug1 = new TestBug(false);
        TestExecutor executor = new TestExecutor();
        executor.addTest(testBug);
        executor.addTest(testBug1);
        executor.executeTest();
    }
}

class TestExecutor {
    private List<Test>  testCases;
    public TestExecutor() {
        testCases = new ArrayList<Test>();
    }
    
    public <T extends Test> void  addTest(T testCase) {
        Class<? extends Object> cl = testCase.getClass();
        Method[] declaredMethods = cl.getDeclaredMethods();
        try {
            for (Method method : declaredMethods) {
                if (method.isAnnotationPresent(BugReport.class)) {
                    BugReport annotation = method.getAnnotation(BugReport.class);
                    if (annotation != null) {
                        System.out.println(annotation.toString());
                        System.out.println(annotation.annotationType().getName());
                        System.out.println(annotation.getClass().getName());
                        String bugId = annotation.id();
                        String bugMsg = annotation.msg();
                        Test obj = (Test) createBugReportHandler(testCase,bugId,bugMsg);
                        testCases.add(obj);
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new IllegalStateException(e);
        }
    }
    public void executeTest() {
        for (Test test : testCases) {
            test.test();
        }
    }
    private Object createBugReportHandler(Test testCase, final String bugId, final String bugMsg) {
        return Proxy.newProxyInstance(testCase.getClass().getClassLoader(), testCase.getClass().getInterfaces(), new InvocationHandler() {
            
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                boolean res = (boolean) method.invoke(testCase, args);
                //也可以输出到文件中形成测试报告
                System.out.println("******************************");
                System.out.println("bug: " + bugId + "测试结果:");
                if (res) {//测试通过
                    System.out.println("已通过");
                } else {//测试不通过
                    System.out.println("未通过");
                }
                System.out.println("备注信息:" + bugMsg);
                return res;
            }
        });
    }
}

interface Test {
    /**
     * 测试方法 2017年4月1日
     * @return 
     * true 通过测试 
     * false 未通过测试
     */
    boolean test();
}

class TestBug implements Test {
    boolean fixed;
    public TestBug(boolean fixed) {
        //控制测试成功或失败
        this.fixed = fixed;
    }
    @BugReport(id = "bug001", msg = "bug注释:这是一条测试bug")
    @Override
    public boolean test() {
        System.out.println("执行测试...");
        //假装测试成功或者失败了
        return fixed;
    }

}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface BugReport {
    String id();

    String msg();
}

运行结果:

@space.kyu.proxy.BugReport(id=bug001, msg=bug注释:这是一条测试bug)
space.kyu.proxy.BugReport
space.kyu.proxy.$Proxy1
@space.kyu.proxy.BugReport(id=bug001, msg=bug注释:这是一条测试bug)
space.kyu.proxy.BugReport
space.kyu.proxy.$Proxy1
执行测试...
******************************
bug: bug001测试结果:
已通过
备注信息:bug注释:这是一条测试bug
执行测试...
******************************
bug: bug001测试结果:
未通过
备注信息:bug注释:这是一条测试bug

上面的代码很简单,我们要注意的有几点:

1.method.isAnnotationPresent(BugReport.class)method.getAnnotation(BugReport.class)

这两个方法来自于接口AnnotatedElement,Method、Field、Package、Constructor、Class这些类都实现了这个接口,使得这些类拥有了提供所声明的注解的功能。

通过method.getAnnotation(BugReport.class)得到了声明在方法上的BugReport注解,获得这个注解的实例之后,我们就可以调用以该注解声明的元素为名称的方法来获取对应的元素值了。

2.annotation.annotationType().getName()annotation.getClass().getName()

annotation.annotationType()方法上面已经提到过了,是Annotation的一个方法,用于描述该注解对象的注解接口。这个方法返回的内容为:space.kyu.proxy.BugReport

annotation.getClass()获得了实现了Annotation接口的代理类,通过调用getName()方法可以打印这个代理类的名称:space.kyu.proxy.$Proxy1。从而印证了我们上面所说,确实自动生成了代理类。

上面的例子很简单,说白了,注解就是给代码加了一些额外的信息,这些信息对代码里面的逻辑是没有任何影响的。但是我们可以通过其他手段获得我们在代码中的注解,从而实现一些重复性的工作。这就是注解的作用。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,026评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,655评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,726评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,204评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,558评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,731评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,944评论 2 314
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,698评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,438评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,633评论 2 247
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,125评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,444评论 3 255
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,137评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,103评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,888评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,772评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,669评论 2 271

推荐阅读更多精彩内容

  • 内容概要 Annotation的概念 Annotation的作用 Annotation的分类 系统内置注解 元注解...
    DevinZhang阅读 4,098评论 0 28
  • 一、什么是注解? 注解对于开发人员来讲既熟悉又陌生,熟悉是因为只要你是做开发,都会用到注解(常见的@Overrid...
    _Justin阅读 1,319评论 0 10
  • Java 中的注解(Annotation) 是一个很方便的特性在Spring当中得到了大量的应用 , 我们也可以开...
    _秋天阅读 8,057评论 3 22
  • 本篇文章讲述Java中注解的相关知识。从Java中内置的注解,到自定义注解,最后再介绍如何使用注解。 一、元素据 ...
    Android进阶与总结阅读 402评论 0 2
  • 01 在我们小的时候,长辈是很喜欢看我们笑的,经常逗我们开心,当我们哭的时候,就会听到他们说:哭什么哭?不听话;乖...
    彩红fy阅读 352评论 6 4