Java八股文系列四:注解

image

一:基本语法

1.1 声明注解与元注解

先看看@MyTest注解是如何声明的:

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

}

使用@interface来声明MyTest是个注解,@Target(ElementType.METHOD)来声明这个注解应用于方法上面,@Retention(RetentionPolicy.RUNTIME)声明这个注解的生存期是运行时。反编译一下MyTest:

public interface MyTest implements Annotation {

 
}

注解就是一个实现了Annotation接口的接口。@Target和@Retention是Java提供的元注解,元注解就是标记其他注解的注解,下面分别介绍。

  • @Target用来约束注解可以应用的地方(如方法、类或字段),其中ElementType是枚举类型,代表可能的取值范围
public enum ElementType {
    /** 类、接口(包括注解)、枚举声明 */
    TYPE,

    /** 字段声明 (包括枚举) */
    FIELD,

    /** 方法声明 */
    METHOD,

    /** 参数声明 */
    PARAMETER,

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

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

    /** 注解声明(可应用于注解之上)*/
    ANNOTATION_TYPE,

    /** 包声明 */
    PACKAGE,

    /**
     * 参数类型声明
     *
     * jdk1.8加入的
     */
    TYPE_PARAMETER,

    /**
     * 类型使用声明
     *
     * jdk1.8加入的
     */
    TYPE_USE
}

当注解未指定Target值时,则此注解可以用于任何元素之上,多个值使用{}包含并用逗号隔开,如下:

@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
  • @Retention用来约束注解的生命周期,其中RetentionPolicy是枚举类型,代表可能的取值范围
public enum RetentionPolicy {
    /**
     * 源码级别,注解将被编译器丢弃
     */
    SOURCE,

    /**
     * 类文件级别,注解将存在源码和class文件中,会被JVM丢弃,默认的就是CLASS级别。
     */
    CLASS,

    /**
     * 注解信息将在运行期(JVM)也保留,因此可以通过反射机制读取注解的信息
     */
    RUNTIME
}

1.2 注解元素及其数据类型

@Target(ElementType.TYPE)//应用于类上
@Retention(RetentionPolicy.RUNTIME)//运行时
public @interface MyTable {
    String name() default "";//声明一个String类型的元素,default设置默认值
}

自定义MyTable注解用于数据库表与Bean类的映射(后面会有完整案例分析),@MyTable的使用方式如下:

@MyTable(name = "STUDENT")
public class Student {
    ...
}

注解支持的元素的数据类型如下:

  • 所有的基本数据类型(byte、short、int、long、float、double、char、boolean)
  • String
  • Class
  • Enum
  • Annotation
  • 上述类型的数组

下面的代码演示了上述类型的使用过程:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTable {
    
    int age();//基本数据类型

    String name() default "";//String

    Class<?> testCase() default Void.class;//Class

    enum sex {MALE, FEMALE};//枚举

    Reference reference() default @Reference(next = true);//注解

    long[] arr();//数组
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Reference{
    boolean next() default false;
}

注意,注解中所有的元素必须有值,要么是默认的值,要么是使用注解时提供的值,不能为null。注解不支持继承,一个注解不能使用关键字extends来继承其他注解。当注解中定义了名称为value的元素时,如果该元素是唯一一个需要赋值的元素,那么无需使用key=value的模式,直接在括号内给出value值即可。

1.3 Java内置注解与其他元注解

内置注解:

  • @Override:源码级,用于标识此方法覆盖了父类方法。
  • @Deprecated:运行时,用于标识此方法或类过时了。
  • @SuppressWarnnings:源码级,用于有选择的关闭编译器对类、方法、成员变量、变量初始化的警告。

元注解:

前面分析了@Target和@Retention,还有三种元注解,@Documented和@Inherited。

  • @Documented 运行时,被修饰的注解会生成到javadoc中。
  • @Inherited 运行时,可以让子类Class对象使用getAnnotations()来获得父类被@Inherited修饰的注解。
  • @Repeatable 运行时,它表示在同一个位置可以重复相同的注解。

二、注解与反射机制

通过反编译可以知道,Java中所有的注解都实现了Annotation接口,实现了Annotation接口就说明这个类是一个注解。注解有了,那么如何获取到注解的信息呢?Java在java.lang.reflect 反射包下新增了AnnotatedElement接口,实现了这个接口的类代表了这个类是被注解修饰的元素,可以得到相应的注解的信息,反射包中的Constructor类、Field类、Method类、Package类和Class类都实现了AnnotatedElement接口,也就是说可以通过反射技术获取到注解信息。

AnnotatedElement相关API:

返回值方法名称说明
booleanisAnnotationPresent(Class<? extends Annotation> annotationClass)指定类型的注解是否此元素上
getAnnotation(Class annotationClass)该元素如果存在指定类型的注解,则返回这些注解,否则返回 null。
Annotation[]getDeclaredAnnotations()返回直接存在于此元素上的所有注解,不包括父类的注解

代码说明:

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface DocumentB {
}

@Retention(RetentionPolicy.RUNTIME)
@Target(METHOD)
@Inherited
@interface DocumentC {
}

@DocumentA
class A{ }

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

    @DocumentC
    public static void test() {
    }

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

        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);

        //获取方法上的注解
        Method method = clazz.getMethod("test");
        DocumentC c = method.getAnnotation(DocumentC.class);
        System.out.println("c: " + c);
    }
}

输出:

A:@com.example.demo.thread.DocumentA()
an:[@com.example.demo.thread.DocumentA(), @com.example.demo.thread.DocumentB()]
an2:[@com.example.demo.thread.DocumentB()]
b:true
c: @com.example.demo.thread.DocumentC()

三、运行时注解处理器

经过上面的分析,我们已经自定义了注解,并且也获取到了注解,那么获取到注解有什么用呢?这个时候我们需要自己写一些处理逻辑来用上注解。下面通过一个实例来说明如何利用注解和反射来实现一个Java Bean和数据库表的映射功能。

/**
 * 表名注解
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTable {
    String name() default "";
}   
/**
 * 注解int型字段
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
    //该字段对应数据库表列名
    String name() default "";
    //嵌套注解
    Constraints constraint() default @Constraints;
}
/**
 * 注解String型字段
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
    //该字段对应数据库表列名
    String name() default "";
    //列类型分配的长度,如varchar(30)的30
    int value() default 0;
    //嵌套注解
    Constraints constraints() default @Constraints();
}
/**
 * 约束注解
 */
@Target(ElementType.FIELD)//只能应用在字段上
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
    //判断是否作为主键约束
    boolean primaryKey() default false;
    //判断是否允许为null
    boolean allowNull() default false;
    //判断是否唯一
    boolean unique() default false;
}

上面的@MyTable、@SQLInteger、@SQLString用于构建数据库表语句,@Constraints嵌套注解用于标识字段是否可以为null、是否唯一、是否是主键等信息,注解的声明周期必须是Runtime的,这样才能在运行时通过反射技术来获取注解的信息。注解定义好了,接下来使用注解:

/**
 * Member Bean
 */
@MyTable(name = "MEMBER")
public class Member {

    //主键
    @SQLInteger(name = "ID", constraint = @Constraints(primaryKey = true, unique = true))
    private Integer id;

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

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

    @SQLString(name = "DESCRIPTION", value = 50, constraints = @Constraints(allowNull = true))
    private String description;
}

给Bean Member类使用注解。接下来获取注解并编写处理逻辑:

public class TableCreator {

    public static String createTableSQL(String className) throws ClassNotFoundException {
        //根据classNam通过反射获取到Class对象
        Class<?> aClass = Class.forName(className);
        //得到类上的注解
        MyTable annotation = aClass.getAnnotation(MyTable.class);
        if (annotation == null) {
            return null;
        }
        List<String> columnDefs = new ArrayList<String>();
        //得到类注解上的name元素
        String tableName = annotation.name();
        //获取类中所有的字段
        for (Field field : aClass.getDeclaredFields()) {
            Annotation[] declaredAnnotations = field.getDeclaredAnnotations();
            if (declaredAnnotations.length < 1) {
                continue;
            }
            //如果是SQLString注解
            if (declaredAnnotations[0] instanceof SQLString) {
                SQLString sqlString = (SQLString) declaredAnnotations[0];
                String name = sqlString.name();
                columnDefs.add(name + " VARCHAR(" + sqlString.value() + ")" + getConstraints(sqlString.constraints()));
            }
            //如果是SQLInteger注解
            if (declaredAnnotations[0] instanceof SQLInteger) {
                SQLInteger sqlInteger = (SQLInteger) declaredAnnotations[0];
                String name = sqlInteger.name();
                columnDefs.add(name + " INT" + getConstraints(sqlInteger.constraint()));
            }
        }
        //数据库建表语句构建
        StringBuilder createSQL = new StringBuilder("CREATE TABLE " + tableName + "(");
        for (String columnDef : columnDefs) {
            createSQL.append("\n    " + columnDef + ",");
        }
        String tableCreate = createSQL.substring(
                0, createSQL.length() - 1) + ");";
        return tableCreate;
    }

    //获取约束条件
    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 ClassNotFoundException {
        String[] arg={"com.example.demo.thread.Member"};
        for(String className : arg) {
            System.out.println("Table Creation SQL for " +
                    className + " is :\n" + createTableSQL(className));
        }
    }
}

输出:

Table Creation SQL for com.example.demo.thread.Member is :
CREATE TABLE MEMBER(
    ID INT NOT NULL PRIMARY KEY UNIQUE,
    NAME VARCHAR(50) NOT NULL,
    AGE INT NOT NULL,
    DESCRIPTION VARCHAR(50));

大概流程:先传递类名,根据Class.forName()方法得到Class对象,然后通过Class对象获得所有的成员字段Field,最后利用field.getDeclaredAnnotations()来获取字段上的注解,判断注解的类型,解析注解的元素获取到值,然后构建SQL语句。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值