框架基础:深刻理解Java注解类型(@Annotation)

注解的概念

注解的官方定义

首先看看官方对注解的描述:html

An annotation is a form of metadata, that can be added to Java source code. Classes, methods, variables, parameters and packages may be annotated. Annotations have no direct effect on the operation of the code they annotate.java

翻译:程序员

注解是一种能被添加到java代码中的元数据,类、方法、变量、参数和包均可以用注解来修饰。注解对于它所修饰的代码并无直接的影响。数据库

经过官方描述得出如下结论:数组

注解是一种元数据形式。即注解是属于java的一种数据类型,和类、接口、数组、枚举相似。
注解用来修饰,类、方法、变量、参数、包。
注解不会对所修饰的代码产生直接的影响。app

注解的使用范围

继续看看官方对它的使用范围的描述:框架

Annotations have a number of uses, among them:Information for the complier - Annotations can be used by the compiler to detect errors or suppress warnings.Compiler-time and deployment-time processing - Software tools can process annotation information to generate code, XML files, and so forth.Runtime processing - Some annotations are available to be examined at runtime.ide

翻译:工具

注解又许多用法,其中有:为编译器提供信息 - 注解能被编译器检测到错误或抑制警告。编译时和部署时的处理 - 软件工具能处理注解信息从而生成代码,XML文件等等。运行时的处理 - 有些注解在运行时能被检测到。ui

##2 如何自定义注解
基于上一节,已对注解有了一个基本的认识:注解其实就是一种标记,能够在程序代码中的关键节点(类、方法、变量、参数、包)上打上这些标记,而后程序在编译时或运行时能够检测到这些标记从而执行一些特殊操做。所以能够得出自定义注解使用的基本流程:

第一步,定义注解——至关于定义标记;
第二步,配置注解——把标记打在须要用到的程序代码中;
第三步,解析注解——在编译期或运行时检测到标记,并进行特殊操做。

基本语法

注解类型的声明部分:

注解在Java中,与类、接口、枚举相似,所以其声明语法基本一致,只是所使用的关键字有所不一样@interface。在底层实现上,全部定义的注解都会自动继承java.lang.annotation.Annotation接口。

public @interface CherryAnnotation {
}

注解类型的实现部分:

根据咱们在自定义类的经验,在类的实现部分无非就是书写构造、属性或方法。可是,在自定义注解中,其实现部分只能定义一个东西:注解类型元素(annotation type element)。我们来看看其语法:

public @interface CherryAnnotation {
    public String name();
    int age() default 18;
    int[] array();
}

定义注解类型元素时须要注意以下几点:

  1. 访问修饰符必须为public,不写默认为public;

  2. 该元素的类型只能是基本数据类型、String、Class、枚举类型、注解类型(体现了注解的嵌套效果)以及上述类型的一位数组;

  3. 该元素的名称通常定义为名词,若是注解中只有一个元素,请把名字起为value(后面使用会带来便利操做);

  4. ()不是定义方法参数的地方,也不能在括号中定义任何参数,仅仅只是一个特殊的语法;

  5. default表明默认值,值必须和第2点定义的类型一致;

  6. 若是没有默认值,表明后续使用注解时必须给该类型元素赋值。

经常使用的元注解

一个最最基本的注解定义就只包括了上面的两部份内容:一、注解的名字;二、注解包含的类型元素。可是,咱们在使用JDK自带注解的时候发现,有些注解只能写在方法上面(好比@Override);有些却能够写在类的上面(好比@Deprecated)。固然除此之外还有不少细节性的定义,那么这些定义该如何作呢?接下来就该元注解出场了!
元注解:专门修饰注解的注解。它们都是为了更好的设计自定义注解的细节而专门设计的。咱们为你们一个个来作介绍。

@Target

@Target注解,是专门用来限定某个自定义注解可以被应用在哪些Java元素上面的。它使用一个枚举类型定义以下:

public enum ElementType {
    /** 类,接口(包括注解类型)或枚举的声明 */
    TYPE,

    /** 属性的声明 */
    FIELD,

    /** 方法的声明 */
    METHOD,

    /** 方法形式参数声明 */
    PARAMETER,

    /** 构造方法的声明 */
    CONSTRUCTOR,

    /** 局部变量声明 */
    LOCAL_VARIABLE,

    /** 注解类型声明 */
    ANNOTATION_TYPE,

    /** 包的声明 */
    PACKAGE
}

 

//@CherryAnnotation被限定只能使用在类、接口或方法上面
@Target(value = {ElementType.TYPE,ElementType.METHOD})
public @interface CherryAnnotation {
    String name();
    int age() default 18;
    int[] array();
}

@Retention

@Retention注解,翻译为持久力、保持力。即用来修饰自定义注解的生命力。
注解的生命周期有三个阶段:一、Java源文件阶段;二、编译到class文件阶段;三、运行期阶段。一样使用了RetentionPolicy枚举类型定义了三个阶段:

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文件中,但在运行时不会被虚拟机保留,这是一个默认的行为)
     */
    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.
     * (注解将被编译器记录在class文件中,并且在运行时会被虚拟机保留,所以它们能经过反射被读取到)
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

咱们再详解一下:

  1. 若是一个注解被定义为RetentionPolicy.SOURCE,则它将被限定在Java源文件中,那么这个注解即不会参与编译也不会在运行期起任何做用,这个注解就和一个注释是同样的效果,只能被阅读Java文件的人看到;

  2. 若是一个注解被定义为RetentionPolicy.CLASS,则它将被编译到Class文件中,那么编译器能够在编译时根据注解作一些处理动做,可是运行时JVM(Java虚拟机)会忽略它,咱们在运行期也不能读取到;

  3. 若是一个注解被定义为RetentionPolicy.RUNTIME,那么这个注解能够在运行期的加载阶段被加载到Class对象中。那么在程序运行阶段,咱们能够经过反射获得这个注解,并经过判断是否有这个注解或这个注解中属性的值,从而执行不一样的程序代码段。咱们实际开发中的自定义注解几乎都是使用的RetentionPolicy.RUNTIME;

自定义注解

在具体的Java类上使用注解

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
@Documented
public @interface CherryAnnotation {
    String name();
    int age() default 18;
    int[] score();
}

 

public class Student {
    @CherryAnnotation(name = "cherry-peng",age = 23,score = {99,66,77})
    public void study(int times){
        for(int i = 0; i < times; i++){
            System.out.println("Good Good Study, Day Day Up!");
        }
    }
}

简单分析下:

  1. CherryAnnotation的@Target定义为ElementType.METHOD,那么它书写的位置应该在方法定义的上方,即:public void study(int times)之上;

  2. 因为咱们在CherryAnnotation中定义的有注解类型元素,并且有些元素是没有默认值的,这要求咱们在使用的时候必须在标记名后面打上(),而且在()内以“元素名=元素值“的形式挨个填上全部没有默认值的注解类型元素(有默认值的也能够填上从新赋值),中间用“,”号分割;

注解与反射机制

为了运行时能准确获取到注解的相关信息,Java在java.lang.reflect 反射包下新增了AnnotatedElement接口,它主要用于表示目前正在 VM 中运行的程序中已使用注解的元素,经过该接口提供的方法能够利用反射技术地读取注解的信息,如反射包的Constructor类、Field类、Method类、Package类和Class类都实现了AnnotatedElement接口,它简要含义以下:

Class:类的Class对象定义   
Constructor:表明类的构造器定义   
Field:表明类的成员变量定义 
Method:表明类的方法定义   
Package:表明类的包定义

下面是AnnotatedElement中相关的API方法,以上5个类都实现如下的方法

 返回值  方法名称  说明
 <A extends Annotation>  getAnnotation(Class<A> annotationClass)  该元素若是存在指定类型的注解,则返回这些注解,不然返回 null。
 Annotation[]  getAnnotations()  返回此元素上存在的全部注解,包括从父类继承的
 boolean  isAnnotationPresent(Class<? extends Annotation> annotationClass)  若是指定类型的注解存在于此元素上,则返回 true,不然返回 false。
 Annotation[]  getDeclaredAnnotations()  返回直接存在于此元素上的全部注解,注意,不包括父类的注解,调用者能够随意修改返回的数组;这不会对其余调用者返回的数组产生任何影响,没有则返回长度为0的数组

 

简单案例演示以下:

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DocumentA {
}

 

package com.zejian.annotationdemo;

import java.lang.annotation.Annotation;
import java.util.Arrays;

@DocumentA
class A{

}

//继承了A类
@DocumentB
public class DocumentDemo extends A{

    public static void main(String... args){

        Class<?> clazz = DocumentDemo.class;
        //根据指定注解类型获取该注解
        DocumentA documentA=clazz.getAnnotation(DocumentA.class);
        System.out.println("A:"+documentA);

        //获取该元素上的全部注解,包含从父类继承
        Annotation[] an= clazz.getAnnotations();
        System.out.println("an:"+ Arrays.toString(an));
        //获取该元素上的全部注解,但不包含继承!
        Annotation[] an2=clazz.getDeclaredAnnotations();
        System.out.println("an2:"+ Arrays.toString(an2));

        //判断注解DocumentA是否在该元素上
        boolean b=clazz.isAnnotationPresent(DocumentA.class);
        System.out.println("b:"+b);

    }
}

执行结果:

A:@com.zejian.annotationdemo.DocumentA()
an:[@com.zejian.annotationdemo.DocumentA(), @com.zejian.annotationdemo.DocumentB()]
an2:@com.zejian.annotationdemo.DocumentB()
b:true

经过反射获取上面咱们自定义注解

public class TestAnnotation {
    public static void main(String[] args){
        try {
            //获取Student的Class对象
            Class stuClass = Class.forName("pojos.Student");

            //说明一下,这里形参不能写成Integer.class,应写为int.class
            Method stuMethod = stuClass.getMethod("study",int.class);

            if(stuMethod.isAnnotationPresent(CherryAnnotation.class)){
                System.out.println("Student类上配置了CherryAnnotation注解!");
                //获取该元素上指定类型的注解
                CherryAnnotation cherryAnnotation = stuMethod.getAnnotation(CherryAnnotation.class);
                System.out.println("name: " + cherryAnnotation.name() + ", age: " + cherryAnnotation.age()
                    + ", score: " + cherryAnnotation.score()[0]);
            }else{
                System.out.println("Student类上没有配置CherryAnnotation注解!");
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
    }
}

运行时注解处理器

了解完注解与反射的相关API后,如今经过一个实例(该例子是博主改编自《Tinking in Java》)来演示利用运行时注解来组装数据库SQL的构建语句的过程

/**
 * Created by ChenHao on 2019/6/14.
 * 表注解
 */
@Target(ElementType.TYPE)//只能应用于类上
@Retention(RetentionPolicy.RUNTIME)//保存到运行时
public @interface DBTable {
    String name() default "";
}


/**
 * Created by ChenHao on 2019/6/14.
 * 注解Integer类型的字段
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
    //该字段对应数据库表列名
    String name() default "";
    //嵌套注解
    Constraints constraint() default @Constraints;
}


/**
 * Created by ChenHao on 2019/6/14.
 * 注解String类型的字段
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {

    //对应数据库表的列名
    String name() default "";

    //列类型分配的长度,如varchar(30)的30
    int value() default 0;

    Constraints constraint() default @Constraints;
}


/**
 * Created by ChenHao on 2019/6/14.
 * 约束注解
 */

@Target(ElementType.FIELD)//只能应用在字段上
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
    //判断是否做为主键约束
    boolean primaryKey() default false;
    //判断是否容许为null
    boolean allowNull() default false;
    //判断是否惟一
    boolean unique() default false;
}

/**
 * Created by ChenHao on 2019/6/14.
 * 数据库表Member对应实例类bean
 */
@DBTable(name = "MEMBER")
public class Member {
    //主键ID
    @SQLString(name = "ID",value = 50, constraint = @Constraints(primaryKey = true))
    private String 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;//我的描述

   //省略set get.....
}

上述定义4个注解,分别是@DBTable(用于类上)、@Constraints(用于字段上)、 @SQLString(用于字段上)、@SQLString(用于字段上)并在Member类中使用这些注解,这些注解的做用的是用于帮助注解处理器生成建立数据库表MEMBER的构建语句,在这里有点须要注意的是,咱们使用了嵌套注解@Constraints,该注解主要用于判断字段是否为null或者字段是否惟一。必须清楚认识到上述提供的注解生命周期必须为@Retention(RetentionPolicy.RUNTIME),即运行时,这样才可使用反射机制获取其信息。有了上述注解和使用,剩余的就是编写上述的注解处理器了,前面咱们聊了不少注解,其处理器要么是Java自身已提供、要么是框架已提供的,咱们本身都没有涉及到注解处理器的编写,但上述定义处理SQL的注解,其处理器必须由咱们本身编写了,以下

package com.chenHao.annotationdemo;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

/**
 * Created by chenhao on 2019/6/14.
 * 运行时注解处理器,构造表建立语句
 */
public class TableCreator {

  public static String createTableSql(String className) throws ClassNotFoundException {
    Class<?> cl = Class.forName(className);
    DBTable dbTable = cl.getAnnotation(DBTable.class);
    //若是没有表注解,直接返回
    if(dbTable == null) {
      System.out.println(
              "No DBTable annotations in class " + className);
      return null;
    }
    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>();
    //经过Class类API获取到全部成员字段
    for(Field field : cl.getDeclaredFields()) {
      String columnName = null;
      //获取字段上的注解
      Annotation[] anns = field.getDeclaredAnnotations();
      if(anns.length < 1)
        continue; // Not a db table column

      //判断注解类型
      if(anns[0] instanceof SQLInteger) {
        SQLInteger sInt = (SQLInteger) anns[0];
        //获取字段对应列名称,若是没有就是使用字段名称替代
        if(sInt.name().length() < 1)
          columnName = field.getName().toUpperCase();
        else
          columnName = sInt.name();
        //构建语句
        columnDefs.add(columnName + " INT" +
                getConstraints(sInt.constraint()));
      }
      //判断String类型
      if(anns[0] instanceof SQLString) {
        SQLString sString = (SQLString) anns[0];
        // Use field name if name not specified.
        if(sString.name().length() < 1)
          columnName = field.getName().toUpperCase();
        else
          columnName = sString.name();
        columnDefs.add(columnName + " VARCHAR(" +
                sString.value() + ")" +
                getConstraints(sString.constraint()));
      }


    }
    //数据库表构建语句
    StringBuilder createCommand = new StringBuilder(
            "CREATE TABLE " + tableName + "(");
    for(String columnDef : columnDefs)
      createCommand.append("\n    " + columnDef + ",");

    // Remove trailing comma
    String tableCreate = createCommand.substring(
            0, createCommand.length() - 1) + ");";
    return tableCreate;
  }


    /**
     * 判断该字段是否有其余约束
     * @param con
     * @return
     */
  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 {
    String[] arg={"com.zejian.annotationdemo.Member"};
    for(String className : arg) {
      System.out.println("Table Creation SQL for " +
              className + " is :\n" + createTableSql(className));
    }
  }
}

推荐博客

  程序员写代码以外,如何再赚一份工资?

输出结果:

Table Creation SQL for com.zejian.annotationdemo.Member is :
CREATE TABLE MEMBER(
ID VARCHAR(50) NOT NULL PRIMARY KEY,
NAME VARCHAR(30) NOT NULL,
AGE INT NOT NULL,
DESCRIPTION VARCHAR(150)
);

若是对反射比较熟悉的同窗,上述代码就相对简单了,咱们经过传递Member的全路径后经过Class.forName()方法获取到Member的class对象,而后利用Class对象中的方法获取全部成员字段Field,最后利用field.getDeclaredAnnotations()遍历每一个Field上的注解再经过注解的类型判断来构建建表的SQL语句。这即是利用注解结合反射来构建SQL语句的简单的处理器模型。

相关文章
相关标签/搜索