Java 注解详解


本文部分摘自 On Java 8


基本语法

注解是 Java 5 所引入的众多语言变化之一,是附加在代码中的一些元信息,用于一些工具在编译、运行时进行解析和使用,起到说明、配置的功能。注解不会也不能影响代码的实际逻辑,仅仅起到辅助性的作用,包含在 java.lang.annotation 包中

注解的语法十分简单,只要在现有语法中添加 @ 符号即可,java.lang 包提供了如下五种注解:

  • @Override

    表示当前的方法定义将覆盖基类的方法,如果你不小心把方法签名拼错了,编译器就会发出错误提示

  • @Deprecated

    如果使用该注解的元素被调用,编译器就会发出警告信息,表示不鼓励程序员使用

  • @SuppressWarnings

    关闭不当的编译器警告信息

  • @SafeVarargs

    禁止对具有泛型可变参数的方法或构造函数的调用方发出警告

  • @FunctionalInterface

    声明接口类型为函数式接口


定义注解

注解的定义看起来和接口的定义很像,事实上它们和其他 Java 接口一样,也会被编译成 class 文件

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

除开 @ 符号, @Test 的定义看起来更像一个空接口。注解的定义也需要一些元注解,元注解用于注解其他的注解

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

不包含任何元素的注解称为标记注解,上例中的 @Test 就是标记注解。注解通常也会包含一些表示特定值的元素,当分析处理注解的时候,程序可以利用这些值。注解的元素看起来就像接口的方法,但可以为其指定默认值

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

....

public class TestUtils {
// 在方法上使用注解 @TestAnnotation
@UseCase(id = 47, description = "description")
public void test() {
...
}
}

注解元素可用的类型如下所示,如果使用了其他类型,编译器就会报错:

  • 所有基本类型(int、float、boolean 等)
  • String
  • Class
  • enum
  • Annotation
  • 以上类型的数组

如果没有给出 description 的值,在分析处理这个类的时候会使用该元素的默认值。元素的默认值不能有不确定的值,也就是说,元素要么有默认值,要么就在使用注解时提供元素的值

这里还有另外一个限制:任何非基本类型的元素,无论是在源代码声明时还是在注解接口中定义默认值时,都不能使用 null 作为值。如果我们希望表现一个元素的存在或者缺失的状态,可以自定义一些特殊的值,比如空字符串或者负数用于表达某个元素不存在

注解不支持继承,你不能使用 extends 关键字来继承 @interface


注解处理器

如果没有用于读取注解的工具,那么注解不会比注释更有用。使用注解中一个很重要的作用就是创建与使用注解处理器。Java 拓展了反射机制的 API 用于帮助你创造这类工具。同时他还提供了 javac 编译器钩子在编译时使用注解

下面是一个非常简单的注解处理器,我们用它来读取被注解的 TestUtils 类,并且使用反射机制来寻找 @TestAnnotation 标记

public class TestAnnotationTracker {
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">trackTestAnnotation</span><span class="hljs-params">(Class&lt;?&gt; cl)</span> </span>{
    <span class="hljs-keyword">for</span>(Method m : cl.getDeclaredMethods()) {
        TestAnnotation ta = m.getAnnotation(TestAnnotation.class);
        <span class="hljs-keyword">if</span>(ta != <span class="hljs-keyword">null</span>) {
            System.out.println(ta.id() + <span class="hljs-string">"\n "</span> + ta.description());
        }
    }
}

<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> </span>{
    trackTestAnnotation(TestUtils.class);
}

}

这里用到了两个反射的方法:getDeclaredMethods()和 getAnnotation(),getAnnotation()方法返回指定类型的注解对象,在本例中就是 TestAnnotation,如果被注解的方法上没有该类型的注解,返回值就为 null。通过调用 id() 和 description() 方法来提取元素值


使用注解实现对象 - 数据库映射

当有些框架需要一些额外的信息才能与你的源代码协同工作,这种情况下注解就会变得十分有用。自定义例如对象 / 关系映射工具(Hibernate 和 MyBatis)通常都需要 XML 描述文件,而这些文件脱离于代码之外。除了定义 Java 类,程序员还必须重复的提供某些信息,而例如类名和包名等信息已经在原始类中提供过了,经常会导致代码和描述文件的同步问题

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

首先创建一个用来映射数据库表的注解,用来修饰类、接口(包括注解类型)或者 enum 声明

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

如下是修饰字段的注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
    boolean primaryKey() default false;
    boolean allowNull() default true;
    boolean unique() default false;
}
public @interface SQLString {
    int value() default 0;
    String name() default "";
    Constraints constraints() default @Constraints;
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
    String name() default "";
    Constraints constraints() default @Constraints;
}

@Constraints 代表了数据库通常提供的约束的一小部分,primaryKey(),allowNull() 和 unique() 元素都提供了默认值,大多数情况下,注解的使用者都不需要输入太多东西

另外两个 @interface 定义的是 SQL 类型。如果希望这个框架更有价值的话,我们应该为每个 SQL 类型都定义相应的注解。不过作为示例,两个元素足够了。这些 SQL 类型具有 name()元素和 constraints() 元素。后者利用了嵌套注解的功能,将数据库列的类型约束信息嵌入其中。注意 constraints()元素的默认值是 @Constraints,没有在括号中指明 @Constraints 元素的值,因此,constraints() 的默认值为所有元素都为默认值。如果要使得嵌入的 @Constraints 注解中的 unique()元素为 true,并作为 constraints() 元素的默认值,你可以像如下定义:

public @interface Uniqueness {
    Constraints constraints() default @Constraints(unique = true);
}

下面是一个简单的,使用了如上注解的类

@DBTable(name = "MEMBER")
public class Member {
    @SQLString(30)
    String firstName;
    @SQLString(50)
    String lastName;
    @SQLInteger
    Integer age;
    @SQLString(value = 30, 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; }
}

类注解 @DBTable 注解给定了元素值 MEMBER,它将会作为表的名字。类的属性 firstName 和 lastName 都被注解为 @SQLString 类型并且给了默认元素值分别为 30 和 50,并在嵌入的 @Constraint 注解中设定 primaryKey 元素的值

下面是一个注解处理器的例子,它将读取一个类文件,检查上面的数据库注解,并生成用于创建数据库的 SQL 命令:

public class TableCreator {
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">generateSql</span><span class="hljs-params">(String[] classnames)</span> <span class="hljs-keyword">throws</span> Exception </span>{
    <span class="hljs-keyword">for</span> (String className : classnames) {
        Class&lt;?&gt; cl = Class.forName(className);
        DBTable dbTable = cl.getAnnotation(DBTable.class);
        String tableName = dbTable.name();
        <span class="hljs-comment">// 如果表名为空字符串,则使用类名</span>
        <span class="hljs-keyword">if</span> (tableName.length() &lt; <span class="hljs-number">1</span>) {
            tableName = cl.getName().toUpperCase();
        }
        List&lt;String&gt; columnDefs = <span class="hljs-keyword">new</span> ArrayList&lt;&gt;();
        <span class="hljs-keyword">for</span> (Field field : cl.getDeclaredFields()) {
            String columnName = <span class="hljs-keyword">null</span>;
            Annotation[] anns = field.getDeclaredAnnotations();
            <span class="hljs-comment">// 该属性不是列</span>
            <span class="hljs-keyword">if</span> (anns.length &lt; <span class="hljs-number">1</span>) {
                <span class="hljs-keyword">continue</span>;
            }
            <span class="hljs-comment">// 处理整数类型</span>
            <span class="hljs-keyword">if</span> (anns[<span class="hljs-number">0</span>] <span class="hljs-keyword">instanceof</span> SQLInteger) {
                SQLInteger sInt = (SQLInteger) anns[<span class="hljs-number">0</span>];
                <span class="hljs-comment">// 如果列名为空字符串,则使用属性名</span>
                <span class="hljs-keyword">if</span> (sInt.name().length() &lt; <span class="hljs-number">1</span>) {
                    columnName = field.getName().toUpperCase();
                } <span class="hljs-keyword">else</span> {
                    columnName = sInt.name();
                }
                columnDefs.add(columnName + <span class="hljs-string">" INT"</span> + getConstraints(sInt.constraints()));
            }
            <span class="hljs-comment">// 处理字符串类型</span>
            <span class="hljs-keyword">if</span> (anns[<span class="hljs-number">0</span>] <span class="hljs-keyword">instanceof</span> SQLString) {
                SQLString sString = (SQLString) anns[<span class="hljs-number">0</span>];
                <span class="hljs-keyword">if</span> (sString.name().length() &lt; <span class="hljs-number">1</span>) {
                    columnName = field.getName().toUpperCase();
                } <span class="hljs-keyword">else</span> {
                    columnName = sString.name();
                }
                columnDefs.add(columnName + <span class="hljs-string">" VARCHAR("</span> + sString.value() + <span class="hljs-string">")"</span> +
                        getConstraints(sString.constraints()));
            }
            <span class="hljs-comment">// 构造并输出 sql 字符串</span>
            StringBuilder createCommand = <span class="hljs-keyword">new</span> StringBuilder(<span class="hljs-string">"CREATE TABLE "</span> + tableName + <span class="hljs-string">"("</span>);
            <span class="hljs-keyword">for</span> (String columnDef : columnDefs) {
                createCommand.append(<span class="hljs-string">"\n "</span> + columnDef + <span class="hljs-string">","</span>);
            }
            String tableCreate = createCommand.substring(<span class="hljs-number">0</span>, createCommand.length() - <span class="hljs-number">1</span>) + <span class="hljs-string">");"</span>;
            System.out.println(<span class="hljs-string">"Table Creation SQL for "</span> + className + <span class="hljs-string">" is:\n"</span> + tableCreate);
        }
    }
}

<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> String <span class="hljs-title">getConstraints</span><span class="hljs-params">(Constraints con)</span> </span>{
    String constraints = <span class="hljs-string">""</span>;
    <span class="hljs-keyword">if</span> (!con.allowNull())
        constraints += <span class="hljs-string">" NOT NULL"</span>;
    <span class="hljs-keyword">if</span> (con.primaryKey())
        constraints += <span class="hljs-string">" PRIMARY KEY"</span>;
    <span class="hljs-keyword">if</span> (con.unique())
        constraints += <span class="hljs-string">" UNIQUE"</span>;
    <span class="hljs-keyword">return</span> constraints;
}

}


编译时注解处理

当 @Retention 的 RetentionPolicy 参数被标注为 SOURCE 或 CLASS,此时你无法通过反射去获取注解信息,因为注解在运行期是不存在的。使用 javac 可以创建编译时注解处理器,在编译时扫描和处理注解。你可以自定义注解,并注册到对应的注解处理器。注解处理器可以生成 Java 代码,这些生成的 Java 代码会组成新的 Java 源文件,但不能修改已经存在的 Java 类,例如向已有的类中添加方法。如果你的注解处理器创建了新的源文件,在新一轮处理中注解处理器会检查源文件本身,在检测一轮之后持续循环,直到不再有新的源文件产生,然后编译所有的源文件

我们来编写一个简单的注解处理器,如下是注解的定义

@Retention(RetentionPolicy.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-";
}

@Retention 的参数为 SOURCE,这意味着注解不会存留在编译后的 class 文件,因为这对应编译时处理注解是没有必要的,在这里,javac 是唯一有机会处理注解的方式

package annotations.simplest;
@Simple
public class SimpleTest {
    @Simple
    int i;
    @Simple
    public SimpleTest() {}
    @Simple
    public void foo() {
        System.out.println("SimpleTest.foo()");}
    @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();}
}

运行 main 方法,程序就会开始编译,如下是一个简单的处理器,作用就是把注解相关的信息打印出来

package annotations.simplest;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import java.util.*;
@SupportedAnnotationTypes("annotations.simplest.Simple")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class SimpleProcessor extends AbstractProcessor {
<span class="hljs-meta">@Override</span>
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">process</span><span class="hljs-params">(Set&lt;? extends TypeElement&gt; annotations,
	RoundEnvironment env)</span> </span>{
    <span class="hljs-keyword">for</span>(TypeElement t : annotations) {
        System.out.println(t);
    }
    <span class="hljs-keyword">for</span>(Element el : env.getElementsAnnotatedWith(Simple.class)) {
        display(el);
    }
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;
}

<span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">display</span><span class="hljs-params">(Element el)</span> </span>{
    System.out.println(<span class="hljs-string">"==== "</span> + el + <span class="hljs-string">" ===="</span>);
    System.out.println(el.getKind() +	<span class="hljs-comment">// 返回此元素的种类,字段,方法,或是类</span>
            <span class="hljs-string">" : "</span> + el.getModifiers() +	<span class="hljs-comment">// 返回此元素的修饰符</span>
            <span class="hljs-string">" : "</span> + el.getSimpleName() +	<span class="hljs-comment">// 返回此元素的简单名称</span>
            <span class="hljs-string">" : "</span> + el.asType());	<span class="hljs-comment">// 返回此元素定义的类型</span>
    <span class="hljs-comment">// 如果元素为CLASS类型,动态向下转型为更具体的元素类型,并打印相关信息</span>
    <span class="hljs-keyword">if</span>(el.getKind().equals(ElementKind.CLASS)) {
        TypeElement te = (TypeElement)el;
        System.out.println(te.getQualifiedName());
        System.out.println(te.getSuperclass());
        System.out.println(te.getEnclosedElements());
    }
    <span class="hljs-comment">// 如果元素为METHOD类型,动态向下转型为更具体的元素类型,并打印相关信息</span>
    <span class="hljs-keyword">if</span>(el.getKind().equals(ElementKind.METHOD)) {
        ExecutableElement ex = (ExecutableElement)el;
        System.out.print(ex.getReturnType() + <span class="hljs-string">" "</span>);
        System.out.print(ex.getSimpleName() + <span class="hljs-string">"("</span>);
        System.out.println(ex.getParameters() + <span class="hljs-string">")"</span>);
    }
}

}

使用 @SupportedAnnotationTypes 和 @SupportedSourceVersion 注解来确定支持哪些注解以及支持的 Java 版本

注解处理器需要继承抽象类 javax.annotation.processing.AbstractProcessor,唯一需要实现的方法就是 process(),这里是所有行为发生的地方。第一个参数获取到此注解处理器所要处理的注解集合,第二个参数保留了剩余信息,这里我们所做的事情只是打印了注解(只存在一个)。process() 中实现的第二个操作是循环所有被 @Simple 注解的元素,并且针对每一个元素调用 display() 方法。展示所有 Element 自身的基本信息



__EOF__

  • 本文作者: 低吟不作语
  • 本文链接: https://www.cnblogs.com/Yee-Q/p/14041933.html
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。