Java自定义注解的实现和应用

注解的定义

注解(Annotation)是代码里的特殊标记,这些标记可以在编译、类加载、运行时被读取,并执行相应的处理。

通过注解,开发人员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充的信息。

需要注意的是,Annotation 仅仅的标记作用,不影响程序代码的执行,无论增加、删除 Annotation,代码都始终如一地执行。

而我们所看到的注解起作用,其实是背后封装了一堆代码,对注解进行读取和添加了响应的业务逻辑。

在写 Java 的时候,肯定都使用过注解,下面是 Java 提供的 5 个基本注解:

  • @Override:限定父类方法,强制一个之类必须覆盖父类的方法。
  • @Deprecated:标示某个类或方法已过时。当其他程序使用已过时的类、方法时,编译器会警告。
  • @SuppressWarnings:抑制编译器警告。被标记的元素以及所有子元素都不会发出编译警告。
  • @SafeVarargs:Java 7 提供的专门用于抑制 ”堆污染“ 警告。
  • @FuncationalInterface:Java 8 提供的专门用于标识函数式接口的注解(只能标注接口)

下面是 @Override 的注解定义:

package java.lang;

import java.lang.annotation.*;

/**

  • Indicates that a method declaration is intended to override a
  • method declaration in a supertype. If a method is annotated with
  • this annotation type compilers are required to generate an error
  • message unless at least one of the following conditions hold:
  • <ul><li>
  • The method does override or implement a method declared in a
  • supertype.
  • </li><li>
  • The method has a signature that is override-equivalent to that of
  • any public method declared in {@linkplain Object}.
  • </li></ul>
  • @author Peter von der Ah&eacute;
  • @author Joshua Bloch
  • @jls 9.6.1.4 @Override
  • @since 1.5
    */
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.SOURCE)
    public @interface Override {
    }

其中用到了 @Target 和 @Retention 这 2 个元注解。

元注解

JDK 在 java.lang.annotation 包下提供了 6 个 Meta Annotation(元注解),常用的有 4 个,分别是:

  • @Retention
  • @Target
  • @Documented
  • @Inherited

使用 @Retention

@Retention 只能用于修饰 Annotation 定义,用于指定被修饰 Annotation 可以保留多长时间。

@Retention 有一个 RetentionPolicy 类型的 value 成员变量,使用 @Retention 时必须指定 value 值。

value 变量的取值只有三个:

  • RetentionPolicy.SOURCE :注解信息仅保留在目标类代码的源码文件中,但对应的字节码文件将不再保留。
  • RetentionPolicy.CLASS :注解信息将进入目标类代码的字节码文件中,但类加载器加载字节码文件时不会将注解加载到 JVM 中,即运行期不能获取注解信息。这是默认值
  • RetentionPolicy.RUNTIME :注解信息在目标类加载到 JVM 后仍然保留,在运行期可以通过反射机制读取类中的注解信息。

例如:

// 定义的 Testable 注解会保留到运行时。
// 也可以这样写:@Retention(value = RetentionPolicy.RUNTIME)
@Retention(RetentionPolicy.RUNTIME)
public @interface Testable{}

我们常用的变量值是 RetentionPolicy.RUNTIME

使用 @Target

@Target 也只能用于修饰 Annotation 定义,用于指定被修饰的 Annotation 能用于修饰哪些程序单元。

@Target 也包含一个名为 value 的成员变量,变量值只能是如下几个:

  • ElementType.TYPE :该策略的注解只能用于 类、接口、注解类、Enum 声明处,称为类型注解
  • ElementType.FIELD :该策略的注解只能用于 类成员变量或常量声明处,称为域值注解。
  • ElementType.METHOD :该策略的注解只能用于 方法声明处,称为方法注解。
  • ElementType.PARAMETER :参数声明处,称为参数注解。
  • ElementType.CONSTRUCTOR :构造函数声明处,称为构造函数注解。
  • ElementType.LOCAL_VARIABLE :局部变量声明处,称为局域变量注解。
  • ElementType.ANNOTATION_TYPE :注解类声明处,称为注解类注解,ElementType.TYPE 包含了它。
  • ElementType.PACKGE :包声明处,称为包注解。

使用 @Documented

@Documented 也是用于修饰 Annotation 定义,用于指定被修饰的 Annotation 将被 javadoc 工具提取成文档。

如果定义注解类时使用了 @Documented 修饰,则所有使用该注解修饰的程序元素的 API 文档中将会包含该注解说明。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
//Testable 注解将被 javadoc 工具提取
@Documented
public @interface Testable{}

使用 @Inherited

@Inherited 指定被它修饰的 Annotation 具有继承性 ---- 如果某个类使用了 @Xxx 注解(定义 @Xxx 时使用了 @Inherited 修饰),则其子类将自动被 @Xxx 修饰。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
//Inheritable 注解修饰的类,其子类会自动使用 Inheritable 修饰
@Inherited
public @interface Inheritable{}

自定义注解

@Retention(RetentionPolicy.RUNTIME)	//1. 声明注解的保留期限
@Target(ElementType.METHOD)	//2. 声明可以使用该注解的目标类型
public @interface Testable{	//3. 定义注解
    boolean value() default true;	// 4. 声明注解成员
String <span class="hljs-title function_">name</span><span class="hljs-params">()</span> <span class="hljs-keyword">default</span> <span class="hljs-literal">null</span>;		<span class="hljs-comment">// 声明注解成员</span>

}

Java 语法规定使用 @interface 修饰符定义注解类。

一个注解可以拥有多个成员,成员声明和接口方法声明类似。

成员声明有以下限制:

  • 成员以无入参、无抛出异常的方式声明。
  • 可以通过 default 为成员指定一个默认值,当然也可以不指定默认值。
  • 成员的数据类型是受限制的,合法的类型包括:原始类型及其封装类、String、Class、enums、注解类型,以及上述类型的数组类型。

一个注解定义好之后就可以使用, 使用上述注解:

public class MyClass {
    // 使用 Testable 注解修饰方法, 并且使用参数覆盖默认值
    @Testable(value = false, name = "info")
    public void info (){
        ...
    }
    ...
}

那么问题来了!!

使用了这个注解之后,这个注解有什么作用呢?

跟 @Override 定义类似,会不会有与 @Override 类似的作用呢?

答案是:没有!

开局的时候就说过,Annotation 仅仅的标记作用,不影响程序代码的执行,无论增加、删除 Annotation,代码都始终如一地执行。

但是可以由开发者提供相应的工具来提取并处理 Annotation 信息。

提取 Annotation 信息

对于 RetentionPolicy.RUNTIME 保留期限的注解,可以通过反射机制访问类中的注解

在 Java 5.0 中,Packge、Class、Constructor、Method 及 Field 等反射对象都新增了访问注解信息的方法:

  • <T ectends Annotation>T getAnnotation(Class<T> annotationClass) :返回该程序元素上指定类型的注解,如果该注解不存在,就返回 null。
  • Annotation[] getAnnotations() :返回该程序元素上存在的所有注解。

其实还有关于注解几个方法,这里只介绍常用的。

下面举个简单例子

定义注解 @AnnoTest

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnoTest {
    boolean value();
}

使用注解 @AnnoTest

public class ForumService {
<span class="hljs-meta">@AnnoTest(value = false)</span><span class="hljs-comment">//标注注解</span>

public void deleteForum(int id){
System.out.println("删除论坛模块:" + id);
}

<span class="hljs-meta">@AnnoTest(value = true)</span><span class="hljs-comment">//标注注解</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">deleteTopic</span><span class="hljs-params">(<span class="hljs-type">int</span> id)</span>{
    System.out.println(<span class="hljs-string">"删除论坛主题:"</span> + id);
}

}

提取处理 @AnnoTest

public class ServiceTest {
<span class="hljs-comment">//提取@AnnoTest并处理</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">tool</span><span class="hljs-params">(Class clazz)</span>{

    <span class="hljs-comment">// 得到 clazz 对应的 Method 数组</span>
    Method[] methods = clazz.getDeclaredMethods();
    System.out.println(methods.length);

    <span class="hljs-keyword">for</span> (Method method : methods) {
        <span class="hljs-comment">// 获取方法上锁标注的注解对象</span>
        <span class="hljs-type">AnnoTest</span> <span class="hljs-variable">at</span> <span class="hljs-operator">=</span> method.getAnnotation(AnnoTest.class);
        <span class="hljs-keyword">if</span> (at != <span class="hljs-literal">null</span>){
            <span class="hljs-keyword">if</span> (at.value()){
                System.out.println(method.getName() + <span class="hljs-string">":删除成功"</span>);
            }<span class="hljs-keyword">else</span> {
                System.out.println(method.getName() + <span class="hljs-string">":删除失败"</span>);
            }
        }
    }
}


<span class="hljs-comment">//测试</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> {

    <span class="hljs-type">ServiceTest</span> <span class="hljs-variable">test</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">ServiceTest</span>();
    test.tool(ForumService.class);
}

}

结果


一般的项目中应用场景可能是记录系统日志,登录验证等地方,结合 spring 的 aop 去实现会更方便。

在 Spring AOP 中使用自定义注解,我们不用自己去提取注解信息,只需用来做个标记即可。

下面举个简单例子,注解的定义还是使用上面的 @AnnoTest (注意,@Retention 的取值必须为 RetentionPolicy.RUNTIME,不然提前不到注解信息)

定义增强类

@Aspect // 开启 spring aop
@Component
public class TestAspect {
<span class="hljs-comment">//增强前置通知,使用 @annotation() 扫描注解AnnoTest</span>
<span class="hljs-meta">@Before("@annotation(AnnoTest)")</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">b</span><span class="hljs-params">()</span>{
    System.out.println(<span class="hljs-string">"--------------------"</span>);
    System.out.println(<span class="hljs-string">"前置通知"</span>);
}

<span class="hljs-comment">//增强后置通知,使用 @annotation() 扫描注解AnnoTest</span>
<span class="hljs-meta">@AfterReturning("@annotation(AnnoTest)")</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">needTestFun</span><span class="hljs-params">()</span>{
    System.out.println(<span class="hljs-string">"needTestFun() executed!"</span>);
    System.out.println(<span class="hljs-string">"增加一个后置通知"</span>);
    System.out.println(<span class="hljs-string">"--------------------"</span>);
}

}

使用注解

@Component
public class TestImpl {
<span class="hljs-meta">@AnnoTest</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">greetTo</span><span class="hljs-params">(String Name)</span> {
    System.out.println(<span class="hljs-string">"TestImpl:greet to "</span> + Name);
}

}

单元测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class ControllerTest {
<span class="hljs-meta">@Autowired</span>
<span class="hljs-keyword">private</span> TestImpl test;

<span class="hljs-meta">@Test</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">aopTest</span><span class="hljs-params">()</span> {
    test.greetTo(<span class="hljs-string">"annotation test"</span>);
}

}

输出结果:

可以看出,使用 @AnnoTest 注解的方法被执行时,通过 SpringAOP 增强一个前置通知和一个后置通知。

小总结

注解起到的作用仅仅是标记作用,有或没有都不影响程序的执行。但可以通过提取注解元素并做一些处理,在项目中一般结合 springAOP 使用会更加方便。

在自定义 Annotation 的时候,我们常用到的元注解是 @Target()和 @Retention(),而 Retention 的取值一般为 RetentionPolicy.RUNTIME ,我们才可以提取注解。