Java 注解是在 JDK5 时引入的新特性,注解(也被称为元数据)为咱们在代码中添加信息提供了一种形式化的方法,使咱们能够在稍后某个时刻很是方便地使用这些数据。注解类型定义指定了一种新的类型,一种特殊的接口类型。 在关键词 interface 前加 @ 符号也就是用 @interface 来区分注解的定义和普通的接口声明。目前大部分框架(如 Spring Boot 等)都经过使用注解简化了代码并提升的编码效率。html
全部的注解本质上都是继承自 Annotation 接口。可是,手动定义一个接口继承 Annotation 接口无效的,须要经过 @interface 声明注解,Annotation 接口自己也不定义注解类型,只是一个普通的接口。java
public interface Annotation { boolean equals(Object obj); int hashCode(); String toString(); /** *获取注解类型 */ Class<? extends Annotation> annotationType(); }
来对比下 @interface 定义注解和继承 Annotation 接口git
public @interface TestAnnotation1 { } public interface TestAnnotation2 extends Annotation { }
经过使用 javap 指令对比两个文件的字节码,发现经过 @interface 定义注解,本质上就是继承 Annotation 接口。程序员
// javap -c TestAnnotation1.class Compiled from "TestAnnotation1.java" public interface com.hncboy.corejava.annotation.TestAnnotation1 extends java.lang.annotation.Annotation {} // javap -c TestAnnotation2.class Compiled from "TestAnnotation2.java" public interface com.hncboy.corejava.annotation.TestAnnotation2 extends java.lang.annotation.Annotation {}
虽然本质上都是继承 Annotation 接口,但即便接口能够实现多继承,注解的定义仍然没法使用继承关键字来实现。github
经过 @interface 定义注解后,该注解也不能继承其余的注解或接口,注解是不支持继承的,以下代码就会报错。正则表达式
public @interface TestAnnotation1 { } /** 错误的定义,注解不能继承注解 */ @interface TestAnnotation2 extends TestAnnotation1 { } /** 错误的定义,注解不能继承接口 */ @interface TestAnnotation3 extends Annotation { }
虽然注解不支持继承其余注解或接口,但可使用组合注解的方式来解决这个问题。如 @SpringBootApplication 就采用了组合注解的方式。spring
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication { }
注解的基本架构如图所示,先简单了解下该架构,后面会详细讲解。编程
该架构的左半部分为基本注解的组成,一个基本的注解包含了 @interface 以及 ElementType 和 RententionPolicy 这两个枚举类。数组
该架构的右半部分为 JDK 部份内置的标准注解及元注解。架构
注解的属性也称为成员变量,注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。
注解内的可以使用的数据类型是有限制的,类型以下:
编译器对属性的默认值也有约束。首先,属性不能有不肯定的的值。也就是说,属性要么具备默认值,要么在使用注解时提供属性的值。对于非基本类型的属性,不管是在源代码中声明时,或是在注解接口中定义默认值时,都不能使用 null 为其值。所以,为了绕开这个约束,咱们须要本身定义一些特殊的值,例如空字符串或负数,来表示某个属性不存在。
经过一个案例来演示下注解可以使用的数据类型及默认值。
@interface Reference { boolean contain() default false; } enum Week { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday } public @interface TestAnnotation { /** * int 基本数据类型 * @return */ int type() default -1; /** * boolean 基本数据类型 * @return */ boolean status() default false; /** * String 类型 * @return */ String name() default ""; /** * Class 类型 * @return */ Class<?> loadClass() default String.class; /** * 枚举类型 * @return */ Week today() default Week.Sunday; /** * 注解类型 * @return */ Reference reference() default @Reference(contain = true); /** * 枚举数组类型 * @return */ Week[] value(); }
咱们已经了解了注解的架构,先来定义一个简单的注解。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface TestAnnotation { }
ElementType 枚举类型的常量为 Java 程序中可能出现注解的声明位置提供了简单的分类。这些常量用于 @Target 注解中。@Target 用于描述注解适用的范围,即注解能修饰的对象范围,经过 ElementType 的枚举常量表示。
先来看下 ElementType 该枚举类的代码。
public enum ElementType { /** * 用于描述类、接口(包括注解类型)、枚举的定义 */ TYPE, /** * 用于描述成员变量、对象、属性(包括枚举常量) */ FIELD, /** * 用户描述方法 */ METHOD, /** * 用于描述参数 */ PARAMETER, /** * 用于描述构造器 */ CONSTRUCTOR, /** * 用于描述局部变量 */ LOCAL_VARIABLE, /** * 用于描述注解的(元注解) */ ANNOTATION_TYPE, /** * 用于描述包 */ PACKAGE, /* * 表示该注解能写在类型变量的声明语句中 * @since 1.8 */ TYPE_PARAMETER, /** * 表示该注解能写在使用类型的任何语句中(声明语句、泛型和强制转换语句中的类型) * @since 1.8 */ TYPE_USE }
由于 Annotation 和 ElementType 是一对多的关系,因此 @Target 中能够存放数组,表示多个范围,默认全部范围。
JDK8 以前,注解只能用于声明的地方,JDK8 中添加了 TYPE_PARAMETER 和 TYPE_USE 类型注解,能够应用于全部地方:泛型、父类、接口,异常、局部变量等。举个例子,定义一个 @AnyWhere 注解,Boy 接口和 Test 类。
@Target({ElementType.TYPE_USE, ElementType.TYPE_PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface AnyWhere { } public interface Boy { } public class Test<@AnyWhere T> extends @AnyWhere Object implements @AnyWhere Boy { private @AnyWhere T test1(@AnyWhere T t) throws @AnyWhere Exception { return t; } private void test2() { Test<Integer> test = new @AnyWhere Test<>(); @AnyWhere List<@AnyWhere Integer> list = new ArrayList<>(); } }
RetentionPolicy 枚举类型的常量用于保留注解的各类策略,即该注解的有效期。它们与 @Retention 注解类型一块儿使用,以指定保留注解的时间。RetentionPolicy 枚举的代码以下。
public enum RetentionPolicy { /** * 表示该注解只存在于源码阶段, */ SOURCE, /** * 表示该注解存在于源码阶段和编译后的字节码文件里 */ CLASS, /** * 表示该注解存在于源码阶段、编译后的字节码文件和运行时期,且注解的内容将会被 JVM 解释执行 * 该范围的注解可经过反射获取到 */ RUNTIME }
Annotation 和 RetentionPolicy 是一对一的关系,即每一个注解只能有一种保留策略。
这三个枚举值是有等级关系的,SOURCE < CLASS < RUNTIME,即 RUNTIME 的有效范围是最大的,其次的是 CLASS,最小的范围是 SOURCE,默认的保留范围为 CLASS。
经过前面咱们了解到,注解本质上继承 Annotation 接口,也就是说,Annotation 接口是全部注解的父接口。@Retention 的保留策略为 RetentionPolicy.RUNTIME 的状况下,咱们能够经过反射获取注解的相关信息。Java 在 java.lang.reflect 包下也提供了对注解支持的接口。
主要来了解下 AnnotationElement 这个接口,其余接口都为该接口的子接口。该接口的对象表明 JVM 运行期间使用注解的类型(Class,Method,Field 等)。该包下的 Constructor 类、Method 类、Package 类和 Class 类等都实现了该接口。简单了解下该接口的部分函数。
public interface AnnotatedElement { /** * default 方法是 Java8 新增的 * 若是指定类型的注解存在该类型上,则返回 true,不然返回 false。此方法的主要目的是方便访问一些已知的注解 * * @param annotationClass 该泛型参数表示全部继承了Annotation 接口的接口,也就是注解 * @return 返回该类型上是否有指定的注解 */ default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) { return getAnnotation(annotationClass) != null; } /** * 根据注解的 Class 查询注解 */ <T extends Annotation> T getAnnotation(Class<T> annotationClass); /** * 返回该类型上的全部注解,包含继承的 */ Annotation[] getAnnotations(); /** * 返回该类型上的全部注解,不包含继承的 */ Annotation[] getDeclaredAnnotations(); }
咱们使用代码来测试下反射获取注解。定义两个注解,一个保留策略为 RetentionPolicy.RUNTIME,另外一个为 RetentionPolicy.CLASS。建立 TestAnnotation 类测试注解,该类上使用了这两个注解。
@Retention(RetentionPolicy.RUNTIME) public @interface TestAnnotation1 { String status() default "hncboy"; } @Retention(RetentionPolicy.CLASS) public @interface TestAnnotation2 { String value() default "hncboy"; } @TestAnnotation1(status = "hncboy2") @TestAnnotation2("hncboy2") public class TestAnnotation { public static void main(String[] args) throws ClassNotFoundException { Class<?> clazz = Class.forName("com.hncboy.corejava.annotation.TestAnnotation"); // 获取该类的全部注解 Annotation[] annotations = clazz.getAnnotations(); for (Annotation annotation : annotations) { System.out.println(annotation.annotationType()); System.out.println(annotation.toString()); } } }
输出结果以下,可见 TestAnnotation2 注解没有输出,由于 TestAnnotation2 注解类型是 RetentionPolicy.CLASS 的,因此用反射方法获取不到。这里还涉及到了注解的一个快捷方法,就是当注解里的属性名字定义为 value 时,能够在使用该注解时不指定属性名,上面的 @Target 注解和 @Retention 注解都属于这种状况,不过当注解里有多个属性时,那就必须指定属性名了。
interface com.hncboy.corejava.annotation.TestAnnotation1 @com.hncboy.corejava.annotation.TestAnnotation1()(status=hncboy2)
元注解即注解的注解且只能做用于注解上的注解,也就是说元注解负责其余注解的注解,并且只能用在注解上面。
JDK8 之前内置的元注解有 @Documented、@Retention、@Target、@Inherited 这四个,JDK 8 引入了 @Repeatable, 前面已经了解过了 @Target 和 @Retention,下面作一些简单的补充。
元注解的 @Target 都为 ElementType.ANNOTATION_TYPE,由于元注解只能应用于注解的注解。元注解在定义该注解的同时也能够直接使用该注解。
该注解用于定义注解能使用的范围,取值为 ElementType 枚举。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { /** * 返回能够应用注解类型的各类范围的枚举数组 * 名字为 value 时能够省略属性名 * @return */ ElementType[] value(); }
使用方式:
@Target(ElementType.METHOD) @Target(value = ElementType.METHOD) @Target({ElementType.METHOD, ElementType.TYPE}) @Target(value = {ElementType.METHOD, ElementType.TYPE})
该注解定义注解的保留策略或者说定义注解的有效期,取值范围为 RetationPolicy 枚举。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Retention { /** * 返回保留策略 * @return */ RetentionPolicy value(); }
使用方式:
@Retention(RetentionPolicy.RUNTIME) @Retention(value = RetentionPolicy.RUNTIME)
该注解的使用表示是否包含在生成的 javadoc 文档中。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Documented { }
举个例子,定义一个 @TestAnnotation 注解和 Test 类。
@Retention(value = RetentionPolicy.RUNTIME) @Documented public @interface TestAnnotation { } @TestAnnotation public class Test { }
经过 javadoc -d doc *.java 命令将该目录下的这两个类生成文档并放在 doc 目录下。生成的文件以下,点击 index.html。
看到如图所示的样子,Test 类中包含 @TestAnnotation。
咱们再把 @TestAnnotation 注解上的 @Documenet 注解注释掉再来生成下文档。此时发现 Test 类中没有 @TestAnnotation 注解了。
该注解表示注解是否具备继承的特性。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Inherited { }
举个例子来测试下。新建 TestAnnotation 注解,Father 类,Son 类,Father 类使用了该注解,Son 类继承 Father 类。
@Retention(RetentionPolicy.RUNTIME) @Inherited public @interface TestAnnotation { } @TestAnnotation public class Father { } public class Son extends Father { }
新建一个测试类,测试 Father 和 Son 这两个类是否包这两个注解。
public class Test { public static void main(String[] args) { System.out.println(Father.class.isAnnotationPresent(TestAnnotation.class)); System.out.println(Son.class.isAnnotationPresent(TestAnnotation.class)); } }
输出为 true true,当把 @TestAnnotation 注解上的 @Inherited 注解注释掉时,输出 true false,如此可见该注解的做用。
JDK8 之前是不支持重复注解的,同一个地方只能使用同一个注解一次。 该注解从 JDK8 引入,该注解类型用于表示其声明注解的注解类型为可重复时。 value() 的值表示可重复注解的类型,包含注解类型。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Repeatable { /** * 指可重复注解的类型,包含注解类型 * @return */ Class<? extends Annotation> value(); }
举个例子,定义 @Activity 和 @Activities 注解,定义 Hncboy 类测试重复注解。@Activity 注解被 @Repeatable(Activities.class) 注解,@Activities 至关于一个容器注解,属性为 Activity 类型的数组,经过这样的方式,使得 @Activity 注解能够被重复使用。
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface Activities { Activity[] value(); } @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Repeatable(Activities.class) public @interface Activity { String value(); } @Activity("打代码") @Activity("吃饭") @Activity("睡觉") public class Hncboy { } @Activities({@Activity("打代码"), @Activity("吃饭"), @Activity("睡觉")}) public class Hncboy { }
JDK 内置的注解有 @Deprecated、@Override、@SuppressWarnnings、@SafeVarargs(JDK 7 引入)、@FunctionalInterface(JDK 引入)等。接下来介绍下 3 中经常使用的内置注解。
注解为 @Deprecated 的类型是不鼓励程序员使用的元素,一般是由于这样作很危险,或者是由于存在更好的替代方法。当在不推荐使用的代码中使用或覆盖不推荐使用的程序元素时,编译器会发出警告。该注解能够用来修饰构造器、字段、局部变量、方法等类型。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) public @interface Deprecated { }
举个例子,使用 @Deprecated 修饰的元素是不推荐使用的,编译器会帮咱们将这些类和方法用删除线标记。直接声明在包上会报 Package annotations should be in file package-info.java 错误。
@Deprecated public class TestDeprecated { @Deprecated String s = "hncboy"; @Deprecated public void test() { } }
@Override 注解咱们常常用到,提示子类须要重写父类的方法。方法重写或实现了在父类中声明的方法时须要加上该注解,该注解用于编译器检查重写的操做是否正确,保留策略为 RetentionPolicy.SOURCE。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
用来关闭编译器生成警告信息,能够用来修饰类、方法、成员变量等,在使用该注解时,应采用就近原则,如方法产生警告是,应该针对方法声明该注解,而不是对类声明,有利于发现该类的其余警告信息。
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) @Retention(RetentionPolicy.SOURCE) public @interface SuppressWarnings { /** * 带有注解的元素中的编译器将禁止的警告集。 * 使用 unchecked 忽略没法识别的警告 */ String[] value(); }
举个例子,rawtypes 用于使用泛型时忽略没有指定相应的类型,unused 用于没有使用过的代码。
public class Test { @SuppressWarnings({"rawtypes", "unused"}) private List test() { return new ArrayList(); } }
自定义注解实现 Spring IOC Bean 实例建立,自定义简单的注解: @Component、@Bean 和 @ComponentScan。
经过什么是反射?这篇文章咱们已经学习到经过反射实现 Spring IOC Bean 实例的三种建立方式,不清楚的能够去看下那篇文章。
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface MyBean { String value() default ""; } @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface MyComponent { String value() default ""; } @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface MyComponentScan { String value() default ""; }
@MyComponent("a") public class A { public A() { System.out.println("调用 A 的无参构造器"); } @MyBean("b") public static B createBInstance() { System.out.println("调用 A 的静态方法 createBInstance"); return new B(); } @MyBean("c") public C createCInstance() { System.out.println("调用 A 的实例方法 createCInstance"); return new C(); } } class B {} class C {}
/** * 定义 map 存放 bean */ public class IOCContainer { private static HashMap<String, Object> container = new HashMap<>(); public static void putBean(String id, Object object) { container.put(id, object); } public static Object getBean(String id) { return container.get(id); } }
大体过程是这样,具体的能够见代码的注释。
@MyComponentScan("com.hncboy.corejava.annotation.spring") public class Test { public static void main(String[] args) throws Exception { Test test = new Test(); // 获取 MyComponentScan 注解中的包名 String scanPackage = test.getScanPackage(); HashSet<String> classPathSet = new HashSet<>(); // 扫描包下的全部类并将类的全限定名放进 classPathSet test.doScanPackage(classPathSet, scanPackage); // 遍历扫描包下的全部类 for (String className : classPathSet) { // 经过类的全限定名获取 Class Class<?> clazz = Class.forName(className); // 判断该类是否实现了 MyComponent 注解 if (clazz.isAnnotationPresent(MyComponent.class)) { // 方式1:经过构造器实例化 IOCContainer.putBean(className, clazz.newInstance()); } Method[] methods = clazz.getDeclaredMethods(); for (Method method : methods) { // 判断方法是否有 MyBean 注解 if (method.isAnnotationPresent(MyBean.class)) { // 获取 bean 值 String beanName = method.getAnnotation(MyBean.class).value(); // 判断该方法是不是静态方法或实例方法 if (Modifier.isStatic(method.getModifiers())) { // 方式2:经过静态工厂实例化 IOCContainer.putBean(beanName, method.invoke(null)); } else { // 方式3:经过实例工厂实例化 // 首先获取该类的实例对象,再调用实例方法进行实例化 IOCContainer.putBean(beanName, method.invoke(IOCContainer.getBean(className))); } } } } } /** * 获取 MyComponentScan 注解中的包名 * * @return */ private String getScanPackage() { Class<?> clazz = this.getClass(); if (!clazz.isAnnotationPresent(MyComponentScan.class)) { return ""; } MyComponentScan scanPackage = clazz.getDeclaredAnnotation(MyComponentScan.class); return scanPackage.value(); } /** * 扫描该包下的类 * * @param classPathSet * @param scanPackage */ private void doScanPackage(HashSet<String> classPathSet, String scanPackage) { // 经过正则表达式将包名中的 . 替代为 /,并获取到该路径的 class url URL url = this.getClass().getResource("/" + scanPackage.replaceAll("\\.", "/")); // 获取该 url 下的全部 File(目录/文件) File classDir = new File(url.getFile()); // 遍历全部 File for (File file : classDir.listFiles()) { // 判断该 file 若是是目录的话 if (file.isDirectory()) { // 拼接该目录的名字并递归遍历该目录 doScanPackage(classPathSet, scanPackage + "." + file.getName()); } else { // 若是文件不是以 .class 结尾 if (!file.getName().endsWith(".class")) { continue; } // 经过 包名+目录名+除去.class的类名 拼接该类的全限定名 String clazzName = (scanPackage + "." + file.getName().replace(".class", "")); // 将该类的全限定名放入 classPathSet classPathSet.add(clazzName); } } } }
输出以下:
调用 A 的无参构造器 调用 A 的静态方法 createBInstance 调用 A 的实例方法 createCInstance
注:APT——这些处理提取和处理 Annotation 的代码统称为 APT(Annotation Processing Tool)。
Java 编程思想
文章同步到公众号和Github,有问题的话能够联系做者。