一起来学Java注解(Annotation)

一. 什么是 Annotation

我们在平时的开发过程中看到很多如 @Override,@SuppressWarnings,@Test 等样式的代码就是注解,注解是放到类、构造器、方法、属性、参数前的标记。

二. Annotation 的作用

给某个类、方法.. 添加了一个注解,这个环节仅仅是做了一个标记,对代码本身并不会造成任何影响,需要后续环节的配合,需要其他方法对该注解赋予业务逻辑处理。就如同我们在微信上发了一个共享定位,此时并没有什么用,只有当后面其他人都进入了这个共享定位,大家之间的距离才能明确,才知道该怎么聚在一起。

注解分为三类:

2.1 编译器使用到的注解

如 @Override,@SuppressWarnings 都是编译器使用到的注解,作用是告诉编译器一些事情,而不会进入编译后的.class 文件。

@Override:告诉编译器检查一下是否重写了父类的方法;

@SuppressWarnings:告诉编译器忽略该段代码产生的警告;

对于开发人员来说,都是直接使用,无需进行其他操作

2.2 .class 文件使用到的注解

需要通过工具对.class 字节码文件进行修改的一些注解,某些工具会在类加载的时候,动态修改用某注解标注的.class 文件,从而实现一些特殊的功能,一次性处理完成后,并不会存在于内存中,都是非常底层的工具库、框架会使用,对于开发人员来说,一般不会涉及到。

2.3 运行期读取的注解

一直存在于 JVM 中,在运行期间可以读取的注解,也是最常用的注解,如 Spring 的 @Controller,@Service,@Repository,@AutoWired,Mybatis 的 @Mapper,Junit 的 @Test 等,这类注解很多都是工具框架自定义在运行期间发挥特殊作用的注解,一般开发人员也可以自定义这类注解。

三. 定义 Annotation

我们使用 @interface 来定义一个注解

/**
 * 定义一个 Table 注解
 */
public @interface Table {
    String value() default "";
}
/**
 * 定义一个 Colum 注解
 */
public @interface Colum {
    String value() default "";
    String name() default "";
    String dictType() default "";
}

这样就简单地将一个注解定义好了

我们上面定义的注解主要用到了 String 类型,但实际上还可以是基本数据类型(不能为包装类)、枚举类型。

注解也有一个约定俗成的东西,最常用的参数应该命名为 value,同时一般情况下我们都会通过 default 参数设置一个默认值。

但这样是不是就满足于我们的使用了呢,我想把@Table注解仅用于类上,@Colum注解仅用于属性上,怎么办?而且开始提到的三类注解,一般开发人员用的都是运行期的注解,那我们定义的是吗?

要回答这些问题,就需要引入一个概念“元注解”。

3.1 元注解

可以修饰注解的注解即为元注解,Java 已经定义了一些元注解,我们可以直接使用。

3.1.1 @Target

顾名思义指定注解使用的目标对象,参数为 ElementType[]

public @interface Target {
    /**
     * Returns an array of the kinds of elements an annotation type
     * can be applied to.
     * @return an array of the kinds of elements an annotation type
     * can be applied to
     */
    ElementType[] value();
}

而下面是 ElementType 枚举中定义的属性,不设置 Target 的时候,除了 TYPE_PARAMETER,TYPE_USE,其他地方都相当于配置上了。

public enum ElementType {
    /** 通过 ElementType.TYPE 可以修饰类、接口、枚举 */
    TYPE,
<span class="hljs-comment">/** 通过ElementType.FIELD可以修饰类属性 */</span>
FIELD,

<span class="hljs-comment">/** 通过ElementType.METHOD可以修饰方法 */</span>
METHOD,

<span class="hljs-comment">/** 通过ElementType.PARAMETER可以修饰参数(如构造器或者方法中的) */</span>
PARAMETER,

<span class="hljs-comment">/** 通过ElementType.CONSTRUCTOR可以修改构造器 */</span>
CONSTRUCTOR,

<span class="hljs-comment">/** 通过ElementType.LOCAL_VARIABLE可以修饰方法内部的局部变量 */</span>
LOCAL_VARIABLE,

<span class="hljs-comment">/** 通过ElementType.ANNOTATION_TYPE可以修饰注解 */</span>
ANNOTATION_TYPE,

<span class="hljs-comment">/** 通过ElementType.PACKAGE可以修饰包 */</span>
PACKAGE,

<span class="hljs-comment">/**
 * 可以用在Type的声明式前
 *
 * <span class="hljs-doctag">@since</span> 1.8
 */</span>
TYPE_PARAMETER,

<span class="hljs-comment">/**
 * 可以用在所有使用Type的地方(如泛型、类型转换等)
 *
 * <span class="hljs-doctag">@since</span> 1.8
 */</span>
TYPE_USE

}

我们主要说一下 ElementType.PACKAGE 和 1.8 添加的 ElementType.TYPE_PARAMETER 和 ElementType.TYPE_USE

ElementType.PACKAGE

@Target(ElementType.PACKAGE)
public @interface Table {
    String value() default "";
}

含义是用来修饰包,但我们用来修饰包的时候却提示错误

我们按照提示创建 package-info.java 文件,这里需要注意一下,通过 IDE 进行 new --> Java Class 是创建不了的,需要通过 new File 文件创建

@Table
package annotation;
class PackageInfo {
    public void hello() {
        System.out.println("hello");
    }
}

ElementType.TYPE_PARAMETER 和 ElementType.TYPE_USE

这两个一起说,因为它们有相似之处。都是 Java1.8 后添加的

@Target(ElementType.TYPE_USE)
public @interface NoneEmpty {
    String value() default "";
}
@Target(ElementType.TYPE_PARAMETER)
public @interface NoneBlank {
    String value() default "";
}

很明显使用 ElementType.TYPE_PARMETER 修饰的注解 @NoneBlank 无法在泛型使用的时候编译通过,仅能用于类的泛型声明,而通过 ElementType.TYPE_USE 修饰的注解 @NoneEmpty 可以。

3.1.2 @Retention

可以用于定义注解的生命周期,参数为枚举 RetentionPolicy,包括了 SOURCE,CLASS,RUNTIME

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();
}
public enum RetentionPolicy {
    /**
     * 仅存在于源代码中,编译阶段会被丢弃,不会包含于 class 字节码文件中.
     */
    SOURCE,
<span class="hljs-comment">/**
 * 【默认策略】,在class字节码文件中存在,在类加载的时被丢弃,运行时无法获取到
 */</span>
CLASS,

<span class="hljs-comment">/**
 * 始终不会丢弃,可以使用反射获得该注解的信息。自定义的注解最常用的使用方式。
 */</span>
RUNTIME

}

3.1.3 @Documented

表示是否将此注解的相关信息添加到 javadoc 文档中

3.1.4 @Inherited

定义该注解和子类的关系,使用此注解声明出来的自定义注解,在使用在类上面时,子类会自动继承此注解,否则,子类不会继承此注解。注意,使用 @Inherited 声明出来的注解,只有在类上使用时才会有效,对方法,属性等其他无效。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Person {
    String value() default "man";
}
@Person
public class Parent {
}
// 子类也拥有 @Person 注解
class Son extends Parent {

}

3.2 定义注解小结

用 @interface 定义注解

可以添加多个参数,核心参数按约定用 value,为每个参数可以设置默认值,参数类型包括基本类型、String 和枚举

可以使用元注解来修饰注解,元注解包括多个,必须设置@Target@Retention@Retention一般设置为RUNTIME

四. Annotation 处理

我们前面已经提到光配置了注解,其实没有作用,需要通过相应的代码来实现该注解想要表达的逻辑。

注解定义后也是一种 class,所有的注解都继承自java.lang.annotation.Annotation,因此,读取注解,需要使用反射 API。

// 定义的注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Colum {
    String value() default "";
    // 用于表示某个属性代表的中文含义
    String name() default "";
}

用注解 @Colum 来修饰某个类的属性

public class Person {
<span class="hljs-meta">@Colum(name = "姓名")</span>
<span class="hljs-keyword">private</span> String name;

<span class="hljs-meta">@Colum(name = "性别")</span>
<span class="hljs-keyword">private</span> String gender;

<span class="hljs-meta">@Colum(name = "年龄")</span>
<span class="hljs-keyword">private</span> <span class="hljs-type">int</span> age;

<span class="hljs-meta">@Colum(name = "住址")</span>
<span class="hljs-keyword">private</span> String address;

<span class="hljs-keyword">public</span> String <span class="hljs-title function_">getName</span><span class="hljs-params">()</span> {<span class="hljs-keyword">return</span> name;}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">setName</span><span class="hljs-params">(String name)</span> {<span class="hljs-built_in">this</span>.name = name;}
<span class="hljs-keyword">public</span> String <span class="hljs-title function_">getGender</span><span class="hljs-params">()</span> {<span class="hljs-keyword">return</span> gender;}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">setGender</span><span class="hljs-params">(String gender)</span> {<span class="hljs-built_in">this</span>.gender = gender;}
<span class="hljs-keyword">public</span> <span class="hljs-type">int</span> <span class="hljs-title function_">getAge</span><span class="hljs-params">()</span> {<span class="hljs-keyword">return</span> age;}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">setAge</span><span class="hljs-params">(<span class="hljs-type">int</span> age)</span> {<span class="hljs-built_in">this</span>.age = age;}
<span class="hljs-keyword">public</span> String <span class="hljs-title function_">getAddress</span><span class="hljs-params">()</span> {<span class="hljs-keyword">return</span> address;}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">setAddress</span><span class="hljs-params">(String address)</span> {<span class="hljs-built_in">this</span>.address = address;}

}

通过反射读取这个类的所有字段的中文含义,并保存到 list 中,然后打印出来

public static void main(String[] args) throws ClassNotFoundException {
    List<String> columNames = new ArrayList<>();
    Class clazz = Class.forName("annotation.Person");
    // 获取 Person 类所有属性
    Field[] fields = clazz.getDeclaredFields();
    for (Field field : fields){
        // 获取该属性的 Colum 注解
        Colum colum = field.getAnnotation(Colum.class);
        // 或者可以先判断有无该注解
        field.isAnnotationPresent(Colum.class);
        // 将该属性通过注解配置好的中文含义取出来放到集合中
        columNames.add(colum.name());
    }
<span class="hljs-comment">//打印集合</span>
columNames.forEach((columName) -&gt; System.out.println(columName));

}

结果如下:

姓名
性别
年龄
住址

比如我们有一些常见的应用场景,需要把网站上的列表导出成 excel 表格,我们通过注解的方式把列名配置好,再通过反射读取实体需要导出(是否需要导出,也可通过注解配置)的每个字段的值,从而实现 excel 导出的组件。

五. 总结

本文只是抛砖引玉地讲解了注解的基本概念,注解的作用,几种元注解的功用以及使用方法,并通过一个简单的例子讲解了一下注解的处理,并不全面,文中通过 Field 讲解了注解的基本 Api,但注解还可以修饰类、构造器、方法等,也有相对应的注解处理方法,大家可自行查一下 API 手册相关内容,大同小异,有不对之处,请批评指正,望共同进步,谢谢!