写在文章开头
注解也叫元数据,在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;
}
}
使用javadoc
命令进行文档生成

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

代码分析
使用反射完成基于代码里标识的注解,对代码进行分析,从而完成动态增强,这也是Spring
框架对注解最典型的运用(因为示例篇幅较大,代码分析的例子会在下文给出)。
编译检查
通过代码里标识的注解让编译器能够实现基本的编译检查,最典型的就是Override
注解。
public class MsgEventFactory implements EventFactory<MessageModel> {
@Override
public MessageModel newInstance() {
return new MessageModel();
}
}
java内置的三大常见注解
@Override
源码如下所示,可以看到元注解有target
和Retention
,其中Retention
为source
,即在编译时
检查当前子类重写的方法在父类中是否存在,如果存在则编译通过,反之报错。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Deprecated
这个注解常用于提醒开发被加上注解的方法已经不推荐使用了。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}
例如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);
}
}
@SuppressWarnings
因为某些原因导致编码在编译时告警,通过该注解即可压制程序中某些警告(即IDEA不报黄)。
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
什么是元注解?它是用来做什么的?
简介
除了直接使用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 {
}
如下所示,我们将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 {
}
在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(){
}
}
使用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 {
}
实现计算类
需要测试的类代码,逻辑比较简单,就是一些简单的计算。
/**
* 计算类
*/
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));
}
}
基于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();
}
}
}
}
}
}
错误日志输出结果:
出错了,第1个错误,方法名:add,错误原因:null
出错了,第2个错误,方法名:div,错误原因:/ by zero
注解的本质是什么呢?
聊到注解的本质,其实最简单的方法就是反编译看看实质,代码如下所示:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Foo{
String[] value();
boolean bar();
}
使用以下命令完成编译生成字节码还反编译为java
文件
javac Foo.java
javap -c Foo.class
我们就可以得出如下一段输出,可以看到看注解的本质就是一个接口。
Compiled from "Foo.java"
public interface com.shark.wiki.interview.javaBase.annotation.Foo extends java.lang.annotation.Annotation {
public abstract java.lang.String[] value();
public abstract boolean bar();
}
注解与反射的关系原理实践
其实我们在日常使用Spring
框架时经常会用到注解,例如Service("userSerivice")
,那么请问Spring
是如何通过注解拿到这个bean
的值的呢?
我们不妨自定义一个注解来实验这个问题,首先我们自定义一个service
:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Service {
String value();
String scope();
}
然后我们编写如下一段代码,获取注解的value
和scope
,执行前在idea
的vm option
键入这一一段jvm
命令-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
生成动态代理的class
文件。
@Service(value = "userService", scope = "singleton")
public class Test {
public static void main(String[] args) throws Exception{
Service service = Test.class.getAnnotation(Service.class);
System.out.println(service.value());
System.out.println(service.scope());
}
}
在查看项目文件中会出现下图这样一个名为proxy
的class
文件文件,不难猜出注解的运行时会对被注解的类生成一个动态代理。

可以得出获取注解时,Java
会为注解生成一个代理类,为scope
和value
生成一个方法。在静态代码快中使用反射初始化将值赋值给m3、m4
,后续我们需要获取值时直接通过m3
、m4
的invoke
方法获取值。
public final class $Proxy1 extends Proxy implements Service {
.....
private static Method m4;
...
private static Method m3;
public $Proxy1(InvocationHandler var1) throws {
super(var1);
}
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);
}
}
public final String value() throws {
try {
return (String)super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
// 静态代码块中完成scope和value的方法的初始化
static {
try {
......
m4 = Class.forName("com.guide.keyword.rewrite.Service").getMethod("scope");
......
m3 = Class.forName("com.guide.keyword.rewrite.Service").getMethod("value");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
以scope
为例,我们看到了这样一段代码,不难看出我们之前调用的Service.scope()
就是使用这个方法。这里有个invoke
,我们点进去看看调用
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);
}
}
这时候我们看到一个接口,没有看到具体实现,没关系,源码的设计者命名永远是合理的,所以,我们完完全全可以通过查找与注解命名相关的继承类。

这时候笔者就发现了这个

具体实现源码如下,关键笔者都在代码中注释了:
public Object invoke(Object var1, Method var2, Object[] var3) {
//var2就是上一步传入的scope
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
byte var7 = -1;
//根据方法名的hashCode决定var7的值。
switch(var4.hashCode()) {
........
}
//根据var2标记var7,然后从返回响应的var值,这里scope就走最后一个分支了返回了scope的字符串
switch(var7) {
.......
default:
//最终返回我们的设置的字符串var6
Object var6 = this.memberValues.get(var4);
.......
return var6;
}
}
}
}
小结
我是sharkchili,CSDN Java 领域博客专家,开源项目—JavaGuide contributor,我想写一些有意思的东西,希望对你有帮助,如果你想实时收到我写的硬核的文章也欢迎你关注我的公众号:写代码的SharkChili,同时我的公众号也有我精心整理的并发编程、JVM、MySQL数据库个人专栏导航。
参考
java注解的基本原理:https://juejin.cn/post/6844903636733001741#heading-0




