目录
当我们开发 SpringBoot 项目,我们只需对启动类加上 @SpringBootApplication,就能自动装配,不需要编写冗余的 xml 配置。当我们为项目添加 lombok 依赖,使用 @Data 来修饰实体类,我们就不需要编写 getter 和 setter 方法,构造函数等等。@SpringBootApplication,@Data 等像这种以 @开头的代码 就是注解,只需简简单单几个注解,就能帮助我们省略大量冗余的代码,这是一个非常不可思议的事情!但我们往往知道在哪些地方加上合适的注解,不然 IDE 会报错,却不知道其中的原理,那究竟什么是注解呢?
注解(Annotation ), 是 Java5 开始引入的新特性,是放在 Java 源码的类、方法、字段、参数前的一种特殊“注释”,是一种标记、标签。注释往往会被编译器直接忽略,能够被编译器打包进入 class 文件,并执行相应的处理。
按照惯例我们去看下注解的源码:
先新建一个注解文件:MyAnnotation.java
public @interface MyAnnotation {}
复制代码
发现 MyAnnotation 是被 @interface 修饰的,感觉和接口 interface 很像。我们再通过 idea 来看下其的类继承:
MyAnnotation 是继承 Annotation 接口的。我们再反编译一下:
$ javac MyAnnotation.java$ javap -c MyAnnotationCompiled from "MyAnnotation.java"public interface com.zj.ideaprojects.test3.MyAnnotation extends java.lang.annotation.Annotation {}
复制代码
发现生成的字节码中 @interface 变成了 interface,MyAnnotation 而且自动继承了 Annotation
我们由此可以明白:注解本质是一个继承了 Annotation 的特殊接口,所以注解也叫声明式接口
注解的分类
一般常用的注解可以分为三大类:
Java 自带的标准注解
例如:
@Override:让编译器检查该方法是否正确地实现了覆写;@SuppressWarnings:告诉编译器忽略此处代码产生的警告。@Deprecated:标记过时的元素,这个我们经常在日常开发中经常碰到。@FunctionalInterface:表明函数式接口注解
元注解
元注解是能够用于定义注解的注解,或者说元注解是一种基本注解,包括 @Retention、@Target、@Inherited、@Documented、@Repeatable 等元注解也是 Java 自带的标准注解,只不过用于修饰注解,比较特殊。
@Retention
注解的保留策略, @Retention 定义了 Annotation 的生命周期。当 @Retention 应用到一个注解上的时候,它解释说明了这个注解的的存活时间。它的参数:
如果 @Retention 不存在,则该 Annotation 默认为 RetentionPolicy.CLASS
示例:
@Retention(RetentionPolicy.RUNTIME)public @interface TestAnnotation { }
复制代码
我们自定义的 TestAnnotation 可以在程序运行中被获取到
@Documented
它的作用是 用于制作文档,将注解中的元素包含到 doc 中一般不怎么用到,了解即可
@Target
@Target 指定了注解可以修饰哪些地方, 比如方法、成员变量、还是包等等当一个注解被 @Target 注解时,这个注解就被限定了运用的场景。常用的参数如下:
@Inherited
@Inherited 修饰一个类时,表明它的注解可以被其子类继承,缺省情况默认是不继承的。换句话说:如果一个子类想获取到父类上的注解信息,那么必须在父类上使用的注解上面 加上 @Inherit 关键字注意:
我们来看一个示例:定义一个注解:
@Inherited@Target(ElementType.TYPE)public @interface MyReport { String name() default ""; int value() default 0;}
复制代码
使用这个注解:
@MyReport(value=1)public class Teacher {}
复制代码
则它的子类默认继承了该注解:
public class Student extends Teacher{ }
复制代码
idea 查看类的继承关系:
@Repeatable
使用 @Repeatable 这个元注解来申明注解,表示这个声明的注解是可重复的 @Repeatable 是 Java 1.8 才加进来的,所以算是一个新的特性。
比如:一个人他既会下棋又会做饭,他还会唱歌。
@Repeatable(MyReport.class)@Target(ElementType.TYPE)public @interface MyReport { String name() default ""; int value() default 0;}@MyReport(value=0)@MyReport(value=1)@MyReport(value=2)public class Man{}
复制代码
自定义注解
我们可以根据自己的需求定义注解,一般分为以下几步:
新建注解文件, @interface 定义注解
public @interface MyReport { }
复制代码
添加参数、默认值
public @interface MyReport { String name() default ""; int value() default 0;}
复制代码
用元注解配置注解
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface MyReport { String name() default ""; int value() default 0;}
复制代码
我们一般设置 @Target 和 @Retention 就够了,其中 @Retention 一般设置为 RUNTIME,因为我们自定义的注解通常需要在程序运行中读取。
自定义注解的读取
读到这里,相信大家已经明白了 如何定义和使用注解,我们接下来 就需要如何将注解利用起来。我们知道读取注解, 需要用到 java 的反射
推荐阅读笔者之前写过关于反射的文章:https://mp.weixin.qq.com/s/_n8HTIjkw7Emcunpb4-Iwg
我们先来写一个简单的示例--反射获取注解:
通过前文的了解,先来改造一下 MyAnnotation.java
@Retention(RetentionPolicy.RUNTIME)//确保程序运行中,能够读取到该注解!!!public @interface MyAnnotation { String msg() default "no msg";}
复制代码
我们再用 @MyAnnotation 来修饰 Person 类的类名、属性、和方法
@MyAnnotation(msg = "this person class")//注解 修饰类public class Person { private String name;//姓名 private String sex;//性别 @MyAnnotation(msg = "this person field public")//注解 修饰 public属性 public int height;//身高 @MyAnnotation(msg = "this person field private")//注解 修饰 private属性 private int weight;//体重 public void sleep(){ System.out.println(this.name+"--"+ "睡觉"); } public void eat(){ System.out.println("吃饭"); } @MyAnnotation(msg = "this person method")//注解 修饰方法 public void dance(){ System.out.println("跳舞"); }}
复制代码
最后我们写一个测试类
public class TestAn { public static void main(String[] args) throws NoSuchFieldException, NoSuchMethodException { //获取Person class 实例 Class<Person> c1 = Person.class; //反射获取 类上的注解 MyAnnotation classAnnotation = c1.getAnnotation(MyAnnotation.class); System.out.println(classAnnotation.msg()); //反射获取 private属性上的注解 Field we = c1.getDeclaredField("weight"); MyAnnotation fieldAnnotation = we.getAnnotation(MyAnnotation.class); System.out.println(fieldAnnotation.msg()); //反射获取 public属性上的注解 Field he = c1.getDeclaredField("height"); MyAnnotation field2Annotation = he.getAnnotation(MyAnnotation.class); System.out.println(field2Annotation.msg()); //反射获取 方法上的注解 Method me = c1.getMethod("dance",null); MyAnnotation methodAnnotation = me.getAnnotation(MyAnnotation.class); System.out.println(methodAnnotation.msg()); }}
复制代码
结果:
this person classthis person field privatethis person field publicthis person method
我们通过反射读取 api 时,一般会先去校验这个注解存不存在:
if(c1.isAnnotationPresent(MyAnnotation.class)) { //存在 MyAnnotation 注解}else { //不存在 MyAnnotation 注解}
复制代码
我们发现反射真的很强大,不仅可以读取类的属性、方法、构造器等信息,还可以读取类的注解相关信息。
那反射是如何实现工作的?我们来看下源码:从 c1.getAnnotation(MyAnnotation.class);通过 idea 点进去查看源码,把重点的给贴出来,其他的就省略了
Map<Class<? extends Annotation>, Annotation> declaredAnnotations = AnnotationParser.parseAnnotations(getRawAnnotations(), getConstantPool(), this);
复制代码
parseAnnotations()去分析注解,其第一个参数是 获取原始注解,第二个参数是获取常量池内容
public static Annotation annotationForMap(final Class<? extends Annotation> var0, final Map<String, Object> var1) { return (Annotation)AccessController.doPrivileged(new PrivilegedAction<Annotation>() { public Annotation run() { return (Annotation)Proxy.newProxyInstance(var0.getClassLoader(), new Class[]{var0}, new AnnotationInvocationHandler(var0, var1)); } }); }
复制代码
Proxy._newProxyInstance_(var0.getClassLoader(), new Class[]{var0}, newAnnotationInvocationHandler(var0, var1)创建动态代理,此处 var0 参数是由常量池获取的数据转换而来。我们监听此处的 var0:
可以推断出注解相关的信息 是存放在常量池中的
我们来总结一下,反射调用 getAnnotations(MyAnnotation.class)方法的背后主要操作:解析注解 parseAnnotations()的时候 从该注解类的常量池中取出注解相关的信息,将其转换格式后,通过 newProxyInstance(注解的类加载器,注解的 class 实例 ,AnotationInvocationHandler 实例)来创建代理对象,作为参数传进去,最后返回一个代理实例。其中 AnotationInvocationHandler 类是一个典型的动态代理类, 这边先挖个坑,暂不展开,不然这篇文章是写不完了
关于动态代理类我们只需先知道: 对象的执行方法,交给代理来负责
class AnnotationInvocationHandler implements InvocationHandler, Serializable { ... private final Map<String, Object> memberValues;//存放该注解所有属性的值 private transient volatile Method[] memberMethods = null; AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) { ... } public Object invoke(Object var1, Method var2, Object[] var3) { ... //调用委托类对象的方法,具体等等一些操作 } ...}
复制代码
反射调用 getAnnotations(MyAnnotation.class),返回一个代理实例,我们可以通过这个实例来操作该注解
示例:注解 模拟访问权限控制
当我们引入 springsecurity 来做安全框架,然后只需添加 @PreAuthorize("hasRole('Admin')")注解,就能实现权限的控制,简简单单地一行代码,就优雅地实现了权限控制,觉不觉得很神奇?让我们一起模拟一个出来吧
@Retention(RetentionPolicy.RUNTIME)public @interface MyPreVer { String value() default "no role";}
复制代码
public class ResourceLogin { private String name; @MyPreVer(value = "User") private void rsA() { System.out.println("资源A"); } @MyPreVer(value = "Admin") private void rsB() { System.out.println("资源B"); }}
复制代码
public class TestLogin { public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException { //模拟 用户的权限 String role = "User"; //模拟 需要的权限 final String RoleNeeded = "Admin"; //获取Class实例 Class<ResourceLogin> c1 = ResourceLogin.class; //访问资源A Method meA = c1.getDeclaredMethod("rsA",null); MyPreVer meAPre = meA.getDeclaredAnnotation(MyPreVer.class); if(meAPre.value().equals(RoleNeeded)) {//模拟拦截器 meA.setAccessible(true); meA.invoke(c1.newInstance(),null);//模拟访问资源 }else { System.out.println("骚瑞,你无权访问该资源"); } //访问资源B Method meB = c1.getDeclaredMethod("rsB",null); MyPreVer meBPre = meB.getDeclaredAnnotation(MyPreVer.class); if(meBPre.value().equals(RoleNeeded)) {//模拟拦截器 meB.setAccessible(true); meB.invoke(c1.newInstance());//模拟访问资源 }else { System.out.println("骚瑞,你无权访问该资源"); } }}
复制代码
结果:
骚瑞,你无权访问该资源
资源 B
尾语
注解 是一种标记、标签 来修饰代码,但它不是代码本身的一部分,即注解本身对代码逻辑没有任何影响,如何使用注解完全取决于我们开发者用 Java 反射来读取和使用。我们发现反射真的很强大,不仅可以读取类的属性、方法、构造器等信息,还可以读取类的注解相关信息,以后还会经常遇到它。注解一般用于
编译器可以利用注解来探测错误和检查信息,像 @override 检查是否重写
适合工具类型的软件用的,避免繁琐的代码,生成代码配置,比如 jpa 自动生成 sql,日志注解,权限控制
程序运行时的处理: 某些注解可以在程序运行的时候接受代码的读取,比如我们可以自定义注解
平时我们只知道如何使用注解,却不知道其是如何起作用的,理所当然的往往是我们所忽视的。
评论