Java注解之深入浅出

官方文档定义:注解是一系列元数据,它提供数据用来解释程序代码,但是注解并非是所解释的代码本身的一部分。注解对于代码的运行效果没有直接影响。

简单来说,你可以理解为一种标签,例如一提到自己心中的女神,脑海中自然会为她贴上"beautiful"这个标签,但是你的这个"beautiful"标签并不属于她本身,你的这个标签对她来说没有直接影响,你只是条单纯的舔狗而已......

好吧,回到正题,注解存在的意义是什么呢?
注解的用处主要如下:

  • 提供信息给编译器: 编译器可以利用注解来探测错误和警告信息
  • 编译阶段时的处理: 软件工具可以用来利用注解信息来生成代码、Html文档或者做其它相应处理。
  • 运行时的处理: 某些注解可以在程序运行的时候接受代码的提取

还是回到官方文档的解释上,注解主要针对的是编译器和其它工具软件(SoftWare tool)。
当开发者使用了Annotation 修饰了类、方法、Field 等成员之后,这些 Annotation 不会自己生效,必须由开发者提供相应的代码来提取并处理 Annotation 信息。这些处理提取和处理 Annotation 的代码统称为 APT(Annotation Processing Tool)。
所以呢,注解是给编译器或APT用的

例如:

public @interface AnnotationTest() {
}

@AnnotationTest
public void Test() {
}

上述代码简单来说就是把AnnotationTest作为一个标签贴到Test类上面去了
但是实际上并没有什么卵用,因为根本没有实现注解功能

想要注解能够正常工作,还是要依赖于元注解这个东西
元注解是一种基本注解,可以注解到其他的注解上面去
主要有@Retention@Document@Target@Inherited@Repeatable5种

@Retention

当它应用到一个注解上的时候,说明了这个注解的存活时间
取值如下:

  • RetentionPolicy.SOURCE: 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
  • RetentionPolicy.CLASS: 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
  • RetentionPolicy.RUNTIME: 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。
@Retention(RetentionPolicy.CLASS)
public @interface AnnotationTest() {
}

@Document

将被标注的注解生成到javadoc中

@Target

用于指定被此元注解标注的注解可以标注的程序元素
可以看一下@Target的源码

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}

可以看到有一个value属性,返回一个枚举ElementType类型的数组,这个数组的值就代表了可以使用的程序元素。

public enum ElementType {
    /** Class, interface (including annotation type), or enum declaration */
    TYPE,
    /** Field declaration (includes enum constants) */
    FIELD,
    /** Method declaration */
    METHOD,
    /** Formal parameter declaration */
    PARAMETER,
    /** Constructor declaration */
    CONSTRUCTOR,
    /** Local variable declaration */
    LOCAL_VARIABLE,
    /** Annotation type declaration */
    ANNOTATION_TYPE,
    /** Package declaration */
    PACKAGE,
    /**
     * Type parameter declaration
     *
     * @since 1.8
     */
    TYPE_PARAMETER,
    /**
     * Use of a type
     *
     * @since 1.8
     */
    TYPE_USE
}

@Target取值如下

  • ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举
  • ElementType.FIELD 可以给属性进行注解
  • ElementType.METHOD 可以给方法进行注解
  • ElementType.PARAMETER 可以给一个方法内的参数进行注解
  • ElementType.CONSTRUCTOR 可以给构造方法进行注解
  • ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
  • ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
  • ElementType.PACKAGE 可以给一个包进行注解
  • ElementType.TYPE_PARAMETER标明注解可以用于类型参数声明(1.8新加入)
  • ElementType.TYPE_USE类型使用声明(1.8新加入),可以标注除class之外的任意类型
    当注解未指定Target值时,则此注解可以用于任何元素之上,多个值使用{}包含并用逗号隔开
@Target(value = {ElementType.CONSTRUCTOR,ElementType.FIELD,ElementType.TYPE_PARAMETER})

@Inherited

其让被修饰的注解拥有被继承的能力,但是它并不是说注解本身可以继承,而是说如果一个父类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了父类的注解。

@Documented
@Retention(RetentionPolicy.CLASS)
@Target(value = {ElementType.TYPE})
@Inherited
public @interface TestAnnotation {}

@TestAnnotation
public class A {}

public class B extends A{}

也就是说B也拥有TestAnnotation 这个注解

@Repeatable

表示注解可以多次应用,通常是注解的值可以取多个
一个很简单的例子:我是一个程序员,同时也是美食家和摄影师

@interface Live {
    People[] value();
}

@Repeatable(Live.class)
@interface People {
    String role() default "";
}

@People(role = "programmer")
@People(role = "gastronome")
@People(role = "photographer")
public class Me {
}

上述代码通过@Repeatable注解了People ,而@Repeatable 后面括号中的Live类相当于一个容器注解(A fucking new concept)

容器注解:就是用来存放其它注解的地方,它本身也是一个注解

按java规定,容器注解里面必须要有一个value属性,属性类型是一个被 @Repeatable 注解过的注解数组

注解的属性也叫成员变量,注解只有成员变量,没有方法,而且成员变量在注解的定义中以“无形参的方法”的形式来声明,方法名定义了成员变量的名字,返回值定义了成员变量的类型。

在注解中定义属性时它的类型必须是 8 种基本数据类型外加 类、接口、注解及它们的数组

注解中属性可以有默认值,默认值需要用 default 关键值指定,这样做的好处是可以省略赋值操作

@People()
public class Me {}

如果成员变量仅有一个名字叫做value的属性时,你甚至可以这样写

@People("programmer")
public class Me {}

如果没有成员变量的话,括号都可以省略哟!

@People
public class Me {}
Java语言本身也提供了预置的注解

@Deprecated
这个元素是用来标记过时的元素,会有一条横线,这是编译器识别后的提醒效果
@SuppressWarnings
阻止警告的意思。之前说过调用被 @Deprecated 注解的方法后,编译器会警告提醒,而有时候开发者会忽略这种警告,他们可以在调用的地方通过 @SuppressWarnings 达到目的。
@SafeVarargs
参数安全类型注解。它的目的是提醒开发者不要用参数做一些不安全的操作,它的存在会阻止编译器产生 unchecked 这样的警告。它是在 Java 1.7 的版本中加入的。
@FunctionalInterface
函数式接口注解,这个是 Java 1.8 版本引入的新特性。函数式编程很火,所以 Java 8 也及时添加了这个特性。
函数式接口 (Functional Interface) 就是一个具有一个方法的普通接口。
PS:函数式接口标记有什么用,这个原因是函数式接口可以很容易转换为 Lambda 表达式

想必上面的理论知识都能很快理解吧,接下来就手撕代码吧

注解只是给我们想要的东西去贴上一个标签,那么怎么去阅读标签呢?
注解通过反射获取
查看一下Class.java源码,可以找到isAnnotationPresent()这个方法,用来判断它是否应用了某个注解

    /**
     * {@inheritDoc}
     * @throws NullPointerException {@inheritDoc}
     * @since 1.5
     */
    @Override
    public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
        return GenericDeclaration.super.isAnnotationPresent(annotationClass);
    }

上面代码可以看出Annotation接口是所有注解的父接口
此外,可以通过getAnnotation(Class<A> annotationClass())获取指定类型的Annotation对象

    /**
     * @throws NullPointerException {@inheritDoc}
     * @since 1.5
     */
    @SuppressWarnings("unchecked")
    public <A extends Annotation> A getAnnotation(Class<A> annotationClass) {
        Objects.requireNonNull(annotationClass);

        return (A) annotationData().annotations.get(annotationClass);
    }

getAnnotations()可以获取到这个元素上的所有类型的Annotation对象

    /**
     * @since 1.5
     */
    public Annotation[] getAnnotations() {
        return AnnotationParser.toArray(annotationData().annotations);
    }

getDeclaredAnnotation(Class<A> annotationClass)返回该元素上存在的直接修饰该元素的指定类型的注解,如果不存在则返回null.

    /**
     * @throws NullPointerException {@inheritDoc}
     * @since 1.8
     */
    @Override
    @SuppressWarnings("unchecked")
    public <A extends Annotation> A getDeclaredAnnotation(Class<A> annotationClass) {
        Objects.requireNonNull(annotationClass);

        return (A) annotationData().declaredAnnotations.get(annotationClass);
    }

getDeclaredAnnotations()返回该元素上存在的直接修饰该元素的所有注解

    /**
     * @since 1.5
     */
    public Annotation[] getDeclaredAnnotations()  {
        return AnnotationParser.toArray(annotationData().declaredAnnotations);
    }

okay,枯燥无聊的源码还有很多,例如getDeclaredAnnotationsByType()、getAnnotationsByType()等等,接下来用一个简单的例子通过反射来获取Annotation的属性
首先,定义一个注解

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
    public int id() default 1;
    public String name() default "Kris";
}

然后呢

@TestAnnotation(id = 666,name = "Kris")
public class Me {
    public static void main(String[] args) {
        boolean hasAnnotation = Me.class.isAnnotationPresent(TestAnnotation.class);
        if (hasAnnotation) {
            TestAnnotation testAnnotation = Me.class.getAnnotation(TestAnnotation.class);
            System.out.println("id:"+testAnnotation.id());
            System.out.println("name:"+testAnnotation.name());
        }
    }
}

okay,接下来执行这个程序就可以发现:
Amazing!!!


那么,继续加把力呗!

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface A {
    String value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface B {
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface C {
}

@TestAnnotation(id = 666,name = "Kris")
public class Me {

    @A(value = "This is A!")
    int a;

    @B
    @C
    public void methodTest() {
    }

    public static void main(String[] args) {
        boolean hasAnnotation = Me.class.isAnnotationPresent(TestAnnotation.class);
        if (hasAnnotation) {
            TestAnnotation testAnnotation = Me.class.getAnnotation(TestAnnotation.class);
            System.out.println("id:"+testAnnotation.id());
            System.out.println("name:"+testAnnotation.name());
        }
        try {
            Field a = Me.class.getDeclaredField("a");
            a.setAccessible(true);
            A annotation = a.getAnnotation(A.class);
            if (annotation != null) {
                System.out.println("A value:"+annotation.value());
            }
            Method methodTest = Me.class.getDeclaredMethod("methodTest");
            if (methodTest != null) {
                Annotation[] ans = methodTest.getAnnotations();
                for (int i=0;i<ans.length;i++) {
                    System.out.println("methodTest Annotation:"+ans[i].annotationType().getSimpleName());
                }
            }
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }
}

显而易见的得到结果:


综上所述,只要通过给元素添加注解,再利用反射就可以获取注解啦,但是获取到有什么用呢?下面就通过数据库SQL创建语句实例来讲解一下APT(Annotation Process Tool)吧。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
    String name() default "";
}

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

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

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
    String name() default "";
    int value() default 0;
    Constraints constraint() default @Constraints;
}

@DBTable(name = "MEMBER")
public class Member {

    @SQLString(name = "ID",value = 50,constraint = @Constraints(primaryKey = true))
    private int id;

    @SQLString(name = "NAME",value = 30)
    private String name;

    @SQLInteger(name = "AGE")
    private int age;

    @SQLString(name = "DESCRIPTION",value = 150,constraint = @Constraints(allowNull = true))
    private String description;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

public class TableCreator {
    public static void main(String[] args) {
        String[] arg={"AnnotationTest.SQLAnnotation.Member"};
        for(String className : arg) {
            System.out.println("Table Creation SQL for " +
                    className + " is :\n" + createTableSql(className));
        }
    }

    private static String createTableSql(String className) {
        Class<?> cl = null;
        DBTable dbTable = null;
        try {
            cl = Class.forName(className);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        if (cl != null) {
            dbTable = cl.getAnnotation(DBTable.class);
        }
        if (dbTable == null) {
            System.out.println("No DBTable Annotations int class!");
            return null;
        }
        String tableName = dbTable.name();

        if (tableName.length() == 0) {
            tableName = cl.getName().toUpperCase();
        }
        List<String> columnDefs = new ArrayList<>();
        for (Field field : cl.getDeclaredFields()) {
            String columnName = null;
            if (field.isAnnotationPresent(SQLInteger.class)) {
                SQLInteger sqlInteger = (SQLInteger) field.getDeclaredAnnotation(SQLInteger.class);
                if (sqlInteger.name().length() == 0) {
                    columnName = field.getName().toUpperCase();
                } else {
                    columnName = sqlInteger.name();
                }
                columnDefs.add(columnName + " INT" + getConstraints(sqlInteger.constraint()));
            } else if (field.isAnnotationPresent(SQLString.class)) {
                SQLString sqlString = (SQLString) field.getDeclaredAnnotation(SQLString.class);
                if (sqlString.name().length() == 0) {
                    columnName = field.getName().toUpperCase();
                } else {
                    columnName = sqlString.name();
                }
                columnDefs.add(columnName+" VARCHAR(" + sqlString.value() + ")" + getConstraints(sqlString.constraint()));
            }
        }
        StringBuilder createCommand = new StringBuilder("CREATE TABLE " + tableName + "(");
        for (String columnDef : columnDefs) {
            createCommand.append("\n " + columnDef + ",");
        }
        return createCommand.substring(0,createCommand.length()-1)+");";
    }

    private static String getConstraints(Constraints con) {
        StringBuilder constraints = new StringBuilder();
        if (!con.allowNull()) {
            constraints.append(" NOT NULL");
        }
        if (con.primaryKey()) {
            constraints.append(" PRIMARY KEY");
        }
        if (con.unique()) {
            constraints.append(" UNIQUE");
        }
        return constraints.toString();
    }
}

OK,这样就可以成功得通过APT得到所需要的SQL语句啦~

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