Java注解(三) 自定义注解与提取注解

来源:互联网 发布:桂树焉知泰山之高的焉 编辑:程序博客网 时间:2024/04/30 14:33

前言

前面两篇文章我们已经认识了Java的基本概念跟一些常用的Java系统注解,特别是上一篇文章中我们详细介绍了Java 的元注解(@Retention@Target@Inherited@Documented),下面我们会来讲解这些元注解怎么用在自定义注解的过程中(如果你还对注解基本知识不是很清楚可以查看上两篇博文)。

只定义了注解对程序的运行是没有什么影响的,如果要根据注解实现一些功能,那么就得懂得如何提取注解,文章的第二部分就会介绍如何提取注解。

概要

  • 自定义注解
    • @interface关键字
    • 自定义注解参数
    • 元注解使用
  • 提取注解
    • AnnotatedElement接口
    • 通过反射机制提取注解中的值

自定义注解

Java不仅提供了一些常用的注解,而且提供了元注解供开发者自定义注解。

学习了自定义注解之后就可以利用注解实现一些简化代码的功能(比如 数据库表关系映射ORM、Android中一些框架提供的用注解来简化findViewById写法 等等)。
但这些方法大都利用运行时注解,然后通过Java的反射机制实现功能,所以难免会在性能上带来损耗(损耗的根源我还不特别清楚,这是我接下来要做的事情之一),所以在使用注解的时候还是要权衡一下的。

@interface关键字
定义注解跟定义接口非常相似,只是关键字上多了个@,所以最简单的一种注解定义如下:
SimplestAnnotation.java
/*** 简单的注解定义*/public @interface SimplestAnnotation{}
定义的注解最终会继承Annotation类(Java内部处理,这里记得就够了),默认情况下,注解可以修饰任何程序元素(类、接口、方法等)。

上面这种不带任何参数的注解有一个特殊的名字---标记注解
既然不带参数的注解有特殊名字,那么带参数的注解应该也有名字才对。没错,带参数的注解可以为被注解元素提供元数据(解释数据的数据),所以称为---元数据注解

自定义注解参数
那我们该如何自定义元数据注解呢?也就是我们怎么给注解加上参数呢?下面看看带参数的注解。
OneParamAnnotation.java
/*** 带一个参数的注解* Created by Jayme on 16/4/25.*/public @interface OneParamAnnotation {    String value();}
上面我们写了一个带一个参数value的注解OneParamAnnotation,这也就是元数据注解的一种。参数的声明方式跟接口的函数声明方式非常类似(访问性修饰符必须为defaultpublic),返回值指明了该参数的类型(这里是String)。还有一点要比较注意的是参数声明不能带任何参数(比如String value(int age);这种是不允许的)。

那我们要怎么使用它呢?接下来继续看使用的代码:

Test.java
public class Test {    /**     *  方法1     */     @OneParamAnnotation(value = "Jayme")    String argOne = "";    /**     *  方法2     */     @OneParamAnnotation("Jayme")    String argTwo = "";}

从上面代码中我们可以看到有两种使用方式,而且显然方法2方法1更加简洁。
方法1使用的是最基本的使用方法,括号里前面为参数名后面为参数值。
方法2则直接使用参数值(但这种方法要保证使用的注解(这里指@OneParamAnnotation)必须只有一个参数而且参数名必须为value),所以我们在自定义注解的时候一般只有一个参数的时候会把参数名取为value

我们掌握了含有一个参数的注解的写法之后,含有多个参数的注解的写法也就很类似了。让我们看看这个含有多个参数的注解:

MultiParamsAnnotation.java
/*** 含有多个参数的注解* Created by Jayme on 16/4/30.*/public @interface MultiParamsAnnotation {    String paramString() default "";    int paramInt();    long paramLong();}

仔细观察的人可能已经发现参数paramString多了一个default修饰,这代表paramString的默认值就是"",也就是说我们在自定义注解的时候是可以给参数加上默认值的。接下来我们来看一下如何使用含有多个参数的注解。

Test.java
/*** 测试类* Created by Jayme on 16/4/26.*/public class Test {    @MultiParamsAnnotation(paramString = "Jayme", paramInt = 12, paramLong = 1111111111)    String argOne = "";    @MultiParamsAnnotation(paramInt = 12, paramLong = 1111111111)    String argTwo = "";}

从上面代码中我们可以看出,带有default属性的参数在使用注解的时候可以重新设置(例如argOne的注解)也可以不设置直接使用默认值(例如argTwo的注解)。但要注意的一点是在使用注解时必须提供所有没有带default的参数的值,不然就会报错(具体看下图)。
这里写图片描述

元注解使用
元注解主要用来表示自定义的注解的可见性、可修饰元素、是否可继承等(这里知识不清晰回看上一篇博文)。
使用元注解比较简单,直接在自定义的注解声明前面加上元注解即可(有参数的带上相应的参数)。我们来看一下实际的例子,下一篇博文会用到的 数据库关系映射(ORM)框架 的注解 表注解(@Table)列注解(@Column)
Table.java
/** * 表注解 * Created by Jayme on 16/4/25. */@Documented@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface Table {    String value();    //省略...}

在ORM中一般把表映射为实体类,所以表注解的修饰类型这里设为ElementType.TYPE代表可修饰类类型;因为想要把@Table注解加入到Javadoc文档中,所以加入了@Documented注解;因为表的数据(表名等)都是在运行时通过反射机制获取的,所以我们设置@Retention注解并传入参数RetentionPolicy.RUNTIME(也就是把表注解的可见性设置为运行时可见)。

Column.java
/*** 列注解* Created by Jayme on 16/4/25.*/@Documented@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public @interface Column {    String value();    //省略...}

在ORM中一般把列映射为类成员变量,所以表注解的修饰类型这里设为ElementType.FIELD代表可修饰成员变量类型

上面我们学习了如何自定义注解跟使用注解,但是可能你会发觉写了那些注解跟没写那些注解对于程序的运行来说没有任何影响。那么我们应该如何利用注解来实现我们想要的一些功能呢?让我们来看接下来的提取注解的部分。

提取注解

我们在使用注解的时候可能会给参数设置值,提取注解针对的对象是可注解元素,做的其实有两件事:
1. 确定该元素是否被注解(标记注解只要知道是否被注解就可以了)
2. 被注解的话获取到注解的参数值

要做到上面的两件事情,我们就需要借助java.lang.reflect包下AnnotatedElement接口的帮助了。

AnnotatedElement接口
AnnotatedElement接口是Java反射包下用于处理注解的接口(会用到简单的反射知识,后续会有反射的介绍)
已知实现AnnotatedElement接口的类: ClassConstructorFieldMethodPackage

因为上面这些类实现了AnnotatedElement接口,所以我们就可以通过反射结合AnnotatedElement接口中的一些方法来实现提取注解的操作。
首先我们先观察一下AnnotatedElement接口的部分源码:

AnnotatedElement.java部分源码

/*** Represents an annotated element of the program currently running in this* VM.  This interface allows annotations to be read reflectively.  All* annotations returned by methods in this interface are immutable and* serializable. The arrays returned by methods of this interface may be modified* by callers without affecting the arrays returned to other callers.*/public interface AnnotatedElement {    /**     * Returns true if an annotation for the specified type     * is present on this element, else false.  This method     * is designed primarily for convenient access to marker annotations.     *     * @since 1.5     */    default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {        return getAnnotation(annotationClass) != null;    }    /**     * Returns this element's annotation for the specified type if     * such an annotation is present, else null.     *     * @since 1.5     */    <T extends Annotation> T getAnnotation(Class<T> annotationClass);    /**     * Returns annotations that are present on this element.     *     * If there are no annotations present on this element, the return     * value is an array of length 0.     *     * The caller of this method is free to modify the returned array; it will     * have no effect on the arrays returned to other callers.     *     * @since 1.5     */    Annotation[] getAnnotations();    /**     * Returns annotations that are <em>directly present</em> on this element.     * This method ignores inherited annotations.     *     * If there are no annotations <em>directly present</em> on this element,     * the return value is an array of length 0.     *     * The caller of this method is free to modify the returned array; it will     * have no effect on the arrays returned to other callers.     *     * @return annotations directly present on this element     * @since 1.5     */    Annotation[] getDeclaredAnnotations();    //省略部分函数(Java1.8 新加入了几个函数)}

我们简单分析下源码:
AnnotatedElement 代表的是被注解的元素,它的作用是让注解可以通过反射被读取。它方法的返回值是不可修改的,通过方法获取到的数组就算被用户改变也不会影响到被注解元素的方法返回结果(也就是说返回的是拷贝而不是引用)。

方法名 参数 返回值 作用 isAnnotationPresent 注解对应的Class boolean 检测该元素是否被参数对应注解修饰 getAnnotation 注解对应的Class Annotation 获取注解对象 getAnnotationsAnnotation[] 返回该程序元素上存在的所有注解(如果没有注解存在于此元素上,则返回长度为零的一个数组。) getDeclaredAnnotationsAnnotation[] 返回直接存在于此元素上的所有注解。与此接口中的其他方法不同,该方法将忽略继承的注解。(如果没有注解直接存在于此元素上,则返回长度为零的一个数组。)

通过对源码的分析我们大概知道了AnnotatedElement街口中方法的含义,下面就来看看具体的代码中怎么提取注解。

首先我先给出会用到的三个自定义的注解(@Table@Column@InheritedAnnotation
Table.java

/*** 表注解* Created by Jayme on 16/4/25.*/@Documented@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface Table {    String value();    //省略部分属性...}

注解中只有一个属性value表示表名。

Column.java

/*** 列注解* Created by Jayme on 16/4/25.*/@Documented@Target(ElementType.FIELD)@Retention(RetentionPolicy.RUNTIME)public @interface Column {    String value();    //省略部分属性...}

注解中只有一个属性value表示列名。

InheritedAnnotation.java

/*** 会被继承的注解* Created by Jayme on 16/5/1.*/@Inherited@Retention(RetentionPolicy.RUNTIME)public @interface InheritedAnnotation {}
这里主要用于测试getAnnotationsgetDeclaredAnnotations区别

ORM框架都是在实体类的基类里面去实现ORM的功能的,所以我们这里也需要有一个实体类的基类(BaseEntity),我们会在里面提取注解,但我们现在不会在里面实现具体的数据库操作,因为我们这篇文章要讲的内容只是提取注解。

BaseEntity.java

/*** 所有实体类的基类* Created by Jayme on 16/4/26.*/@InheritedAnnotationpublic class BaseEntity {    public void test() {        String toShowMessage = "";        Class entityClass = this.getClass();        /** 获取表名信息 */        if (entityClass.isAnnotationPresent(Table.class)) {            Table table = (Table) entityClass.getAnnotation(Table.class);            toShowMessage += "Table: " + table.value() + "\n";        }        /** 获取列名信息 */        Field[] entityFields = entityClass.getDeclaredFields();        for (Field field :                entityFields) {            if (field.isAnnotationPresent(Column.class)) {                Column column = field.getAnnotation(Column.class);                toShowMessage += column.value() + " : ";                Object value = null;                try {                    field.setAccessible(true);                    value = field.get(this);                    toShowMessage += value + "\n";                } catch (IllegalAccessException e) {                    e.printStackTrace();                }            }        }        /**         * 这里你就可以获取到Entity的信息         * 然后执行正真的数据库操作         */        //ORM框架可以在这里执行数据库操作        /**         * 下面看下getAnnotations跟getDeclaredAnnotations的区别         */        toShowMessage += "getAnnotations(): ";        Annotation[] getAnnotations = entityClass.getAnnotations();        for (Annotation annotation :                getAnnotations) {            toShowMessage += annotation.toString() + "     ";        }        toShowMessage += "\ngetDeclaredAnnotations(): ";        Annotation[] getDeclaredAnnotations = entityClass.getDeclaredAnnotations();        for (Annotation annotation :                getDeclaredAnnotations) {            toShowMessage += annotation.toString() + " ";        }        System.out.println(toShowMessage);    }}

上面的代码通过反射机制并且结合AnnotatedElement的函数获取到了运行时可见注解的信息及被注解元素的一些信息,我们在获得这些信息之后就可以执行相应的数据库操作(比如创建表之类的),这也就是ORM框架利用注解的基本原理。为了检验getAnnotationsgetDeclaredAnnotations的不同之处也特意加上了一些检验代码。下面让我们来写一个User类继承自BaseEntity看看结果。

User.java

/*** 用户表* Created by Jayme on 16/4/26.*/@Table("User")public class User extends BaseEntity {    @Column("id")    private String id = "123456";    @Column("name")    private String name = "Jayme";    public static void main(String[] args) {        User user = new User();        user.test();    }}

我们运行User中的main方法,就会看到以下输出:

这里写图片描述

从输出结果我们也可以看出我们确实通过反射机制成功提取注解的信息。

输出信息中getAnnotationsgetDeclaredAnnotations获取结果的区别在于有没有获取到继承注解@InheritedAnnotations,这样我们也就知道getAnnotations会获取返回该程序元素上存在的所有注解,但getDeclaredAnnotations只返回直接存在于此元素上的所有注解,忽略继承的注解。

总结

这篇博文花了比较多的信息去讲如何自定义注解跟提取注解,涉及到一些比较细节的内容,整体难度不会很大,但要对反射机制有一定的了解。

思路脑图

这里写图片描述

完整的思维导图可以在我的github上面查找,地址为https://github.com/liujiescut/AndroidKnowledge

如果时间运行,下一篇文章将介绍简单的ORM框架原理。

1 0