一篇文章搞定Java注解^_^
0.序言
自己写这些文章本来想着自己系统的整理下知识,将知识串起来,后面复习用,或者以后年龄大了,去教育机构呀,拿出自己整理的笔记,你看这人爱总结爱分享,文笔也还能看,方便找工作不是。
很开心的是,有人扫描了微信二维码(并不是打赏哈),可能是个新手吧,跟我以前一样,可能看了文章还不懂,觉得讲的不够庸俗,哈哈通俗易懂哈,我就想着后面写文章先把要写的技术是做什么的用生活术语或者段子写出来,不是上来就是一堆定义,使用这些,对看博客的人不友好。
好了开始今天的注解:
从前有个动物饲养员,会往不同的动物身上挂不同的牌子,熊猫就挂上可爱,老鼠就挂上人人喊打,老虎就挂上凶猛的怪兽,当这些贴了标签的动物某天上街表演,小孩子们看到小熊猫都笑嘻嘻的说好可爱呀,要喂竹子吃,看到老鼠就高喊打他打他,看到老虎就躲妈妈怀抱里远远的看着。
注解是和这个类似的,首先我们编码过程中可以写一些自定义注解,给不同的代码贴上这些注解标签,我们还会编写代码去读取这些注解标签,根据标签的不同,可能会进行不同的代码逻辑,如果仅仅定义了标签,而没有对标签写程序处理逻辑,则这些标签就是摆设了。
1.什么是注解
1.1概念
-
注解:说明程序的,给计算机看的。
-
注释:用文字描述程序的,给程序员看的。
百度上的解释:
Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制,是一种代码级别的说明。Java 语言中的类、方法、变量、参数和包等都可以被标注。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容 。 当然它也支持自定义 Java 标注。
概念描述:
- JDK1.5 之后的新特性
- 用来说明程序的
- 使用注解:@注解名称
1.2 作用分类
- 编译检查:通过代码里标识的注解让编译器能够实现基本的编译检查【Override】等
- 编写文档:通过代码里标识的注解生成文档【生成文档doc文档】,API文档是通过抽取代码中的文档注释生成的。
- 简化代码:自定义注解并编写对应的注解解析程序,根据注解和注解的值去执行不同的代码逻辑。例如第三方框架常用注解,均为框架开发者自定义的,简化或者省去了配置文件
例如新建一个类:
public class AnnoDemo {
/**
*
* @param height 身高
* @param weight 体重
* @return
*/
public double addHeightAndWeight(int height,double weight){
return height+weight;
}
}
打开命令行窗口,用 **javadoc AnnoDemo.java **这个命令进行抽取,点开生成文件中的index.html:
代码分析:通过代码里标识的注解对代码进行分析【使用反射】
2. JDK中预定义的一些注解
Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中。
作用在代码的注解是
- @Override - 检查该方法是否是重写方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
- @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
- @SuppressWarnings - 压制警告,一般传递参数all @SuppressWarnings("all")
作用在其他注解的注解(或者说 元注解)是:
- @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
- @Documented - 标记这些注解是否包含在用户文档中。
- @Target - 标记这个注解应该是哪种 Java 成员。
- @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)
从 Java 7 开始,额外添加了 3 个注解:
- @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
- @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
- @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。
3. 自定义注解
3.1 格式
元注解: public @interface 注解名称{}
3.2 注解的本质
注解本质上就是一个接口,该接口默认继承Annotation接口
package java.lang.annotation;
public interface Annotation {
boolean equals(Object obj);
int hashCode();
String toString();
Class<? extends Annotation> annotationType();
}
将以下注解编译过后进行反编译,得到结果:
编译前:
javac MyAnno.java
public @interface MyAnno {
String value();
int size();
}
javap MyAnno.class
反编译后:
//Compiled from "MyAnno.java"
public interface com.huawei.subtitle.portal.MyAnno extends java.lang.annotation.Annotation {
public abstract java.lang.String value();
public abstract int size();
}
3.3 属性
可以理解为接口中可以定义的抽象方法。
要求:
1.属性的返回值类型只能为以下几种:
- 基本数据类型
- String
- 枚举
- 注解
- 以上类型的数组
- 定义了的属性(本质上是抽象方法),在使用时最好进行赋值
- 如果定义属性时,使用default关键字给属性默认初始化值,则使用注解时,可以不进行属性的赋值。
- 如果只有一个属性需要赋值,并且这个属性的名称是value,那么value可以省略,直接赋值即可。
- 数组赋值时,值使用大括号包裹。如果数组中只有一个值,那么{}可以省略
3.4 元注解
概念:用于描述注解的注解。
@Target:描述能够作用的位置
@Target(value = {ElementType.TYPE}) //表示该MyAnno注解只能作用于类上
public @interface MyAnno {
}
其中value中ElementType取值可以有以下几种情况:
- TYPE:可以作用在类上
- METHOD:可以作用在方法上
- FIELD:可以作用于成员变量上
@Target(value = {ElementType.TYPE,ElementType.METHOD,ElementType.FIELD})
//表示该MyAnno注解可以同时作用于类上,方法上和成员变量上
public @interface MyAnno {
}
@Retention:描述注解被保留的阶段
@Retention(RetentionPolicy.RUNTIME):当前被描述的注解,会保留到字节码文件中,并被JVM读取到,一般自己定义的注解都加RUNTIME
@Documented:描述该注解是否会被抽取到api文档中
@Inherited:描述注解是否被子类继承
4. 在程序中使用注解
注解在程序中经常和反射一起使用,注解大多数来说都是用来替换配置文件的,拿之前反射的程序来举例:
被测试的类AnnoTest1:
public class AnnoTest1 {
public void play(){
System.out.println("AnnoTest1 method play()");
}
}
原始的反射代码:
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Properties;
public class ReflectTest {
public static void main(String[] args) throws Exception {
/**
* 前提:不能改变该类的任何代码。可以创建任意类的对象,可以执行任意方法
* 即:拒绝硬编码
*/
//1.加载配置文件
//1.1创建Properties对象
Properties pro = new Properties();
//1.2加载配置文件,转换为一个集合
//1.2.1获取class目录下的配置文件 使用类加载器
ClassLoader classLoader = ReflectTest.class.getClassLoader();
InputStream is = classLoader.getResourceAsStream("pro.properties");
pro.load(is);
//2.获取配置文件中定义的数据
String className = pro.getProperty("className");
String methodName = pro.getProperty("methodName");
//3.加载该类进内存
Class cls = Class.forName(className);
//4.创建对象
Object obj = cls.newInstance();
//5.获取方法对象
Method method = cls.getMethod(methodName);
//6.执行方法
method.invoke(obj);
}
}
对应的配置文件pro:
className=cn.other.annotation.AnnoTest1
methodName=play
新建注解AnnoReflect :
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE) //可以被作用在类上
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnoReflect {
String className();
String methodName();
}
** 使用注解的方式来淘汰配置文件(注释很重要):**
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Properties;
@AnnoReflect(className = "cn.other.annotation.AnnoTest1",methodName = "play")
public class ReflectAnnotationTest {
public static void main(String[] args) throws Exception {
/**
* 前提:不能改变该类的任何代码。可以创建任意类的对象,可以执行任意方法
* 即:拒绝硬编码
*/
//1. 解析注解
//1.1 获取该类的字节码文件对象
Class<ReflectAnnotationTest> rac = ReflectAnnotationTest.class;
//1.2 获取上面的注解对象,其实就是在内存中生成了一个该注解接口的子类实现对象
AnnoReflect an = rac.getAnnotation(AnnoReflect.class);
/*
相当于
public class AnnotationReflect implements AnnoReflect{
public String className(){
return "cn.other.annotation.AnnoTest1";
}
public String methodName(){
return "play";
}
}
*/
//2. 调用注解对象中定义的抽象方法,获取返回值
String className = an.className();
String methodName = an.methodName();
//3.加载该类进内存
Class cls = Class.forName(className);
//4.创建对象
Object obj = cls.newInstance();
//5.获取方法对象
Method method = cls.getMethod(methodName);
//6.执行方法
method.invoke(obj);
}
}
**运行结果: **
使用总结:
在程序中使用注解:获取注解中定义的属性值
- 获取注解定义的位置的对象 (Class, Method, Field)
- 获取指定的注解:getAnnotation(Class)
- 调用注解中的抽象方法获取配置的属性值
案例:简单的测试框架
需求:设计一个框架,检测一个类中的方法使用有异常,并进行统计。
待测试的类
public class calculator {
public void add(){
System.out.println("1+0="+(1+0));
}
public void sub(){
System.out.println("1-0="+(1-0));
}
public void mul(){
System.out.println("1*0="+(1*0));
}
public void div(){
System.out.println("1/0="+(1/0));
}
public void show(){
System.out.println("今天天气真不错!");
}
}
如何实现
首先自定义一个注解:
@Retention(RetentionPolicy.RUNTIME) //运行时
@Target(ElementType.METHOD) //加在方法前面
public @interface Check {
}
然后编写一个类专门用于检查(注意注释):
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
*简单的测试框架
* 当主方法执行后,会自动自行检测所有方法(加了check注解的方法),判断方法是否有异常并记录
*/
public class TestCheck {
public static void main(String[] args) throws IOException {
//1. 创建计算机对象
calculator c = new calculator();
//2. 获取字节码文件对象
Class cls = c.getClass();
//3. 获取所有方法
Method[] methods = cls.getMethods();
int num = 0; //记录出现异常的次数
BufferedWriter bw = new BufferedWriter(new FileWriter("bug.txt"));
for(Method method:methods){
//4. 判断方法上是否有Check注解
if(method.isAnnotationPresent(Check.class)){
//5. 有注解就执行,捕获异常
try {
method.invoke(c);
} catch (Exception e) {
e.printStackTrace();
//6.将异常记录在文件中
num++;
bw.write(method.getName()+"方法出异常了");
bw.newLine();
bw.write("异常的名称是:"+e.getCause().getClass().getSimpleName());
bw.newLine();
bw.write("异常原因:"+e.getCause().getMessage());
bw.newLine();
bw.write("=====================");
bw.newLine();
}
}
}
bw.write("本次测试一共出现"+num+"次异常");
bw.flush();
bw.close();
}
}
在待测试的类中每个需要测试的方法前面都加上@Check
public class calculator {
@Check
public void add(){
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));
}
@Check
public void div(){
System.out.println("1/0="+(1/0));
}
public void show(){
System.out.println("今天天气真不错!");
}
}
运行TestCheck类中的主方法,就会自动检查所有注解@Check的方法是否异常:
小结 :
- 大多数时候,我们会使用注解而不是自定义注解
- 注解给编译器和解析程序用
- 注解不是程序的一部分,可以理解为标签
5.注解补充
有人私信问@Repeatable注解和@Inherited注解怎么使用,这里举例并分析下,抛砖引玉哈哈,大家后面自己写下
5.1 @Inherited的含义是,它所标注的Annotation将具有继承性
假设,我们定义了某个 Annotaion,它的名称是 MyAnnotation,并且 MyAnnotation 被标注为 @Inherited。现在,某个类 Base 使用了MyAnnotation,则 Base 具有了"具有了注解 MyAnnotation";现在,Sub 继承了 Base,由于 MyAnnotation 是 @Inherited的(具有继承性),所以,Sub 也 "具有了注解 MyAnnotation"。另外需要注意的是:获取从父类继承的注解并打印注解的值,大家会发现注解的值也是从父类继承的,如果父类使用注解时没有给值,则使用默认值如果父类使用注解时给了值,则继承的值也是父类给的
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
注解代码演示:
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Inherited;
/**
* 自定义的Annotation。
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited//不希望继承则将这行注释掉哈
@interface Inheritable {
int size() default 1;
}
@Inheritable(size = 5)
class InheritableFather {
public InheritableFather() {
// InheritableBase是否具有 Inheritable Annotation
System.out.println("InheritableFather:" + InheritableFather.class.isAnnotationPresent(Inheritable.class));
}
}
/**
* InheritableSon 类只是继承于 InheritableFather,
*/
public class InheritableSon extends InheritableFather {
public InheritableSon() {
super(); // 调用父类的构造函数
// InheritableSon类是否具有 Inheritable Annotation
System.out.println("InheritableSon:" + InheritableSon.class.isAnnotationPresent(Inheritable.class));
/*获取从父类继承的注解并打印注解的值,大家会发现注解的值也是从父类继承的,如果父类使用注解时没有给值,则使用默认值
如果父类使用注解时给了值,则继承的值也是父类给的,即本例中的5,并不是默认值1
*/
Inheritable annotation = InheritableSon.class.getAnnotation(Inheritable.class);
System.out.println(annotation.size());
}
public static void main(String[] args) {
InheritableSon is = new InheritableSon();
}
}
运行结果:
5.2 @repeatable的含义是,它所标注的Annotation注解可以多次应用于相同的声明或类型
Repeatable 源代码
package java.lang.annotation;
/**
* The annotation type {@code java.lang.annotation.Repeatable} is
* used to indicate that the annotation type whose declaration it
* (meta-)annotates is <em>repeatable</em>. The value of
* {@code @Repeatable} indicates the <em>containing annotation
* type</em> for the repeatable annotation type.
*
* @since 1.8
* @jls 9.6 Annotation Types
* @jls 9.7 Annotations
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Repeatable {
/**
* Indicates the <em>containing annotation type</em> for the
* repeatable annotation type.
* @return the containing annotation type
*/
Class<? extends Annotation> value();
}
@Repeatable 注解用于指示它注解声明的注解类型是可重复的。@Repeatable 的值用于指示一个注解类型,这个注解类型用来存放可重复的注解类型。
初次看这段文字时,觉得比较难以理解(我反正刚开始是理解不了的,这写的什么这是,我也不笨哈)。
首先我们可以看到,@Repeatable 注解的value值需要的是一个继承注解接口的类型,也就是说我们使用的时候,是这种格式@Repeatable(xx.class)
,xx本身也是一个注解。也就是我们想使用@Repeatable这个注解,就天然的需要再来一个注解去辅助它的使用,那这个进行辅助的注解是什么用呐?
我们再想下这个注解的目的,就是多次被使用,每次使用的时候是不是可能有个value的赋值呀,那多个值的话,肯定对应多个注解对象,我们是不是要用数组去存放。这就是这个辅助注解的作用,大家看到这里可能就稍微明白一些了,接下来再举实例带大家看下:
案例功能描述
我们用:用户-角色场景可以通俗的解释 @Repeatable 注解,一个系统中可以设定多个角色,每个角色我们称之为 Role,系统定义的角色如下:
- 系统管理员:system_admin
- 业务管理员:biz_admin
- 客户:custom
一个用户(User)可以拥有其中的一个或者多个角色,用户拥有的角色列表我们称之为 Roles,假设有两个用户 User1、User2 ,他们的权限分别如下:
- User1:system_admin
- User2 :biz_admin、custom
通过 @Repeatable 注解来实现以上功能
定义角色注解 Role
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Roles.class)
public @interface Role {
String value() default "";
}
这里需要说明 @Repeatable(Roles.class)
,它指示在同一个类中 @Role
注解是可以重复使用的,重复的注解被存放至 @Roles
注解中。
定义角色列表注解 Roles
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Roles {
Role[] value();
}
@Roles
注解是如何存放 @Role
注解的呢?它定义了 Role[] value();
用来存放可重复的注解。
这里提出一个问题,如果在同一个类中只有一个可重复的 @Role
注解,那这个值会被存入 @Roles
注解中吗?
定义 User1
@Role("system_admin")
public class User1 {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
定义 User2
@Role("biz_admin")
@Role("custom")
public class User2 {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
进行测试
public class RepeatableDemo {
public static void main(String[] args) {
if(User1.class.isAnnotationPresent(Roles.class)){
Roles roles = User1.class.getAnnotation(Roles.class);
System.out.println("User1的角色如下:");
for (Role role : roles.value()){
System.out.println(role.value());
}
}
if(User2.class.isAnnotationPresent(Roles.class)){
Roles roles = User2.class.getAnnotation(Roles.class);
System.out.println("User2的角色如下:");
for (Role role : roles.value()){
System.out.println(role.value());
}
}
}
}
执行 main 方法,输出如下:
从执行结果中可以看到 User2 的角色列表,通过注解的值我们可以进行用户角色判定。
同时可以看到 User1 的角色是@Role("system_admin")
,但是 User1 的角色没有被输出,在加上一个 Role 的话,就可以输出角色了。由此可见,如果只声明了一个注解 Role(被 @Repeatable 声明的注解),那么注解值是不会被存放至 Roles 注解中的,测试类中不会存在 Roles 注解。
解惑
修改 User1 的代码,为其增加 @Role("custom")
角色:
package org.learn.annotation;
/**
* @author zhibo
* @date 2019/5/31 15:03
*/
@Role("system_admin")
@Role("custom")
public class User1 {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
再次执行 main 方法,结果如下:
与我们的预期一致,由此可见,在写代码时,不能用惯性思维去理解问题,面对不确定的问题,一定要通过代码进行验证。
至此,注解我们已经学完,后续的深刻理解还需要在实际项目中去使用,如通过注解/aop/反射,实现权限校验、日志记录、动态数据源切换等,有问题可随时问哈。
@Repeatable 注解使用参考此篇文章,感谢嘻嘻