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é
- @author Joshua Bloch
- @jls 9.6.1.4 @Override
@since 1.5
*/
public 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 ,我们才可以提取注解。