Java 注解原理详细介绍

什么是注解

Java 注解(Annotation)又称 Java 标注或元数据,是 JDK5.0 引入的新特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。例如我们常见的@Override、@Deprecated、@Test等。

简单来说,可以把注解理解成代码中的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。通过注解,开发人员可以在不改变原有代码情况下,在源代码中嵌入补充信息。


annotation.png

注解的作用

  1. 生成文档:通过代码里标识的元数据生成javadoc文档;
  2. 编译检查:通过代码里标识的元数据,让编译器在编译期间进行检查;
  3. 编译时动态处理:例如编译时通过代码里标识的元数据,动态生成代码;
  4. 运行时动态处理:运行时通过代码里标识的元数据动态处理,例如使用反射注入实例

注解语法和使用

通常,注解可分为以下三类:
1. 元注解
元注解是用于定义注解的注解,java.lang.annotation提供了四种元注解,专门注解其他的注解:

  • @Documented: 标明是否生成javadoc文档
  • @Retention: 标明注解被保留的阶段,即什么时候使用该注解
public enum RetentionPolicy {
    SOURCE,            /* Annotation信息仅存在于编译器处理期间,编译器处理完之后就没有该Annotation信息了  */
    CLASS,             /* 编译器将Annotation存储于类对应的.class文件中,默认行为  */
    RUNTIME            /* 编译器将Annotation存储于class文件中,并且可由JVM读入 */
}

例如 @Override, 当它修饰一个方法的时候,表明该方法是重写父类的方法,编译期间会进行语法检查,但编译器处理完后,@Override就没有任何作用。

  • @Target: 标明注解使用的范围,即该注解用于什么地方
public enum ElementType {
    TYPE,               /* 描述类、接口(包括注释类型)或枚举  */
    FIELD,              /* 描述成员变量、对象、属性  */
    METHOD,             /* 用来描述方法 */
    PARAMETER,          /* 参数声明  */
    CONSTRUCTOR,        /* 构造方法声明  */
    LOCAL_VARIABLE,     /* 描述局部变量  */
    ANNOTATION_TYPE,    /* 注释类型声明  */
    PACKAGE             /* 包声明  */
}
  • @Inherited: 标明注解可继承,是否允许子类继承该注解

2. Java常用注解
@Override: 标明重写某个方法
@Deprecated: 标明某个类或方法过时
@SuppressWarnings: 标明要忽略的警告,
当代码中使用这些注解后,编译器就会进行检查。

例如,这两段代码是@Override和@SuppressWarnings的Java源文件:

[Override.java]
@Target(ElementType.METHOD)                       /* 表明@Override用于描述方法  */
@Retention(RetentionPolicy.SOURCE)                /* 表明@Override仅在于编译器处理期间存在  */
public @interface Override {
}

[SuppressWarnings.java]
 /*   表明@SuppressWarnings可以描述方法、字段、参数、构造方法类等  */
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) 
@Retention(RetentionPolicy.SOURCE)             /*   表明@SuppressWarnings仅在于编译器处理期间存在     */
public @interface SuppressWarnings {
    String[] value();
}

@interface: 表示实现了 java.lang.annotation.Annotation 接口,即该注解就是一个Annotation。
定义 Annotation 时,@interface 是必须的。
注意:它和通常的 implemented 实现接口的方法不同,Annotation 接口的实现细节都由编译器完成。
通过 @interface 定义注解后,该注解不能继承其他的注解或接口。
@Override使用很常见,来看一个使用@SuppressWarnings的例子:

@SuppressWarnings("deprecation")
private void push() {
    ......
}

如果push() 方法是过期的方法,编译时就会产生警告。而使用了 @SuppressWarnings(value={"deprecation"})后,编译器会对"调用 push() 产生的警告"保持沉默。

3. 自定义注解
可以根据自己的需求定义注解,如写UT/IT case 时使用到的@Test,Google 开源依赖注入框架Dagger2中的@Inject、@Module等

@Test注解后,在运行该方法时,测试框架会自动识别该方法并单独调

注解处理器

如果没有注解处理器,那注解跟注释其实没有什么区别,下面是Thinking Java的一个Demo,使用注解创建数据表:

1. 定义用来创建表名的注解@DBTable:

//DBTable.java
@Target(ElementType.TYPE) //Applies to classes only
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
    public String name() default "";
}

2. 定义用来约束的注解@Constraints:

//Constraints.java
@Target(ElementType.FIELD) 
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
    boolean primarykey() default false;
    boolean allowNull() default true;
    boolean unique() default false;
}

3. 定义声明String类型的注解@SQLString:

//SQLString.java
@Target(ElementType.FIELD)
public @interface SQLString {
    int value() default 0;
    String name() default "";
    Constraints constraints() default @Constraints;
}

4. 定义声明Integer类型的注解@SQLInteger:

//SQLInteger.java
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
    String name() default "";
    Constraints constraints() default @Constraints;
}

注意在SQLString.java和SQLInteger.java中constraints()元素的默认值就是@Constraints注解设定的默认值。如果现在要令嵌入的@Constraints注解中的unique()元素为true, 并以此作为constraints元素的默认值,则需要如下定义该元素

//Uniqueness.java
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Uniqueness {
    Constraints constraints() default @Constraints(unique = true)
}

5. 使用以上这些注解,定义一个数据库表:

//Member.java
@DBTable(name = "MEMBER")
public class Member {
    @SQLString(30) String firstName;
    @SQLString(50) String lastName;
    @SQLInteger Integer age;
    @SQLString(value = 30, constraints = @Constraints(primarykey = true))
    String handle;
    static int memberCount;

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public Integer getAge() {
        return age;
    }

    public String getHandle() {
        return handle;
    }

    public static int getMemberCount() {
        return memberCount;
    }
}

6. 实现注解处理器:

//TableCreator.java
{Args:com.sdkd.database.Member}
public class TableCreator {
    private static String getConstraints(Constraints con) {
        String constraints = "";
        if(!con.allowNull()) constraints += "NOT NULL";
        if(con.primarykey()) constraints += "PRIMARY KEY";
        if(con.unique()) constraints += " UNIQUE";
        return constraints;
    }
    public static void main(String[] args)throws Exception {
        if(args.length < 1) {
            System.out.println("arguments: annotated classes");
            System.exit(0);
        }
        for(String className : args) {
            Class<?> cl = Class.forName(className);
            DBTable dbTable = cl.getAnnotation(DBTable.class);
            if(dbTable == null) {
                System.out.println("No DBTable annotations in class" + className);
                continue;
            }
            String tableName = dbTable.name();
            //If the name is empty, use the Class name
            if(tableName.length() < 1) {
                tableName = cl.getName().toUpperCase();
            }
            List<String> columnDefs = new ArrayList<String>();
            for(Field field : cl.getDeclaredFields()) {
                String columnName = null;
                Annotation[] anns = field.getDeclaredAnnotations();
                if (anns.length < 1) continue;
                if (anns[0] instanceof SQLInteger) {
                    SQLInteger sInt = (SQLInteger) anns[0];
                    //Use field name if name not specified
                    if (sInt.name().length() < 1) {
                        columnName = field.getName().toUpperCase();
                    } else {
                        columnName = sInt.name();
                    }
                    columnDefs.add(columnName + " INT" + getConstraints(sInt.constraints()));
                }
                if (anns[0] instanceof SQLString) {
                    SQLString sString = (SQLString) anns[0];
                    if (sString.name().length() < 1) {
                        columnName = field.getName().toUpperCase();
                    } else {
                        columnName = sString.name();
                    }
                    columnDefs.add(columnName + " VARCHAR(" +
                            sString.value() + ")" +
                            getConstraints(sString.constraints()));
                }
            }
            StringBuilder createCommand = new StringBuilder(
                    "CREATE TABLE " + tableName + "("
            );
            for(String columnDef : columnDefs) {
                createCommand.append("\n    " + columnDef + ",");
                String tableCreate = createCommand.substring(0, createCommand.length() - 1) + ");";
                System.out.println("Table Creation SQL for " +
                        className + " is :\n" + tableCreate);
            }
        }
    }
}

7. main()方法测试:

public class Test {
    public static void main(String[] args) throws Exception {
        String[] arg = {"com.sdkd.database.Member"};
        new TableCreator().main(arg);
    }
}

7. 输出:

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