Java注解机制的应用研究
作者: 曾水新 黄日胜
摘要:Java的注解机制在JDK5就已推出,后续发布的新版不断完善,目前注解机制的应用已经很广泛,如用于辅助开发的工具Lombok、AutoValue、Immutables等,主流的开发框架如Spring、MyBatis也大量以注解替代了XML配置文件。大多数开发者仅会使用注解,但不了解其工作原理,该文详细介绍了JDK内置的注解、元注解的作用和用法,分析了注解的工作原理,并以案例演示了如何编写自定义注解,包括声明注解、处理注解、使用注解三个流程,最后介绍了注解的应用场景。
关键词:Java;注解;反射技术;框架技术;编译器
中图分类号:TP311.1 文献标识码:A
文章编号:1009-3044(2022)34-0035-04
1 引言
Java或Android的开发者对注解(Annotation) 机制一定不会陌生,在项目开发过程中,开发者会接触到很多注解,如@Override、@Deprecated、@SuppressWarnings等,如果使用框架,可能会使用到注解@Controller、@Param、@Select等。目前关于注解原理的资料相对比较贫乏,很多开发者会使用注解,但不了解注解的工作原理、运行机制,也不清楚如何编写注解。
2 Java注解机制介绍
2.1 注解的概念
注解是JDK5.0引入的一种标注机制,Oracle官方定义为:“Annotations, a form of metadata, provide data about a program that is not part of the program itself.”[1]。即注解是元数据的一种形式,注解提供了程序的信息但不属于程序的一部分。注解可以对代码添加附加信息,但不会侵入业务代码,也不会影响代码的具体执行过程[2]。开发者可以对包、类、接口、字段、方法参数、局部变量等进行注解,添加特殊标记,在编译期或运行期可以对这些被标记的类、变量、方法或方法参数进行一些特殊的操作。
注解与Javadoc注释容易混淆,两者貌似相同,实则区别很大:
Javadoc是Sun公司提供的一种工具,它可以从程序源代码中抽取类、方法、成员等注释,然后形成一个和源代码配套的API帮助文档,相当于产品说明书,是为了便于开发者调用时了解类、方法和属性的作用、用法而诞生的,Javadoc是特殊的、格式化的注释,本质上还是注释。注释是给开发者看的。
注解则不一样,开发者通过配置,使其在编译时、类加载时、运行时可见,还可通过Java的反射机制获取注解内容,加入自定义的处理逻辑。注解是非侵入性的,不会干涉代码本身的处理流程,而是通过低耦合的“贴标签”形式,向原有代码附加信息,编码辅助工具、部署工具、IDE都可以读取注解,为开发者提供检测代码、自动编码、验证部署的服务,注解是给机器看的。
2.2 Java内置的标准注解
1) @Override:该注解是使用频率较高的一个,标注在方法上,表示该方法是重写父类方法。编译器会进行检测是否符合重写规则,如果不符合,比如父类(包括接口)没有这个方法,则会提示错误。不写@Override其实也不会影响程序运行,这个注解是否就没有存在的意义?不是的,编写代码时,通常开发者很清楚他要重写一个方法,但是可能单词拼写错误,如果没加注解,编译器就发现不了错误。
2) @Deprecated:用于标记类、成员变量、成员方法或者构造方法已废弃,不推荐开发者使用,如果开发者调用了被标记了@Deprecated的方法,编译时会有警告信息,但仍能强制编译。
3) @SuppressWarnings:该注解的作用是指示编译器对被注解的代码元素内部的某些警告保持静默,支持在类、属性、方法、参数、构造方法、本地变量上使用。标注了“@SuppressWarnings”不等于消除了警告内容,只是编译器不显示而已,除非开发者确定该警告的隐患不会影响程序的正常运行,否则不建议使用该注解。
4) @SafeVarargs:JDK7加入的注解,用于取消编译器产生的unchecked警告。在声明一个有泛型的可变参数的构造函数或者方法时,编译器会提示unchecked警告,如果开发者确定该构造函数或方法不会造成不安全的操作时,可使用@SafeVarargs进行修饰,编译器就会忽略unchecked警告。例如定义了一个静态方法如下,添加上@SafeVarargs后,编译器警告消失:
@SafeVarargs
public static <T> void myMethod(T...array){
//函数主体
}
5) @FunctionalInterface:JDK8新增了函数式编程[3],相应地加入了函数式接口注解,所谓函数式接口实际上是一个Lambda表达式,它本质上是接口,比普通接口多了一个约束:有且仅有一个抽象方法。标注了@FunctionalInterface注解,即指示编译器检查开发者编写的接口是否符合函数式接口的约束条件,如不符合,编译器会给出错误提示。
2.3 Java内置的元注解
元注解是注解的注解,是JDK的基础注解,它作用在其他注解上面,用于标记和描述注解的基本信息。编写一个注解需要指明其保留的时间和生效的上下文等最基本的信息,JDK原生的元注解则提供了可以用于标注并描述这些信息的注解。JDK提供的元注解有:
1) @Retention:用于设定注解的生命周期,可以取值为:①RetentionPolicy.SOURCE,注解只在源码阶段保留,源码被编译之后就不存在了。②RetentionPolicy.CLASS,注解内容被编译到字节码文件(.class) 里,但JVM读取字节码文件时,并不将其加载,这是@Retention的默认取值。③RetentionPolicy.RUNTIME,注解的生命周期贯穿于源码、.class文件、JVM三个阶段,因此程序在运行时可以获取到它们。
2) @Documented:注解后Javadoc工具可从源代码中抽取出类、方法、成员等注释形成一个配套的API帮助文档,默认情况下,类和方法的注解内容是不会出现在Javadoc中的,使用@Documented修饰后,该注解即可被Javadoc工具提取到API文档。要使@Documented注解生效的前提是:@Retention的值需设置为RetentionPolicy.RUNTIME。
3) @Target:用于指定注解的放置目标。注解可用于修饰包、接口、类、方法、变量等类型,@Target注解确定该注解可以出现在哪个位置,取值范围定义在ElementType枚举里:①TYPE:表示可用于标注类、接口、注解、枚举;②FIELD:表示可用于标注成员变量;③METHOD:表示可用于标注成员方法;④ PARAMETER:表示可用于标注参数;⑤CONSTRUCTOR:表示可用于标注构造器;⑥LOCAL_VARIABLE:表示可用于标注本地变量;⑦ANNOTATION_TYPE:表示可用于标注注解(即元注解);⑧PACKAGE:表示可用于标注包;⑨TYPE_PARAMETER:这是JDK8新增的,表示可用于标注自定义类型参数;⑩TYPE_USE:JDK8新增的,表示可用于标注除class外的任意类型。
@Target可使用单个枚举值,如设定为元注解,代码为:
@Target(ElementType. ANNOTATION_TYPE)
也可使用多个枚举值,需将多个枚举值用大括号“{}”包围,如设定注解可添加到成员方法和成员变量上,代码为:
@Target({ElementType.METHOD, ElementType.FIELD})
4) @Inherited:用于指明父类注解会被子类继承。@Inherited仅针对@Target(ElementType.TYPE)类型的注解有效,并且仅针对class的继承,对interface的继承无效。
5) @Native:JDK8新增的注解,表示被修饰的成员变量可以被本地代码引用,常被代码生成工具使用。
6) @Repeatable:JDK8新增的注解。JDK8之前,同一程序元素前最多只能有一个相同类型的注解。@Repeatable允许在相同的程序元素中重复注解。
3 自定义注解
内置的注解并不多,开发者可以编写自定义注解,主要有三个步骤:一是声明注解,二是处理注解,三是使用注解。
3.1 自定义注解的声明
3.1.1 自定义注解的语法
[[public] @interface 注解名称{
[数据类型 变量名称();]
} ]
关键字“@interface”与标准的接口关键字interface是不一样的,从反编译的代码中,可以看到类似“public interface AnnoDemo extends Annotation {}”的代码,意味着注解继承了Annotation接口(在java.lang.annotation包中),即该注解就是一个Annotation,因此注解本质上是一个特殊的接口(interface) ,接口里可以定义什么,注解里同样也可以定义,它的修饰符与接口一样,也是默认被public abstract修饰。它和普通的接口不一样的地方:
1) 定义普通接口使用interface修饰,但定义注解使用的是@interface。
2) 普通接口使用implements关键字实现接口,注解的实现是由编译器完成。
3) 普通接口可以继承多个接口,注解不能继承其他的注解或接口。
4) 在定义注解时可以定义属性,但是属性必须使用括号“()”,形式上是一个方法。
一个典型的注解声明代码如下:
[@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE,ElementType.TYPE})
public @interface AnnoDemo {
String role() ;
} ]
3.1.2 注解的属性
如前所述,注解的属性与普通类属性不一样,它的属性是抽象方法,注解的属性规则有:
1) 返回值类型必须是以下几种:基本数据类型、String类型、枚举类型、注解、以上类型的数组。
2) 使用时需给属性赋值,如下面代码SomeClass类前面添加了@AnnoDemo注解,并为其role属性赋值为admin。
[@AnnoDemo(role = "admin")
class SomeClass{
} ]
3) 可以使用default关键字设置属性的默认值,有默认值的属性,在使用时可不给属性赋值。
[public @interface AnnoDemo {
String role() default "user";
}
@AnnoDemo
class SomeClass{
} ]
4) 注解如有多个属性,赋值时可以在注解括号中用“,”号隔开分别给对应的属性赋值。
5) 如注解只有一个属性,可将其命名为value,赋值时可省略value直接定义值。