万字长文带你细聊Java注解本质
# 写在文章开头
注解也叫元数据,在jdk1.5引入,和类、接口等属于同一个层级。也可用在类、字段、接口、方法、形参上。 初学者可能会把注解和注释混淆,实际上注释是告知告知程序员这段程序的用意,而注解则是告知计算机这段程序的用意。

你好,我叫sharkchili,目前还是在一线奋斗的Java开发,经历过很多有意思的项目,也写过很多有意思的文章,是CSDN Java领域的博客专家,也是Java Guide的维护者之一,非常欢迎你关注我的公众号:写代码的SharkChili,这里面会有笔者精心挑选的并发、JVM、MySQL数据库专栏,也有笔者日常分享的硬核技术小文。

# 详解注解的作用
# 生成带有说明的文档
如下所示代码,由于添加了作者、版本、since这些注解,所以在生成文档的时候就会体现这些内容。
/**
* 注解javadoc演示
*
* @author test
* @version 1.0
* @since 1.5
*/
public class AnnoDemo1 {
/**
* 计算两数的和
* @param a 整数
* @param b 整数
* @return 两数的和
*/
public int add(int a, int b ){
return a + b;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
使用javadoc命令进行文档生成

可以看到生成的类文档的作者、版本等都是我们注解后面编写的值。

# 代码分析
使用反射完成基于代码里标识的注解,对代码进行分析,从而完成动态增强,这也是Spring框架对注解最典型的运用(因为示例篇幅较大,代码分析的例子会在下文给出)。
# 编译检查
通过代码里标识的注解让编译器能够实现基本的编译检查,最典型的就是Override注解。
public class MsgEventFactory implements EventFactory<MessageModel> {
@Override
public MessageModel newInstance() {
return new MessageModel();
}
}
2
3
4
5
6
# java内置的三大常见注解
# @Override
源码如下所示,可以看到元注解有target和Retention,其中Retention为source,即在编译时检查当前子类重写的方法在父类中是否存在,如果存在则编译通过,反之报错。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
2
3
4
# @Deprecated
这个注解常用于提醒开发被加上注解的方法已经不推荐使用了。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
2
3
4
5
例如hutool下的excel工具类就有过期的方法,从注释即可看到官方推荐的新用法,这也是我们为什么希望程序员多看源码及注释的原因。
/**
* Sax方式读取Excel07
*
* @param in 输入流
* @param rid Sheet rid,-1表示全部Sheet, 0表示第一个Sheet
* @param rowHandler 行处理器
* @return {@link Excel07SaxReader}
* @since 3.2.0
* @deprecated 请使用 {@link #readBySax(InputStream, int, RowHandler)}
*/
@Deprecated
public static Excel07SaxReader read07BySax(InputStream in, int rid, RowHandler rowHandler) {
try {
return new Excel07SaxReader(rowHandler).read(in, rid);
} catch (NoClassDefFoundError e) {
throw new DependencyException(ObjectUtil.defaultIfNull(e.getCause(), e), PoiChecker.NO_POI_ERROR_MSG);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# @SuppressWarnings
因为某些原因导致编码在编译时告警,通过该注解即可压制程序中某些警告(即IDEA不报黄)。
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
2
3
4
5
6
# 什么是元注解?它是用来做什么的?
# 简介
除了直接使用JDK 定义好的注解,我们还可以自定义注解,在JDK 1.5中提供了4个标准的用来对注解类型进行注解的注解类,我们称之为 meta-annotation(元注解),他们分别是:
- @Target
- @Retention
- @Documented
- @Inherited
# @Target
Target用于描述注解能够作用的位置,有3个值,可以通过ElementType获取。
ElementType取值:
TYPE:可以作用于类上。METHOD:可以作用于方法上。FIELD:可以作用于成员变量上。
如下注解 只可作用于类和字段上,在函数上则会报错:
@Target({ElementType.TYPE,ElementType.FIELD})
public @interface MyAnno3 {
}
2
3
4
如下所示,我们将Target在字段上的注解用在方法上就报错了。

# @Retention
描述注解被保留的阶段,例如 @Retention(RetentionPolicy.RUNTIME)即表示当前被描述的注解,会保留到class字节码文件中,并被JVM读取到。
# @Documented
描述注解是否被抽取到接口文档中,这里我们举个例子演示一下,我们自定义注解Myanno3,如下代码,Myanno3加入注解@Documented后,worker类使用该Myanno3注解,当使用worker生成文档后该注解会被显示在使用注解的类、函数、字段上:
import java.lang.annotation.*;
/**
元注解:用于描述注解的注解
* @Target:描述注解能够作用的位置
* @Retention:描述注解被保留的阶段
* @Documented:描述注解是否被抽取到api文档中
* @Inherited:描述注解是否被子类继承
*
*/
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface MyAnno3 {
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在Worker上使用MyAnno注解,可以看到我们在类、字段、方法上都用到了注解。
@MyAnno(value=12,per = Person.P1,anno2 = @MyAnno2,strs="bbb",name = "李四")
@MyAnno3
public class Worker {
@MyAnno3
public String name = "aaa";
@MyAnno3
public void show(){
}
}
2
3
4
5
6
7
8
9
10
11
12
13
使用javadoc命令生成文档后,可以看到该类的myAnno3的注解都存在:

# @Inherited
描述注解是否被子类继承
# 基于注解实现简单的测试框架
# 需求描述
我们希望自定义一个注解check,通过check注解获取我们编写方法并执行,查看其是正常输出还是异常。
# 定义注解
首先定义check注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Check {
}
2
3
4
5
6
7
8
9
10
# 实现计算类
需要测试的类代码,逻辑比较简单,就是一些简单的计算。
/**
* 计算类
*/
public class Calculator {
/**
* 加法,存在空指针异常
*/
@Check
public void add() {
String str = null;
str.toString();
System.out.println("1 + 0 =" + (1 + 0));
}
//减法
@Check
public void sub() {
System.out.println("1 - 0 =" + (1 - 0));
}
//乘法
@Check
public void mul() {
System.out.println("1 * 0 =" + (1 * 0));
}
/**
* 除法 被除数为0
*/
@Check
public void div() {
System.out.println("1 / 0 =" + (1 / 0));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# 基于check注解反射调用
因为我们对方法指明了注解,所以在执行时可以根据注解的存在判断当前方法是否需要检查,对于需要检查的方法我们直接通过反射的方式完成调用,若报错则将信息写入error日志中。
public class CheckUtil {
public static void main(String[] args) throws IOException {
Calculator calculator = new Calculator();
//反射获取calculator的方法
Class<? extends Calculator> clzz = calculator.getClass();
Method[] methods = clzz.getMethods();
String methodName = null;
int errCount = 0;
try (BufferedWriter writer = new BufferedWriter(new FileWriter("err.log"))) {
for (Method method : methods) {
methodName = method.getName();
//判断是否有check注解若有则反射调用
if (method.isAnnotationPresent(Check.class)) {
try {
method.invoke(calculator);
} catch (Exception e) {
//有报错则将错误写入日志
String msg = String.format("出错了,第%d个错误,方法名:%s,错误原因:%s", ++errCount, methodName, e.getCause().getMessage());
System.out.println(msg);
writer.write(msg);
writer.flush();
writer.newLine();
}
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
错误日志输出结果:
出错了,第1个错误,方法名:add,错误原因:null
出错了,第2个错误,方法名:div,错误原因:/ by zero
2
3
# 注解的本质是什么呢?
聊到注解的本质,其实最简单的方法就是反编译看看实质,代码如下所示:
@Retention(RetentionPolicy.RUNTIME)
public @interface Foo{
String[] value();
boolean bar();
}
2
3
4
5
6
使用以下命令完成编译生成字节码还反编译为java文件
javac Foo.java
javap -c Foo.class
2
3
我们就可以得出如下一段输出,可以看到看注解的本质就是一个接口,然后内部包含一个获取字符串数组的value方法和返回bar布尔值的接口方法:
Compiled from "Foo.java"
public interface com.shark.annotation.Foo extends java.lang.annotation.Annotation {
public abstract java.lang.String[] value();
public abstract boolean bar();
}
2
3
4
5
6
7
8
# 注解与反射的关系原理实践
其实我们在日常使用Spring框架时经常会用到注解,例如Service("userSerivice"),那么请问Spring是如何通过注解拿到这个bean的值的呢?
我们不妨自定义一个注解来实验这个问题,首先我们自定义一个service:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
String value();
String scope();
}
2
3
4
5
6
然后我们编写如下一段代码进行测试,代码的逻辑比较简单即获取注解并拿到value和scope:
@Service(value = "userService", scope = "singleton")
public class Main {
public static void main(String[] args) {
Service service = Main.class.getAnnotation(Service.class);
System.out.println(service.value());
System.out.println(service.scope());
}
}
2
3
4
5
6
7
8
9
10
执行前针对程序键入如下参数以保证生成代理字节码文件:
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
在查看项目文件中会出现下图这样一个名为$Proxy0和$Proxy1的class文件文件,不难猜出注解的运行时会对被注解的类生成一个动态代理文件,查看源码内部可以看到几个方法的定义和初始化,这其中就包含我们的注解的scope和value方法:
//......
private static Method m4;
private static Method m3;
//......
static {
try {
//......
m4 = Class.forName("com.sharkchili.Service").getMethod("scope");
//......
m3 = Class.forName("com.sharkchili.Service").getMethod("value");
} catch (NoSuchMethodException var2) {
//......
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这里我们直接以scope为例,当我通过注解的scope()方法获取值时,本质上就是调用动态代理的InvocationHandler(即下面的变量h)通过反射调用m4即上文运行时加载的scope方法,从而完成值的返回:
public final String scope() throws {
try {
return (String)super.h.invoke(this, m4, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
2
3
4
5
6
7
8
9
# 常见面试题
# 注解的作用是什么?
java注解本质上是一种元数据,通俗来说就是在类或者方法代码进行附加信息标注,注解本身不会直接影响程序的运行,它只有在编译或运行时才能进行代码生成、验证、动态增强的操作。
最典型的作用有:
- 编译时检查:例如检查重写的Override是否正确覆盖了父类方法,泛型集合内部是否存储的正确的类型对象。
- 代码生成:最经典的运用的Lombok在运行时动态生成常见的
getter和setter方法。 - 运行时修改:例如Spring框架中的
AOP时通过注解获取相应信息进行某些拓展或者执行某些能力。 - 文档生成
- 配置:通过注解在记录某些程序的配置,例如
Spring中的@Value等注解。
# 小结
我是sharkchili,CSDN Java 领域博客专家,开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号: 写代码的SharkChili,同时我的公众号也有我精心整理的并发编程、JVM、MySQL数据库个人专栏导航。

# 参考
java注解的基本原理:https://juejin.cn/post/6844903636733001741#heading-0 (opens new window)