本文内容基于 JDK8。注解是 JDK5 引入的,后续 JDK 版本扩展了一些内容,本文中没有明确指明版本的注解都是 JDK5 就已经支持的注解。
:notebook: 本文已归档到:「 blog 」
:keyboard: 本文中的示例代码已归档到:「 javacore 」
Java 中,注解是以 @
字符开始的修饰符。如下:
@Override void mySuperMethod() { ... } 复制代码
注解可以包含命名或未命名的属性,并且这些属性有值。
@Author( name = "Benjamin Franklin", date = "3/27/2003" ) class MyClass() { ... } 复制代码
如果只有一个名为 value 的属性,那么名称可以省略,如:
@SuppressWarnings("unchecked") void myMethod() { ... } 复制代码
如果注解没有属性,则称为 标记注解
。如: @Override
。
从本质上来说, 注解是一种标签,其实质上可以视为一种特殊的注释,如果没有解析它的代码,它并不比普通注释强。
解析一个注解往往有两种形式:
注解有许多用途:
作为 Java 程序员,多多少少都曾经历过被各种配置文件(xml、properties)支配的恐惧。过多的配置文件会使得项目难以维护。个人认为,使用注解以减少配置文件或代码,是注解最大的用处。
凡事有得必有失,注解技术同样如此。使用注解也有一定的代价:
但是,正所谓瑕不掩瑜,注解所付出的代价,相较于它提供的功能而言,还是可以接受的。
注解可以应用于类、字段、方法和其他程序元素的声明。
JDK8 开始,注解的应用范围进一步扩大,以下是新的应用范围:
类实例初始化表达式:
new @Interned MyObject(); 复制代码
类型转换:
myString = (@NonNull String) str; 复制代码
实现接口的声明:
class UnmodifiableList<T> implements @Readonly List<@Readonly T> {} 复制代码
抛出异常声明:
void monitorTemperature() throws @Critical TemperatureException {} 复制代码
JDK 中内置了以下注解:
@Override @Deprecated @SuppressWarnnings @SafeVarargs @FunctionalInterface
@Override
用于表明被修饰方法覆写了父类的方法。
如果试图使用 @Override
标记一个实际上并没有覆写父类的方法时,java 编译器会告警。
@Override
示例:
public class OverrideAnnotationDemo { static class Person { public String getName() { return "getName"; } } static class Man extends Person { @Override public String getName() { return "override getName"; } /** * 放开下面的注释,编译时会告警 */ /* @Override public String getName2() { return "override getName2"; } */ } public static void main(String[] args) { Person per = new Man(); System.out.println(per.getName()); } } 复制代码
@Deprecated
用于标明被修饰的类或类成员、类方法已经废弃、过时,不建议使用。
@Deprecated
有一定的 延续性
:如果我们在代码中通过继承或者覆盖的方式使用了过时的类或类成员,即使子类或子方法没有标记为 @Deprecated
,但编译器仍然会告警。
注意: @Deprecated
这个注解类型和 javadoc 中的 @deprecated
这个 tag 是有区别的:前者是 java 编译器识别的;而后者是被 javadoc 工具所识别用来生成文档(包含程序成员为什么已经过时、它应当如何被禁止或者替代的描述)。
@Deprecated
示例:
public class DeprecatedAnnotationDemo { static class DeprecatedField { @Deprecated public static final String DEPRECATED_FIELD = "DeprecatedField"; } static class DeprecatedMethod { @Deprecated public String print() { return "DeprecatedMethod"; } } @Deprecated static class DeprecatedClass { public String print() { return "DeprecatedClass"; } } public static void main(String[] args) { System.out.println(DeprecatedField.DEPRECATED_FIELD); DeprecatedMethod dm = new DeprecatedMethod(); System.out.println(dm.print()); DeprecatedClass dc = new DeprecatedClass(); System.out.println(dc.print()); } } //Output: //DeprecatedField //DeprecatedMethod //DeprecatedClass 复制代码
@SuppressWarnings
用于关闭对类、方法、成员编译时产生的特定警告。
@SuppressWarning
不是一个标记注解。它有一个类型为 String[]
的数组成员,这个数组中存储的是要关闭的告警类型。对于 javac 编译器来讲,对 -Xlint
选项有效的警告名也同样对 @SuppressWarings
有效,同时编译器会忽略掉无法识别的警告名。
@SuppressWarning
示例:
@SuppressWarnings({"rawtypes", "unchecked"}) public class SuppressWarningsAnnotationDemo { static class SuppressDemo<T> { private T value; public T getValue() { return this.value; } public void setValue(T var) { this.value = var; } } @SuppressWarnings({"deprecation"}) public static void main(String[] args) { SuppressDemo d = new SuppressDemo(); d.setValue("南京"); System.out.println("地名:" + d.getValue()); } } 复制代码
@SuppressWarnings
注解的常见参数值的简单说明:
deprecation unchecked fallthrough path serial finally all
@SuppressWarnings({"uncheck", "deprecation"}) public class InternalAnnotationDemo { /** * @SuppressWarnings 标记消除当前类的告警信息 */ @SuppressWarnings({"deprecation"}) static class A { public void method1() { System.out.println("call method1"); } /** * @Deprecated 标记当前方法为废弃方法,不建议使用 */ @Deprecated public void method2() { System.out.println("call method2"); } } /** * @Deprecated 标记当前类为废弃类,不建议使用 */ @Deprecated static class B extends A { /** * @Override 标记显示指明当前方法覆写了父类或接口的方法 */ @Override public void method1() { } } public static void main(String[] args) { A obj = new B(); obj.method1(); obj.method2(); } } 复制代码
@SafeVarargs
在 JDK7 中引入。
@SafeVarargs
的作用是:告诉编译器,在可变长参数中的泛型是类型安全的。可变长参数是使用数组存储的,而数组和泛型不能很好的混合使用。
简单的说,数组元素的数据类型在编译和运行时都是确定的,而泛型的数据类型只有在运行时才能确定下来。因此,当把一个泛型存储到数组中时,编译器在编译阶段无法确认数据类型是否匹配,因此会给出警告信息;即如果泛型的真实数据类型无法和参数数组的类型匹配,会导致 ClassCastException
异常。
@SafeVarargs
注解使用范围:
@SafeVarargs
注解可以用于构造方法。 @SafeVarargs
注解可以用于 static
或 final
方法。
@SafeVarargs
示例:
public class SafeVarargsAnnotationDemo { /** * 此方法实际上并不安全,不使用此注解,编译时会告警 */ @SafeVarargs static void wrongMethod(List<String>... stringLists) { Object[] array = stringLists; List<Integer> tmpList = Arrays.asList(42); array[0] = tmpList; // 语法错误,但是编译不告警 String s = stringLists[0].get(0); // 运行时报 ClassCastException } public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("A"); list.add("B"); List<String> list2 = new ArrayList<>(); list.add("1"); list.add("2"); wrongMethod(list, list2); } } 复制代码
以上代码,如果不使用 @SafeVarargs
,编译时会告警
[WARNING] /D:/Codes/ZP/Java/javacore/codes/basics/src/main/java/io/github/dunwu/javacore/annotation/SafeVarargsAnnotationDemo.java: 某些输入文件使用了未经检查或不安全的操作。 [WARNING] /D:/Codes/ZP/Java/javacore/codes/basics/src/main/java/io/github/dunwu/javacore/annotation/SafeVarargsAnnotationDemo.java: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。 复制代码
@FunctionalInterface
在 JDK8 引入。
@FunctionalInterface
用于指示被修饰的接口是函数式接口。
需要注意的是,如果一个接口符合"函数式接口"定义,不加 @FunctionalInterface
也没关系;但如果编写的不是函数式接口,却使用 @FunctionInterface
,那么编译器会报错。
什么是函数式接口?
函数式接口(Functional Interface)就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口。函数式接口可以被隐式转换为 lambda 表达式。
函数式接口的特点:
示例:
public class FunctionalInterfaceAnnotationDemo { @FunctionalInterface public interface Func1<T> { void printMessage(T message); } /** * @FunctionalInterface 修饰的接口中定义两个抽象方法,编译时会报错 * @param <T> */ /*@FunctionalInterface public interface Func2<T> { void printMessage(T message); void printMessage2(T message); }*/ public static void main(String[] args) { Func1 func1 = message -> System.out.println(message); func1.printMessage("Hello"); func1.printMessage(100); } } 复制代码
JDK 中虽然内置了几个注解,但这远远不能满足开发过程中遇到的千变万化的需求。所以我们需要自定义注解,而这就需要用到元注解。
Java 中提供了以下元注解类型:
@Retention @Target @Documented @Inherited @Repeatable
这些类型和它们所支持的类在 java.lang.annotation
包中可以找到。下面我们看一下每个元注解的作用和相应分参数的使用说明。
@Retention
指明了注解的保留级别。
@Retention
源码:
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Retention { RetentionPolicy value(); } 复制代码
RetentionPolicy
是一个枚举类型,它定义了被 @Retention
修饰的注解所支持的保留级别:
RetentionPolicy.SOURCE RetentionPolicy.CLASS RetentionPolicy.RUNTIME
@Retention
示例:
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Column { public String name() default "fieldName"; public String setFuncName() default "setField"; public String getFuncName() default "getField"; public boolean defaultDBValue() default false; } 复制代码
@Documented
表示无论何时使用指定的注解,都应使用 Javadoc(默认情况下,注释不包含在 Javadoc 中)。更多内容可以参考:Javadoc tools page。
@Documented
示例:
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Column { public String name() default "fieldName"; public String setFuncName() default "setField"; public String getFuncName() default "getField"; public boolean defaultDBValue() default false; } 复制代码
@Target
指定注解可以修饰的元素类型。
@Target
源码:
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { ElementType[] value(); } 复制代码
ElementType
是一个枚举类型,它定义了被 @Target
修饰的注解可以应用的范围:
ElementType.ANNOTATION_TYPE ElementType.CONSTRUCTOR ElementType.FIELD ElementType.LOCAL_VARIABLE ElementType.METHOD ElementType.PACKAGE ElementType.PARAMETER ElementType.TYPE
@Target
示例:
@Target(ElementType.TYPE) public @interface Table { /** * 数据表名称注解,默认值为类名称 * @return */ public String tableName() default "className"; } @Target(ElementType.FIELD) public @interface NoDBColumn {} 复制代码
@Inherited
表示注解类型可以被继承(默认情况下不是这样) 。
表示自动继承注解类型。 如果注解类型声明中存在 @Inherited
元注解,则注解所修饰类的所有子类都将会继承此注解。
注意: @Inherited
注解类型是被标注过的类的子类所继承。类并不从它所实现的接口继承注解,方法并不从它所覆写的方法继承注解。
此外,当 @Inherited
类型标注的注解的 @Retention
是 RetentionPolicy.RUNTIME
,则反射 API 增强了这种继承性。如果我们使用 java.lang.reflect
去查询一个 @Inherited
类型的注解时,反射代码检查将展开工作:检查类和其父类,直到发现指定的注解类型被发现,或者到达类继承结构的顶层。
@Inherited public @interface Greeting { public enum FontColor{ BULE,RED,GREEN}; String name(); FontColor fontColor() default FontColor.GREEN; } 复制代码
@Repeatable
表示注解可以重复使用。
以 Spring @Scheduled
为例:
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Schedules { Scheduled[] value(); } @Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Repeatable(Schedules.class) public @interface Scheduled { // ... } 复制代码
应用示例:
public class TaskRunner { @Scheduled("0 0/15 * * * ?") @Scheduled("0 0 12 * ?") public void task1() {} } 复制代码
使用 @interface
自定义注解时,自动继承了 java.lang.annotation.Annotation
接口,由编译程序自动完成其他细节。在定义注解时,不能继承其他的注解或接口。 @interface
用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、enum)。可以通过 default
来声明参数的默认值。
这里,我会通过实现一个名为 RegexValid
的正则校验注解工具来展示自定义注解的全步骤。
注解的语法格式如下:
public @interface 注解名 {定义体} 复制代码
我们来定义一个注解:
@Documented @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface RegexValid {} 复制代码
说明:
通过上一节对于元注解、、的说明,这里就很容易理解了。
@RegexValid
的注解。 @Documented
表示 @RegexValid
应该使用 javadoc。 @Target({ElementType.FIELD, ElementType.PARAMETER})
表示 @RegexValid
可以在类成员或方法参数上修饰。 @RegexValid
在运行时有效。 此时,我们已经定义了一个没有任何属性的注解,如果到此为止,它仅仅是一个标记注解。作为正则工具,没有属性可什么也做不了。接下来,我们将为它添加注解属性。
注解属性的语法形式如下:
[访问级别修饰符] [数据类型] 名称() default 默认值; 复制代码
例如,我们要定义在注解中定义一个名为 value 的字符串属性,其默认值为空字符串,访问级别为默认级别,那么应该定义如下:
String value() default ""; 复制代码
定义注解属性有以下要点:
注解属性的数据类型有限制要求。支持的数据类型如下:
注解属性必须有确定的值,建议指定默认值。注解属性只能通过指定默认值或使用注解时指定属性值,相较之下,指定默认值的方式更为可靠。注解属性如果是引用类型,不可以为 null。这个约束使得注解处理器很难判断注解属性是默认值,或是使用注解时所指定的属性值。为此,我们设置默认值时,一般会定义一些特殊的值,例如空字符串或者负数。
如果注解中只有一个属性值,最好将其命名为 value。因为,指定属性名为 value,在使用注解时,指定 value 的值可以不指定属性名称。
// 这两种方式效果相同 @RegexValid("^((//+)?86//s*)?((13[0-9])|(15([0-3]|[5-9]))|(18[0,2,5-9]))//d{8}$") @RegexValid(value = "^((//+)?86//s*)?((13[0-9])|(15([0-3]|[5-9]))|(18[0,2,5-9]))//d{8}$") 复制代码
示例:
了解了注解属性的定义要点,让我们来为 @RegexValid
注解定义几个属性。
@Documented @Target({ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface RegexValid { enum Policy { // @formatter:off EMPTY(null), DATE("^(?:(?!0000)[0-9]{4}([-/.]?)(?:(?:0?[1-9]|1[0-2])//1(?:0?[1-9]|1[0-9]|2[0-8])|(?:0?[13-9]|1[0-2])//1" + "(?:29|30)|(?:0?[13578]|1[02])//1(?:31))|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|" + "(?:0[48]|[2468][048]|[13579][26])00)([-/.]?)0?2//2(?:29))$"), MAIL("^[A-Za-z0-9](([_//.//-]?[a-zA-Z0-9]+)*)@([A-Za-z0-9]+)(([//.//-]?[a-zA-Z0-9]+)*)//.([A-Za-z]{2,})$"); // @formatter:on private String policy; Policy(String policy) { this.policy = policy; } public String getPolicy() { return policy; } } String value() default ""; Policy policy() default Policy.EMPTY; } 复制代码
说明:
在上面的示例代码中,我们定义了两个注解属性: String
类型的 value 属性和 Policy
枚举类型的 policy 属性。 Policy
枚举中定义了几个默认的正则表达式,这是为了直接使用这几个常用表达式去正则校验。考虑到,我们可能需要自己传入一些自定义正则表达式去校验其他场景,所以定义了 value 属性,允许使用者传入正则表达式。
至此, @RegexValid
的声明已经结束。但是,程序仍不知道如何处理 @RegexValid
这个注解。我们还需要定义注解处理器。
如果没有用来读取注解的方法和工作,那么注解也就不会比注释更有用处了。使用注解的过程中,很重要的一部分就是创建于使用注解处理器。JDK5 扩展了反射机制的 API,以帮助程序员快速的构造自定义注解处理器。
Annotation
接口源码如下:
public interface Annotation { boolean equals(Object obj); int hashCode(); String toString(); Class<? extends Annotation> annotationType(); } 复制代码
除此之外,Java 中支持
注解处理器接口 java.lang.reflect.AnnotatedElement
,该接口代表程序中可以接受注解的程序元素,该接口主要有如下几个实现类:
Class Constructor Field Method Package
java.lang.reflect
包下主要包含一些实现反射功能的工具类。实际上, java.lang.reflect
包所有提供的反射 API 扩充了读取运行时注解信息的能力。当一个注解类型被定义为运行时的注解后,该注解才能是运行时可见,当 class 文件被装载时被保存在 class 文件中的注解才会被虚拟机读取。 AnnotatedElement
接口是所有程序元素(Class、Method 和 Constructor)的父接口,所以程序通过反射获取了某个类的 AnnotatedElement
对象之后,程序就可以调用该对象的如下四个个方法来访问注解信息:
getAnnotation getAnnotations isAnnotationPresent getDeclaredAnnotations
了解了以上内容,让我们来实现 @RegexValid
的注解处理器:
import java.lang.reflect.Field; import java.util.regex.Matcher; import java.util.regex.Pattern; public class RegexValidUtil { public static boolean check(Object obj) throws Exception { boolean result = true; StringBuilder sb = new StringBuilder(); Field[] fields = obj.getClass().getDeclaredFields(); for (Field field : fields) { // 判断成员是否被 @RegexValid 注解所修饰 if (field.isAnnotationPresent(RegexValid.class)) { RegexValid valid = field.getAnnotation(RegexValid.class); // 如果 value 为空字符串,说明没有注入自定义正则表达式,改用 policy 属性 String value = valid.value(); if ("".equals(value)) { RegexValid.Policy policy = valid.policy(); value = policy.getPolicy(); } // 通过设置 setAccessible(true) 来访问私有成员 field.setAccessible(true); Object fieldObj = null; try { fieldObj = field.get(obj); } catch (IllegalAccessException e) { e.printStackTrace(); } if (fieldObj == null) { sb.append("/n") .append(String.format("%s 类中的 %s 字段不能为空!", obj.getClass().getName(), field.getName())); result = false; } else { if (fieldObj instanceof String) { String text = (String) fieldObj; Pattern p = Pattern.compile(value); Matcher m = p.matcher(text); result = m.matches(); if (!result) { sb.append("/n").append(String.format("%s 不是合法的 %s !", text, field.getName())); } } else { sb.append("/n").append( String.format("%s 类中的 %s 字段不是字符串类型,不能使用此注解校验!", obj.getClass().getName(), field.getName())); result = false; } } } } if (sb.length() > 0) { throw new Exception(sb.toString()); } return result; } } 复制代码
说明:
以上示例中的注解处理器,执行步骤如下:
RegexValid valid = field.getAnnotation(RegexValid.class);
这样的形式获取,注解实例化对象,然后,就可以使用 valid.value()
或 valid.policy()
这样的形式获取注解中设定的属性值。 完成了以上工作,我们就可以使用自定义注解了,示例如下:
public class RegexValidDemo { static class User { private String name; @RegexValid(policy = RegexValid.Policy.DATE) private String date; @RegexValid(policy = RegexValid.Policy.MAIL) private String mail; @RegexValid("^((//+)?86//s*)?((13[0-9])|(15([0-3]|[5-9]))|(18[0,2,5-9]))//d{8}$") private String phone; public User(String name, String date, String mail, String phone) { this.name = name; this.date = date; this.mail = mail; this.phone = phone; } @Override public String toString() { return "User{" + "name='" + name + '/'' + ", date='" + date + '/'' + ", mail='" + mail + '/'' + ", phone='" + phone + '/'' + '}'; } } static void printDate(@RegexValid(policy = RegexValid.Policy.DATE) String date){ System.out.println(date); } public static void main(String[] args) throws Exception { User user = new User("Tom", "1990-01-31", "xxx@163.com", "18612341234"); User user2 = new User("Jack", "2019-02-29", "sadhgs", "183xxxxxxxx"); if (RegexValidUtil.check(user)) { System.out.println(user + "正则校验通过"); } if (RegexValidUtil.check(user2)) { System.out.println(user2 + "正则校验通过"); } } } 复制代码