注解的定义以及注解编译器

注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化的方式,使我们可以在稍后的某个时刻更容易的使用这些数据。
注解在一定程度上是把元数据和源代码文件结合在一起的,而不是保存在外部文档。
注解是 Java 5 所引入的,它提供了 Java 无法表达的但是你需要完整表述程序所需的额外信息。这种信息是以编译器验证的格式来校验的。注解可以生成描述符文件,甚至是新的类定义,并且有助于减轻编写“样板”代码的负担。
通过使用注解,你可以将元数据保存在Java 源代码中,并拥有如下优势:简单易读的代码,编译器类型检查,使用 Annotation API 为自己的注解构造处理工具。即使 Java 定义了一些类型的元数据,但是一般来说注解类型的添加和如何使用完全取决于你。
注解是真正语言层级的概念,构造出来就享有编译器的类型检查保护。注解在源代码级别保存所有信息而不是通过注释文字,这使得代码更加简洁和便于维护。

注解的语法十分简单,主要是在现有语法中添加 @ 符号,Java 5 引用了前三种定义在 java.lang 包中的注解:

  • @Override:表示当前的方法定义将覆盖基类的方法。
  • @Deprecated:如果使用该注解元素被调用,编译器就会发出警告信息。
  • @SuppressWarnings:关闭不当的编译器警告信息。
  • @SafeVarargs:在 Java 7 中加入用于禁止对具有泛型 varargs 参数的方法或构造函数的调用方法发出警告。
  • @FunctionalInterface:Java 8 中加入用于表示类型声明为函数式接口。

除此之外,还有 5 中额外的注解类型用于创造新的注解。下面将会描述。
每当创建涉及重复工作的类或接口时,你通常都可以使用注解来自动化和简化流程。例如在 Enterprise JavaBean(EJB)中的许多额外工作就是通过注解来消除的。

1. 基本语法

1.1 定义注解

如下是一个注解的定义。它和其他的 Java 接口一样,也会编译成 class 文件。

@Target(ElementType.METHOD)//定义的注解可以应用在哪里(比如方法还是字段)
@Retention(RetentionPolicy.RUNTIME)//定义了注解在哪里可用,在源代码中(SOURCE),class文件(CLASS)或者是运行时(RUNTIME)
public @interface Test {
}

除了@符号外,@Test的定义看起来更像一个空接口。注解的定义需要一些元注解(meta-annotation),比如@TargetRetention
注解通常会包含一些表示特定值的元素。当分析处理注解的时候,程序或工具可以利用这些值。注解的元素看起来像是接口的方法,但是可以为其指定初始值。
而不包含任何元素的注解就是标记注解(marker annotation),如上例 @Test

下面是一个简单的注解,我们可以用它来追踪项目中的用例。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
    int id();
		//如果注解某个方法时没有给出description的值,则该注解的处理器会使用此元素的默认值
    String description() default "no description";
}

程序员可以用该注解标注满足特定用例的一个方法或者一组方法。于是,项目经理可以通过统计已经实现这个注解的用例来掌控项目的进度,而开发者在维护项目时可以轻松的找到用例用于更新或是可以调试系统中的业务逻辑
id 和 description 与方法定义类似。由于编译器会对 id 进行类型检查因此将跟踪数据库与用例文档和源代码相关联是可靠的方式。
下面的类中,有三个方法被注解为用例:

public class PasswordUtils {
    @UseCase(id = 47,description = "Password must contain at least one numeric")
    public boolean validatePassword(String passwd) {
        return (passwd.matches("\\w*\\d\\w"));
    }

    @UseCase(id = 48)
    public String encryptPassword(String passwd) {
        return new StringBuilder(passwd).reverse().toString();
    }

    @UseCase(id = 49, description = "New password can't equal previously used ones")
    public boolean checkForNewPassword(List<String> prePasswords,String passwd) {
        return !prePasswords.contains(passwd);
    }
}

注解的元素在使用的时候表现为 名-值 对的形式,并且需要放置在@UseCase声明之后的括号内。

1.2 元注解

Java 有 5 种标准注解(前面总结的),以及 5 种元注解(meta-annotation):
  元注解的作用就是负责注解其他注解。Java5.0定义了4个标准的meta-annotation类型,它们被用来提供对其它 annotation类型作说明。Java5.0定义的元注解:
    1. @Target,
    2. @Retention,
    3. @Documented,
    4. @Inherited
    5. @Repeatable
@Target:
   @Target说明了Annotation所修饰的对象范围:Annotation可被用于 packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数)。
  作用:用于描述注解的使用范围(即:被描述的注解可以用在什么地方)
  取值(ElementType)有:
    1. CONSTRUCTOR:用于描述构造器;
    2. FIELD:用于字段声明(包括 enum 实例);
    3. LOCAL_VARIABLE:用于描述局部变量;
    4. METHOD:用于描述方法;
    5. PACKAGE:用于描述包;
    6. PARAMETER:用于描述参数;
    7. TYPE:类、接口(包括注解类型) 或enum声明。

@Retention:
  @Retention定义了注解信息被保留的时间长短:某些Annotation仅出现在源代码中,而被编译器丢弃;而另一些却被编译在class文件中;编译在class文件中的Annotation可能会被虚拟机忽略,而另一些在class被装载时将被读取(请注意并不影响class的执行,因为Annotation与class在使用上是被分离的)。使用这个meta-Annotation可以对 Annotation的“生命周期”限制。
  作用:表示需要在什么级别保存该注释信息,用于描述注解的生命周期(即:被描述的注解在什么范围内有效)
  取值(RetentionPoicy)有:
    1.SOURCE:在源文件中有效(即源文件保留),注解将被编译器丢弃;
    2.CLASS:在class文件中有效(即class保留)但是会被 VM 丢弃;
    3.RUNTIME:VM 在运行时有效(即运行时保留),因此可以通过反射机制读取注解的信息
@Documented:将此注解保存在 Javadoc 中。
@Inherited:允许子类继承父类的注解。
@Repeatable:允许一个注解可以被使用一次或者多次。

大多数时候,程序员定义自己的注解,并编写自己的注解处理器。

使用@interface自定义注解时,自动继承了java.lang.annotation.Annotation接口

  • @interface用来声明一个注解,格式:public @interface 注解名{定义内容}
  • 其中的每一个方法实际上是声明了一个配置参数
  • 方法的参数就是参数的名称
  • 返回值类型就是参数的类型(返回值只能是基本类型,Class,String,enum)
  • 可以通过default来声明参数的默认值
  • 如果只有一个参数成员,一般参数名为value

2. 编写注解处理器

如果没有用于读取注解的工具,那么注解就和注释一样了。使用注解中一个很重要的部分就是,创建与使用注解处理器。Java 拓展了反射机制的 API 用于帮助你创建这类工具。同时它还提供了 javac 编译器在编译时使用注解。
下面是一个简单的注解处理器。

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

    public static void main(String[] args) {
        List<Integer> collect = IntStream.range(47, 51)
                .boxed().collect(Collectors.toList());
        trackUseCases(collect, PasswordUtils.class);
    }
}
/*
outputs:
Found Use Case 48
no description
Found Use Case 47
Password must contain at least one numeric
Found Use Case 49
New password can't equal previously used ones
Missing use case 50
 */

2.1 默认值限制

编译器对于元素的默认值有些过于挑剔。首先,元素不能有不确定的值,也就是说,元素要么有值,要么就使用注解提供的值。
在定义默认值时,当表示一个元素缺失或不存在的状态时,可以自定义一些特殊值,如空字符串或负数表示。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SimulatingNull {
    int id() default -1;

    String description() default "";
}

2.2 生成外部文件

当有些框架需要一些额外的信息才能与你的源代码协同工作,这种情况下注解就会变得十分有用。
像 Enterprise JavaBeans(EJB3之前)这样的技术,每一个 Bean 都需要大量的接口和部署描述文件。网络服务、自定义标签库以及对象/关系映射工具(如 Hibernate)通常都需要 XML 描述文件,而这些文件都脱离于代码之外。
除了定义 Java 类,程序员还必须提供某些信息,例如类名和包名这些已经提供过的信息。每当你使用外部描述文件时,它就拥有了一个类的两个独立信息源,这经常导致代码的同步问题。同时也要求程序员在知道如何编写代码的同时,也必须知道如何描述文件。
假如你想提供一些基本的对象/关系映射功能,能够自动生成数据库表。你可以使用 XML 描述文件来指明类的名字,每个成员以及数据库映射的相关信息。但是,通过使用注解,你可以把所有信息都保存在 JavaBean 源文件中。为此,你需要一些用于定义数据库表名称、数据库列以及将 SQL 类型映射到属性的注解。
以下是一个注解的定义,它告诉注解处理器应该创建一个数据库表:

@Target(ElementType.TYPE) //这个自定义的注解只能用于指定的类型,可以指定 enum ElementType 中的一个,或用逗号分割的形式指定多个值。
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
    /**
     * 通过这个元素为处理器创建数据库时提供表的名字
     * @return 表名
     */
    String name() default "";
}

以下是修饰字段的注解:

/**
 * 该注解允许处理器提供数据库表的元数据,它代表了数据库通常提供的约束的一小部分,但是它所要表达的思想已经很清楚了
 * @Date 2022/1/12 10:03
 * @Created by gt136
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
    boolean primaryKey() default false;

    boolean allowNull() default true;

    boolean unique() default false;
}

/*********************************************************/
/**
 * SQLString 定义的是 SQL 类型,如果希望这个框架有价值的话,我们应该为每个 SQL 类型都定义相应的注解。这里两个(SQLString、SQLInteger)只是作为示例
 * @Date 2022/1/12 10:08
 * @Created by gt136
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
    int value() default 0;

    String name() default "";

    /**
     * 运用了嵌套注解的功能,将数据库列的类型约束信息嵌入其中
     * 注意:constraints() 的默认值是{@link @Constraints} 
     * 由于在 Constraints 注解类型后,没有在括号中指明 {@link @Constraints}元素的值,因此值为默认值
     * @return Constraints 的默认注解
     */
    Constraints constraints() default @Constraints;
}

/*********************************************************/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
    String name() default "";

    Constraints constraints() default @Constraints;
}

这些 SQL 类型都有 name()constraints()元素。后者使用了嵌套注解。constraints() 的默认值是 @Constraints。如果要修改它的值,你可以像下面这样定义:

public @interface Uniqueness {
    //通过像如下定义 constraints() 的值
    Constraints constraints() default @Constraints(unique = true);
}

下面的是简单的使用了如上注解的类:

/**
 * 根据注解创建数据库表
 * @Date 2022/1/12 10:56
 * @Created by gt136
 */
@DBTable(name = "Member") //表名为 Member
public class Member {
    @SQLString(30) //设置值为 30
    String firstName;
    @SQLString(50) //
    String lastName;
    @SQLInteger
    Integer age;
		//主键
    @SQLString(value = 30, name = "", constraints = @Constraints(primaryKey = true))
    String reference;
    static int memberCount;
    public String getReference() { return reference; }
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    @Override
    public String toString() { return reference; }
    public Integer getAge() { return age; }
}

这些注解都有两个有趣的地方:首先,它们都使用了嵌入的 @Constraints 注解的默认值,其次,它们都应用了注解的快捷方式特性。那就是如果你在注解中定义了名为 value 的元素,并且在使用该注解时,value 值是你唯一需要赋值的元素,你就不需要使用 名——值 对赋值的语法,直接给值就可。

2.3 实现处理器

下面是一个注解处理器的例子:

/**
 * 基于反射的注解处理器
 * @Date 2022/1/12 15:04
 * @Created by gt136
 */
public class TableCreator {
    public static void main(String[] args) throws ClassNotFoundException {
        if (args.length < 1) {
            System.out.println("arguments: annotated classes");
            System.exit(0);
        }

        for (String className : args) {
            Class<?> aClass = Class.forName(className);
            DBTable dbTable = aClass.getAnnotation(DBTable.class);
            if (dbTable == null) {
                System.out.println("No DBTable annotations in class" + className);
                continue;
            }
            String tableName = dbTable.name();

            /**
             * 如果没有定义表名,则将类名作为表名
             */
            if (tableName.length() < 1) {
                tableName = aClass.getName().toUpperCase();
            }

            List<String> columDefs = new ArrayList<>();
            //遍历字段
            for (Field declaredField : aClass.getDeclaredFields()) {
                String columnName;
                //获取字段上的注解
                Annotation[] annos = declaredField.getDeclaredAnnotations();

                if (annos.length < 1) {
                    //没有数据库表的列,直接跳过此字段
                    continue;
                }
                
                //如果是 SQLInteger 注解类型
                if (annos[0] instanceof SQLInteger) {
                    SQLInteger sInt = (SQLInteger) annos[0];
                    //
                    if (sInt.name().length() < 1) {
                        columnName = declaredField.getName().toUpperCase();
                    }else {
                        columnName = sInt.name();
                    }
                    columDefs.add(columnName + " INT" + getConstraints(sInt.constraints()));
                }

                //如果是 SQLString 注解类型
                if (annos[0] instanceof SQLString) {
                    SQLString sString = (SQLString) annos[0];
                    //
                    if (sString.name().length() < 1) {
                        columnName = declaredField.getName().toUpperCase();
                    }else {
                        columnName = sString.name();
                    }
                    columDefs.add(columnName + " VARCHAR(" + sString.value() + ")" + 
                            getConstraints(sString.constraints()));
                }
                //下面这两段移除循环外直接生成最后结果
                StringBuilder createCommand = new StringBuilder("CREATE TABLE " + tableName + "(");
                for (String columDef : columDefs) {
                    createCommand.append("\n " + columDef + ",");
                }
                //移除多余的空
                String tableCreate = createCommand.substring(0, createCommand.length() - 1) + ");";
                System.out.println("Table Creation SQL for " + className + " is:\n" + tableCreate);
            }
        }
    }

    private static String getConstraints(Constraints constraints) {
        String cons = "";
        if (!constraints.allowNull()) {
            cons += " NOT NULL";
        }
        if (constraints.primaryKey()) {
            cons += " PRIMARY KEY";
        }
        if (constraints.unique()) {
            cons += " UNIQUE";
        }
        return cons;
    }
}
/*
outputs:
Table Creation SQL for com.gui.demo.thingInJava.annotation.database.Member is:
CREATE TABLE Member(
 FIRSTNAME VARCHAR(30));
Table Creation SQL for com.gui.demo.thingInJava.annotation.database.Member is:
CREATE TABLE Member(
 FIRSTNAME VARCHAR(30),
 LASTNAME VARCHAR(50));
Table Creation SQL for com.gui.demo.thingInJava.annotation.database.Member is:
CREATE TABLE Member(
 FIRSTNAME VARCHAR(30),
 LASTNAME VARCHAR(50),
 AGE INT);
Table Creation SQL for com.gui.demo.thingInJava.annotation.database.Member is:
CREATE TABLE Member(
 FIRSTNAME VARCHAR(30),
 LASTNAME VARCHAR(50),
 AGE INT,
 REFERENCE VARCHAR(30) NOT NULL PRIMARY KEY);
 */

嵌套的 @Constraint 注解被传递给getConstraints()方法,并用它来构造一个包含 SQL 约束的 String 对象。
上面的演示技巧对于真实的对象/映射 关系而言,不太符合。

3. 使用 javac 处理注解

通过 javac ,你可以通过创建编译时(compile-time)_注解处理器_在 java 源文件上使用注解,而不是编译后的 class 文件。
但是你不能通过处理器来改变源代码,唯一影响输出的方式就是创建新的文件。如果你的注解处理器创建了新的源文件,在新一轮处理中注解会检查源文件本身。工具在检测一轮之后持续循环,直到不再有新的源文件产生,然后它编译所有源文件。
每一个你编写的注解都需要处理器,但是 javac 可以非常容易的将多个注解处理器合并在一起。你可以指定多个需要处理的类,并且可以添加监听器用于监听注解处理完成后接到通知。

3.1 最简单的处理器

@Retention(
        RetentionPolicy.SOURCE //retention 的参数现在为 SOURCE,这意味着注解不会在存留在编译后的代码
)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR,
        ElementType.ANNOTATION_TYPE, ElementType.PACKAGE,
        ElementType.FIELD, ElementType.LOCAL_VARIABLE})
public @interface Simple {
    String value() default "-default-";
}

/*====================================*/

@Simple
public class SimpleTest {
    @Simple
    int i;

    @Simple
    public SimpleTest() {
    }
    
    @Simple
    public void foo() {
        System.out.println("SimpleTest.fool()");
    }

    @Simple
    public void bar(String s, int i, float f) {
        System.out.println("SimpleTest.bar()");
    }

    @Simple
    public static void main(String[] args) {
        @Simple
        SimpleTest st = new SimpleTest();
        st.foo();
    }
}


@Simple
public class SimpleTest {
    @Simple
    int i;

    @Simple
    public SimpleTest() {
    }
    
    @Simple
    public void foo() {
        System.out.println("SimpleTest.fool()");
    }

    @Simple
    public void bar(String s, int i, float f) {
        System.out.println("SimpleTest.bar()");
    }

    @Simple
    public static void main(String[] args) {
        @Simple
        SimpleTest st = new SimpleTest();
        st.foo();
    }
}


@SupportedAnnotationTypes("com.gui.demo.thingInJava.annotation.simplelist.Simple") //确定支持那些注解
@SupportedSourceVersion(SourceVersion.RELEASE_8) //支持的 Java 版本
public class SimpleProcessor extends AbstractProcessor {

    /**
     * 唯一需要实现的就是 process(),这是所有行为发生的地方。第一个参数告诉我们哪个注解是存在的,第二个参数保存了剩余的信息。
     * <p>for 循环打印注解</p>
     * @param annotations 哪个注解存在
     * @param roundEnv 剩余信息
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //打印注解
        for (TypeElement t : annotations) {
            System.out.println(t);
        }
        //循环所有被 @Simple 注解的元素,并针对每一个元素调用 display() 
        for (Element element : roundEnv.getElementsAnnotatedWith(Simple.class)) {
            display(element);
        }   
        return false;
    }

    /**
     * 每一个元素展示了自身的基本信息
     * @param element 被 Simple 注解的元素
     */
    private void display(Element element) {
        System.out.println("==== " + element + " ===");
        System.out.println(element.getKind() + " : " + 
                element.getModifiers() + " : " + 
                element.getSimpleName() + "" + 
                element.asType());
        
        if (element.getKind().equals(ElementKind.CLASS)) {
            TypeElement el = (TypeElement) element;
            System.out.println(el.getQualifiedName());
            System.out.println(el.getSuperclass());
            System.out.println(el.getEnclosedElements());
        }

        if (element.getKind().equals(ElementKind.METHOD)) {
            ExecutableElement ex = (ExecutableElement) element;
            System.out.print(ex.getReturnType() + " ");
            System.out.print(ex.getSimpleName() + "(");
            System.out.println(ex.getParameters()+")");
        }
    }
}
/*
outputs:
annotations.simplest.Simple
==== annotations.simplest.SimpleTest ====
CLASS : [public] : SimpleTest : annotations.simplest.SimpleTest
annotations.simplest.SimpleTest
java.lang.Object
i,SimpleTest(),foo(),bar(java.lang.String,int,float),main(java.lang.String[])
==== i ====
FIELD : [] : i : int
==== SimpleTest() ====
CONSTRUCTOR : [public] : <init> : ()void
==== foo() ====
METHOD : [public] : foo : ()void
void foo()
==== bar(java.lang.String,int,float) ====
METHOD : [public] : bar : (java.lang.String,int,float)void
void bar(s,i,f)
==== main(java.lang.String[]) ====
METHOD : [public, static] : main : (java.lang.String[])void
void main(args)
 */

Element 只能执行那些编译器解析的所有基本对象共有的操作,而类和方法之类的东西有额外的信息需要提取。所以你检查它是那种 ElementKind,然后向下转型为更具体的元素类型,注入针对 CLASS 的TypeElement 和针对 METHOD 的ExecutableElement。此时,就可以为这些元素调用其他方法。
动态向下转型(在编译器不受检查)并不像是 Java 的做事风格。

3.2 更复杂的处理器

当你创建用于 javac 的注解处理器时,你不能用 Java 的反射特性,因为你处理的是源代码,而并非编译后的 class 文件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值