公众号「稀有猿诉」 原文链接 Java Annotations Made Easy
在Java编程语言中,注解Annotations是一种元数据,能提供代码以外的信息,是元编程的一种体现形式。注解的引入极大的增强了Java语言的元编程能力,能在编译时生成代码,大大减少了重复代码,让Java具有了相当高的动态特性,让一些高级技术如依赖注入,AOP等成为可能。今天将从基础使用,核心概念理解和创建自定义注解三个角度来彻底学会注解,并学会使用注解来提升开发效率。
基础知识
注解在代码中使用是非常常见的,相信只要有编程经验的同学都对注解非常的熟悉。
什么是注解
Java 注解(Annotation)是JDK5.0及以后版本引入的,它可以用于创建文档,代码分析,编译检查以及编译时生成代码。Java 注解是接口的一种特殊实现,程序可以通过反射来获取指定程序元素的Annotion对象,然后使用该对象来获取注解里面的元数据。
注解的用法
注解的使用是非常简洁明了的,Java 注解的基本语法是使用“@”符号来定义一个注解,然后在这个符号后面跟上注解的名字,并在这个名字的后面添加一个括号,括号中是这个注解所需要的参数列表。Java 注解是接口的一种特殊实现,因此注解的定义方式类似于接口的定义方式。Java 注解可以分为三种类型:标记注解、单值注解和完整注解。标记注解没有成员变量,只有一个标记作用;单值注解有一个成员变量;完整注解有多个成员变量。
内置注解
Java内置了一些注解,相信写过代码或者看过代码的人都对此非常的了解,因为在代码中是非常非常的常见的。
- @Override - 用于类的方法上,标记该方法要覆写基类(包括接口)的方法。编译器会对标记的方法作签名检查是否符合覆写规则。
- @Deprecated - 可以标记类,成员变量和成员方法为过时的方法,编译器会对调用这些类,成员或者方法给出警告(Compile warnings)。
- @SuppressWarnings - 可以用在类和方法上,强制忽略编译警告,即阻止编译器发出编译警告。后面需要加括号,里面传入字符串或者字符串数组代表要忽略的警告类型。
- @FunctionalInterface - 这是在Java 8版本中引入的,用在接口上,标记接口是一个函数式接口(即只有一个方法的接口,可以直接用一个lambda来作为接口的实例)。
- @SafeVarargs - 用于方法和构造方法上,断言varargs参数(即可变长参数)会被安全地使用。比如涉及泛型的可变长参数会有『unchecked』警告,加了@SafeVarargs时编译器不会再给出『unchecked』警告。
通过这些内置注解可以了解注解的类型和特点,并掌握注解的使用方法,这是学习自定义注解,即注解高级玩法的基础。
理解注解
可以发现注解并不直接对其修饰的代码产生影响,它是为代码提供额外的信息,它是代码的元数据,注解与代码一起构成了编译器的完整输入,编译器借助注解可以生成并得到最终完整的代码。
注解本身无论是使用还是定义都相对直观和简洁,非常容易理解,因为注解本身就是一种元数据,提供一种标记或者额外的数据。重点在于注解的处理,这是注解功能发挥作用的地方也就是注解功能逻辑实现的地方。
元编程
注解是程序的元数据,所以这属于元编程范畴。元编程Metaprogramming也即是以代码为操作目标和目标输出的编程范式,元编程是生产力工具,可以减少重复代码,大大的提高代码开发效率。大多数通用编程语言都支持元编程,像C/C++语言中的宏,Java中的注解,反射和动态代理,大Python中的装饰器(Decorators装饰器是高阶函数对函数进行操作)和元类(Metaclasses,对类进行操作可理解为类的模板)等等都是元编程。
优秀的框架(Spring)和领域驱动开发(DDD)都是元编程的典型应用。
关于Java的元编程,推荐这两篇文章:
注解的分类
注解是向编译器提供额外信息的一种元编程机制,那么依据机制的简单到复杂,可以把注解分为5个类型:
标记注解(Marker Annotations)
最简单的注解,对于某个声明进行标记,编译器会对被标记的声明进行检查和处理。如@Override和@Deprecated。
单值注解(Single Value Annotations)
需要给注解传递一个参数且只有一个参数,如@SuppressWarnings("unchecked")。
全值注解(Full Annotations)
需要给注解传递很多参数(多个键值对),如:
@Test(owner="Paul", values="Class Greeks")
public void testSomeMethod() {
// ...
}
类型注解(Type Annotations)
可以用在类型被声明的地方,比如方法返回值,方法的参数声明等,如:
public @NonNull String transform(@Nullable String source) { ... }
重复注解(Repeating Annotations)
常规的注解在同一个地方只能出现一次,但重复注解可以在同一个地方出现多次,如:
@Words(word="Hello", value=1)
@Words(word="World", value=2)
public void method() {
// ...
}
自定义注解
注解的使用是非常的直观和简洁的,无论是内置注解还是各种框架定义好了的注解,使用起来那是相当的香。但这远远不够,因为注解最大的威力在于元编程,比如代码操作和代码生成,这是减少重复劳动(重复代码)和提供开发效率的大杀器。所以我们必须学会高级玩法,即自定义注解。
元注解
元注解,也即定义注解时所需要的注解。这有点类似于编译器自举,语言本身定义了一个最基础的注解,在其基础之上可以扩展出更多的注解,而注解的处理是通过反射,只要知道一些特殊的标记就可以了,其余的都是逻辑。
@Inherited
默认情况下,在基类中使用的注解是不会被子类继承的,如果注解本身标记有@Inherited,那么注解就会出现在被使用的继承体系中:
@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Company {
String name() default "ABC";
String city() default "xyz";
}
@Company
public class Employee { .. }
public class Manager extends Employee { ... }
这个🌰中,如果把@Inherited从注解Company中去掉,那么给类Employee加的注解在其子类Manager中就不会得到继承。大部分时候定义注解时都要加上@Inherited标记。
@Documented
使用了@Documented标记的注解可以出现在文档中(JavaDocs)。
@Repeatable
对应着可重复的注解,指定着可以在哪些标识符上面重复注解。
@Target
指定注解可以作用于何种标识符,如果不指定则可以使用于任何标识符即任何程序元素。可选的选项有:
- ElementType.ANNOTATION_TYPE - 可以用于其他的注解上面
- ElementType.CONSTRUCTOR - 可以用于构造方法上面
- ElementType.FIELD - 可以用于成员变量上面
- ElementType.LOCAL_VARIABLE - 可以用于方法的本地变量(栈内变量)
- ElementType.METHOD - 可以用于方法上面
- ElementType.PACKAGE - 可以用于包(package)上面。
- ElementType.PARAMETER - 可以用于方法的参数上面。
- ElementType.TYPE - 可以用于类型声明的地方(即类,接口和枚举的声明)。
可以指定一个@Target(ElementType.METHOD)或者多个目标@Target({ElementType.FIELD, ElementType.LOCAL_VARIABLE})。
@Retention
元注解@Retention用于指定注解保留的生命周期。注解是一种元数据,目标是代码,而代码是有生命周期的:编辑或者说源码时;编译时;运行时。这是程序代码的典型生命周期。而@Retention的作用就是指明注解保留到哪个生命周期。
- RetentionPolicy.SOURCE - 在源码时保留,编译时就被丢弃,也就是说在编译时并不使用。一般用于编译前的源码处理工具使用,如javadoc,以及代码生成。
- RetentionPolicy.CLASS - 编译后仍会保留在class文件中,但在运行时(就是JVM加载class时)被丢弃。主要是在编译时使用(比如生成代码)。
- RetentionPolicy.RUNTIME - 保留到运行时,在运行时可以被使用。
自定义注解
注解可以视为一个特殊的接口,注解的定义就是定义一个接口,而每个接口就是其实现。注解的处理器利用反射获取注解接口的类型信息,再结合注解提供的数据就生成接口的实现代码。这就是注解的工作机制。
用@interface就可以声明一个自定义注解,通用的格式是:
[Access Modifier] @interface <Annotation name> {
<Type> <Method name>() [default value];
}
可以看到注解本质上是一种接口,但它有一些具体的限制规则:
- 注解的方法不能有参数和异常签名(throws)
- 方法的返回值不受限制,可以是任意类型
- 方法的默认返回值是可选的(即可以有,也可以没有)
- 定义注解时可以使用元注解,这种套娃机制可以实现更为复杂和更为强大的注解
看一个完整自定义注解的🌰
@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodInfo {
String author() default "Kevin";
String date();
int revision() dfeault 1;
String comments();
}
运行时注解解析
定义了注解后,就可以在代码中使用了,但这还没完,还需要对注解进行解析和处理。在运行时需要用到反射来解析注解,反射API中有专门用于处理注解的API:
- AnnotatedElement - 这是反射接口处理注解的核心类型,它是反射类型Method,Field和Constructor的基类,通过它的方法来获取注解Annotation实例。
- 用Annotation来处理具体的注解
注意注意,注解的解析和处理用的是反射,所以注解定义时要用RententionPolicy.RUNTIME,否则用反射是拿不到注解信息的,因为反射是在运行时(Runtime)。下面我们会用一个完整的实例来学习如何处理自定义注解。
完整示🌰
至此注解的概念的原理都清楚了,融会贯通一下,用一个完整的🌰来展示自定义注解。
Step 1:定义注解
直接复用前面定义的@MethodInfo。
Step 2:使用注解
public class MethodInfoExample {
@Override
@MethodInfo(author = "Alex", comments = "Main method", date = "Mar 29 2024", revision = 2)
public String toString() {
return "toString method Overridden";
}
@Deprecated
@MethodInfo(comments = "Deprecated method", date = "Mar 30, 2024")
public static void oldMethod() {
System.out.println("Old method out!");
}
@SuppressWarnings({"unchecked", "deprecation"})
@MethodInfo(author = "Paul", comments = "Main method", date = "Mar 31 2024")
public void genericsMethod() throws FileNotFoundException {
List list = new ArrayList();
list.add("Xyz");
oldMethod();
}
}
Step 3:解析注解
public class MethodInfoParsing {
public static void main(String[] args) {
try {
Method[] methods = MethodInfoParsing.class
.getClassLoader().loadClass("MethodInfoExample").getDeclaredMethods();
for (Method method : methods) {
if (!method.isAnnotationPresent(MethodInfo.class)) {
continue;
}
for (Annotation annotation : method.getDeclaredAnnotations()) {
System.out.println("Annotation " + annotation + " on method " + method.getName());
}
MethodInfo info = method.getAnnotation(MethodInfo.class);
if ("Paul".equals(info.author())) {
System.out.println("From Pauls: " + method.getName());
}
}
} catch (ClassNotFoundException e) {
}
}
}
注解处理器
在运行时解析注解比较简单,较麻烦的是在编译时(Compile time)处理注解,这时的处理又特别的关键,因为像代码生成是在这一阶段做的。编译时处理注解需要用到Annotation Processor。
一个典型的Annotation processor实现过程:
- 实现一个Processor,通常通过继承AbstractProcess。
- 覆写方法process来处理注解,这里面过滤出想要处理的注解,然后用JavaWriter来生成Java文件(或者粗暴的用PrintWriter也可以)。
- 注册实现好的Processor给编译器:可以通过编译命令javac -processor来指定处理器;也可以把处理器打成jar包然后当成库添加到项目中,因为编译器在开始编译前会自动的去搜索注解和注解处理器。
可以参考如下文章来详细了解Annotation processor的实现过程:
这里是一系列优秀的Annotation processor案例。
为什么用注解
注解是非常优雅的元编程方式,可以生成代码(减少重复),降低耦合。比如著名的单元测试框架JUnit,在其4.0时(即JUnit4)就用注解替代了继承。在JUnit3要这样写测试:
// Using JUnit 3.0
public class MyClassTest extends TestCase {
private MyClass instance;
@Override
protected void setup() throws Exception {
super.setup();
instance = new MyClass();
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
}
public void testSomeMethod() {
assertNotNull(instance);
assertEquals("Hello, world", instance.say());
}
}
这是类MyClass的一个简单的测试用例。在JUnit4使用了注解后,就可以这样写了:
// Using JUnit 4.0
public class MyClassTest {
private MyClass instance;
@Before
private void setup() {
instance = new MyClass();
}
@After
private void tearDown() {}
@Test
public void testSomeMethod() {
assertNotNull(instance);
assertEquals("Hello, world", instance.say());
}
}
通过注解@Before标记测试前准备和@After测试后清理,用@Test标记测试用例,也不用继承TestCase了,整体测试代码非常的优雅。这就是注解的作用。
什么时候用注解
注解的本质是程序的元数据,为编译器提供的代码以外的额外的数据。注解是优雅的元编程的一种方式,可以减少重复的代码,提升开发效率。所以每当需要减少重复代码,生成代码,提供元数据时就要用注解来实现。特别是特定领域的问题,非常适合大量使用注解,如数据库(Room),网络请求(Retrofit),单元测试(JUnit)等等。并且注解的大部分应用都是在编译时生成代码,也不影响性能,所以可劲造儿,尽可能的使用注解吧。
总结
本文从注解的基础用法出发,再到核心概念的阐述,最后用一个自定义注解的例子展示如何用注解来实现元编程,全方位的阐述了注解。相信通过此文对注解的理解会更上一个层次。
参考资料
- Lesson: Annotations
- Annotations in Java
- Java Annotations
- Creating a Custom Annotation in Java
- An Introduction to Annotations and Annotation Processing in Java
- 教科书级讲解,秒懂最详细Java的注解
- Java 基础 - 注解机制详解
- java注解的本质以及注解的底层实现原理
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
原创不易,「打赏」,「点赞」,「在看」,「收藏」,「分享」 总要有一个吧!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。