一文带你回忆Java注解的有关内容

409 阅读7分钟

Java注解

在面对大型的Java EE项目中,常常通过注解进行地址映射,对象注入等操作,因此有必要去了解一下注解的相关的知识。

注解是 java5 引入众多语言变化之一,可以用于表达在java中无法表达且你需要完整表述程序所需的信息。注解可以让我们可以以编译器验证的格式存储程序的额外信息。通过使用注解,可以将元数据保存在Java源代码中,且具备以下优势:

  • 简单易读
  • 编译器类型检查
  • 使用annotation API为自己的注解构造处理工具。

JAVA 5引入的注解:

  • @Override:表示当前的方法定义将覆盖基类的方法。(若不小心拼写错误,或者方法签名被错误拼写,编译器会发出错误提示)
  • @Deprecated:如果使用该注解的元素,编译器会报错。(表示该元素已经被弃用)
  • @SuppressWarnings:关闭不当的编译器警告信息。
  • @SafeVarargs:在JAVA7中加入用于禁止对具有泛型的varargs参数的方法或构造函数的调用发出警告
  • @FunctionalInterface:在JAVA8中加入用于表示类型声明为函数式接口。

一个注解的定义:

注解的定义类似于接口,与其他的java接口一样,同样会被编译成class文件。

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

注解的定义也需要一些元注解:

  • @Target:定义你的注解可以应用在哪里(方法还是字段)
  • @Retention:定义了注解在哪里可用(源代码(SOURCE),class文件(CLASS),运行时(RUNTIME))

我们也可以用注解包含一些特定值。不包含任何元素的注解称之为:标记注解(maker annotation),如上面的@Test

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
    int id();
    String description() default "no description";
}

id()description()与方法的定义类似。description带有一个默认值,当你不显式使用时,就会使用默认值。

应用:

public class AnnotationTest {
    
    @UseCase(id = 1,description = "This a Test demo")
    public void print(){
        System.out.println("Test demo...");
    }
    @UseCase(id = 2,description = "This a Test demo")
    public void print2(){
        System.out.println("Test demo...");
    }
}

下面的是五种元注解,上面已经讲过了5个标准注解:

  • @Target: 表示注解可以用于哪些地方。可能的ElementType参数:
    • CONSTRUCTOR:构造器的声明
    • FIELD:字段声明(包括enum实例)
    • LOCAL_VARIABLE:局部变量声明
    • METHOD:方法声明
    • PACKAGE:包声明
    • PARAMETER:参数声明
    • TYPE:类、接口(包括注解类型)或者enum声明
  • @Retention表示注解信息保存的时长。可选的RetentionPolicy参数包括:
    • SOURCE:注解将被编译器丢弃
    • CLASS:注解在class文件中可用,但是会被VM丢弃。
    • RUNTIME:VM将在运行期也保留注解,因此可以通过反射机制读取注解的信息。
  • @Documented:将此注解保存在Javadoc中
  • Inherited:允许子类继承父类的注解
  • Repeatable:允许一个注解可以被使用一次或者多次(Java 8)。

注解处理器

根据名字我们就可以知道,这个是用于读取注解的工具。

一个编写的示例:

public class UseCaseTracker {
    public static void
    trackUseCases(List<Integer> useCases,Class<?> c1){
        for (Method m : c1.getDeclaredMethods()){
            UseCase us = m.getAnnotation(UseCase.class);
            if(us != null){
                System.out.println("Found Use Case "+us.id() + "\n" +us.description());
                useCases.remove(Integer.valueOf(us.id()));
            }
            useCases.forEach(i-> System.out.println("Missing use case"+i));
        }

    }
    public static void main(String[] args) {
        List<Integer> useCases = IntStream.range(1,3).boxed().collect(Collectors.toList());
        trackUseCases(useCases,AnnotationTest.class);
    }
}

输出:

Found Use Case 1
This a Test demo
Missing use case2
Found Use Case 2
This a Test demo

getDeclaredMethods()getAnnotation()是上面使用的两个反射方法,属于AnnotatedElement接口(Class,MethodField类都实现该接口)。getAnnotation()方法返回指定类型的注解对象。其后调用相应的方法(如id())返回值.

注解元素

有以下类型:

  • 基本类型(intfloat)
  • String
  • Class
  • enum
  • Annotation
  • 以上的数组

注意:使用其他类型,编译器会报错

默认值限制

根据规定,元素是不能够有不确定的值。也就是说元素需提供默认值或者显式给出。同时有:任何非基本类型的元素,无论在源代码声明时还是注解接口中定义默认值都不能使用null作为其值。由此可见,通过这个限制使得注解中所有元素都时存在的,并且有相对应的值。那么为了绕开这个约束,可以自行定义一些默认值。

生成外部文件

假设你想提供一些基本的对象/关系映射功能,能够自动生成数据库表。你可以使用 XML 描述文件来指明类的名字、每个成员以及数据库映射的相关信息。但是,通过使用注解,你可以把所有信息都保存在 JavaBean 源文件中。为此你需要一些用于定义数据库表名称、数据库列以及将 SQL 类型映射到属性的注解。

以下注解的定义用于告诉注解处理器创建一个数据库表:

package ink.kilig.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

package ink.kilig.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

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

上面的注解表示了一个数据库表的一些属性,同时也提供了一些默认值,就不需要使用者输入太多的东西。

一个使用了注解的类:

package ink.kilig.annotation;

@DBTable(name = "MEMBER") //提供表名
public class Member {
    //前面的注解表示了其被注解为xxx类型,并且提供了默认值,用于表示该列的大小
    @SQLString(30) String firstName;
    @SQLString(50) String lastName;
    @SQLInteger Integer age;
    @SQLString(value = 30,constraints = @Constraints(primaryKey = true))
    String reference; 

    @Override
    public String toString() {
        return reference;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public Integer getAge() {
        return age;
    }
    public String getReference() {
        return reference;
    }
}

因为上面reference其注解写得有些复杂了,所以我们需要有一些替代方案,使得看起来结果清晰易懂。

替代方案

  1. 可以使用一个单一的注解类,拥有一个enum元素,专门定义一些必须的类型。
  2. 可以使用一个String类型来描述实际的SQL类型
  3. 建议:将上面的一个注解分解为多个注解同时使用。

注解是不支持继承的

也许你会思考,上面的例子是如何将这些注解和实际的对象绑定到一起的?那么这时候我们就要重新回到上面所说的注解处理器。它就是专门把我们的相对应的注解结合对象转化为对数据库操作的SQL语句。下面是一个例子:

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

public class TableCreator {
    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<>();
            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];
                    // 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];
                    // 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.constraints()));
                }
                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) + ");";
                System.out.println("Table Creation SQL for " +
                        className + " is:\n" + 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;
    }
}

其输出:

Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
    FIRSTNAME VARCHAR(30));
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
    FIRSTNAME VARCHAR(30),
    LASTNAME VARCHAR(50));
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
    FIRSTNAME VARCHAR(30),
    LASTNAME VARCHAR(50),
    AGE INT);
Table Creation SQL for annotations.database.Member is:
CREATE TABLE MEMBER(
    FIRSTNAME VARCHAR(30),
    LASTNAME VARCHAR(50),
    AGE INT,
    REFERENCE VARCHAR(30) PRIMARY KEY);

至此,本文的内容已经结束,如果你还是意犹未尽,请移步参考文章内容,将会有关注解器的一个实现。

参考文章

on java 8