目录
学习内容
- 怎么使用内置注解
- 怎么自定义注解
- 反射中怎么获取注解内容
1.注解概念
- Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。
- Java 语言中的类、方法、变量、参数和包等都可以被标注。
- 和注释不同,Java 标注可以通过反射获取标注内容。
- 在编译器生成类文件时,标注可以被嵌入到字节码中。
- Java 虚拟机可以保留标注内容,在运行时可以通过反射获取到标注内容 。 当然它也支持自定义 Java 标注。
- 总之,注解就是可以把注释嵌入到字节码文件,被jvm通过反射读取其中内容的一种注释机制。
- @Prop_Anno(className="annotationTest.Demo01",methodName = "show")对于自定义注解的解析,先获取字节码文件对象,继而获取该注解对象,通过注解对象调用注解属性(即注解中定义的抽象方法)获取注解参数值
注解本质
- 注解本质上就是一个接口,该接口默认继承Annotation接口
public interface Demo04MyAnno extends java.lang.annotation.Annotation{}
- 可以通过反编译来查看注解的本质:首先IDEA中创建一个注解
public @interface Demo04MyAnno {}
- 将来该文件会被编译为字节码文件,我们可以通过反编译来看注解本质,
- 首先将该注解文件(.java)复制到一个文件夹中,并将其中的包名删除,
- 然后打开此文件的命令行窗口,先将该java文件编译一下,生成字节码文件.class
javac Demo04MyAnno.java
- 进行反编译将字节码文件反编译为java文件Demo04MyAnno.java
javap Demo04MyAnno.class
- 该java文件就长这样;
public interface Demo04MyAnno extends java.lang.annotation.Annotation{}
2.注解三大作用
生成文档
- ①编写文档:通过代码里标识的注解生成文档[生成doc文档]
- 比如jdk1.8的文档(即API文档),它就是抽取代码中的文档注解生成的。
- 下面可以演示注解生成文档:
在annotation包中创建类Demo02Anno,在类中写一些方法和文档注释,文档注释中使用了些注解,这些注解就可以被抽取到javadoc文档中,首先在桌面创建文件夹,把Demo02Anno类复制进入,然后为了方便抽取,打开该类把包名删除然后在当前目录上面的路径显示框中输入CMD,然后回车即可弹出命令行窗口,然后窗口中输入javac Demo02Anno.java这个命令,回车后就生成很多html、还有css和js,然后点开index.html打开,就会有一 个文档生成,里面有一个类Demo02Anno,这个文档在描述Demo02Anno这个类时,出现了中文乱码,可以重新打开 Demo02Anno.java文件,在格式下拉单中选择"以ANSI格式编码",即系统默认的GBK编码。然后重新在命令行输入命令
代码分析
- 在运行的代码中,可以通过反射来解析注解,获取注解内容
编译检查
- 编译检查:通过代码里标识的注解让编译器能够实现基本的编译检查[如Override]
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", a1='" + a1 + '\'' +
", a2='" + a2 + '\'' +
", b='" + b + '\'' +
", c='" + c + '\'' +
", d='" + d + '\'' +
'}';
}
- 比如在Person类中的toString()方法就有一个注解@Override,这个注解就是用来编译检查的,检查被这个注解标识的方法是否是复写的父类方法(从父类中重写的方法),如果不是则编译失败,比如将toString()改为toString1(),就会编译失败,报出错误Method does not override method from its superclass.
3.JDK预定义(内置)注解
1️⃣@Override:
- 检测被注解标识的方法是否是继承自父类(接口)的。(即是否是重写的父类方法)
2️⃣@Deprecated:
- 该注解标注的内容,表示已经过时了。但还是可以调用的
3️⃣@SuppressWarnings:
- 压制警告.一般传递参数all,@SuppressWarnings(all)
- 比如定义的变量、类没有被引用就会变成灰色,通过注解压制这些警告
package annotation;
@SuppressWarnings("all") //压制该类中所有警告
public class Demo03Anno {
@Override
public String toString(){
return super.toString();
}
//此方法已被废弃,请通过show02来操作
@Deprecated
public void show01(int index){
System.out.println(index);
//过一段时间,有新版本发布,该方法有缺陷,写了show02方法对该方法做了
//替换,这个方法就过时了,不建议使用,可以使用@Deprecated注解标识该方法,
//表示该方法过时,但是还是可以使用的,是不可删除的,毕竟后期要使用第一版的话
//还可以调用show01方法
}
public void show02(){
//替代show01()方法的
if(index<0||index>100){
throw new RunntimeException("index不合理");//抛出一个异常
}
System.out.println(index);
}
public void demo(){
show01(); //表示show01()方法已过时,但可以使用
}
}
4.自定义注解
4.1.注解中属性定义
- 注解中的属性就是接口中抽象方法;(接口中抽象方法可以省略public abstract修饰)
属性的要求:
- ①属性的返回值类型只有以下几种:(不能有void)
- 基本数据类型
- String
- 枚举
- 注解
- 以上类型的数组
- ②定义了属性,在使用时需要给属性赋值
- 1.如果定义属性时,使用default关键字给属性默认初始化值,则使用注解时可不进行属性赋值
- 2.如果只有一个属性需要赋值,且属性名称为value,则赋值时value可省略,直接定义值即可
- 3.属性是数组,数组赋值时,值使用{}包裹,如果数组只有一个值,则{}可省
- 比如在类Demo04MyAnno3中使用自定义的注解Demo04MyAnno,并给注解中的属性age和name赋值,即如下
- 当然也可以在定义注解属性时设置默认值比如String name() default "张三";
- @SuppressWarnings("all")这个注解赋值时并没有属性名,说明属性名一定是value.
- 注解中的每一个方法,实际是声明的注解配置参数
- 注解元素必须要有值,我们定义注解元素时,经常使用空字符串、0作为默认值。
@Demo04MyAnno(age=1,name="张三")
public class Demo04MyAnno3(){}
4.2.注解中元注解定义
一.元注解:
- 用于描述注解的注解,即可以使用jdk中定义的元注解来描述我们自己定义的注解。
二.JDK预定义的元注解:
1.@Target及其属性值:
- 描述注解能够作用的位置(类/方法/文件等)
- ElementType取值(注解的用途类型):
- TYPE:可以作用在类上
- METHOD:可以作用在方法上
- FIELD:可以作用于成员变量上
2.@Retention及其属性值:
- 描述注解被保留的阶段(java三个阶段,源码阶段、class阶段、runtime阶段)
- @Retention(RetentionPolicy.RUNTIME)表示被描述的注解,被保留到class字节码文件中,并被jvm读取到,一般都是使用runtime这个属性值
- 另外还有两个属值SOURCE和CLASS,前者所描述的注解不会被保留到字节码文件中,而后者描述的注解不会被jvm读取到。
3.@Documented:
- 描述注解是否被抽取到api文档中
- @Documented注解:表示在类中被该注解标识的信息可以抽取到api文档中。需要把类文件和自定义注解文件都复制到文件夹中操作。
4.@Inherited:
- 描述注解是否被子类继承
Target元注解的属性值:
- 查看Target注解的源码
public @interface Target { ElementType[] value();}
- 可知它的属性返回值类型是ElementType类型数组,再查看ElementType源码,它是枚举类型enum, 即
public enum ElementType{TYPE,FIELD,METHOD,.....},
- Target注解常用的属性值为TYPE、FILED、METHOD分别表示注解能够作用的位置是类、成员变量、成员方法上。
- 若自定义的注解期望可以作用在类上,那么在定义注解时,需要设置元注解Target, 即如下,由于属性名value,且此时只有一个值,所以可以省略属性名直接写值,
@Target(ElementType.TYPE)
- 若表示该注解Demo05MyAnno可以作用在类上、方法上、成员变量上,使用ElementType[]数组存储值:
@Target(value={ElementType.TYPE,ElementType.METHOD,ElementType.FIELD})
- 由于属性名为value,且只有一个值{}赋值,所以可以省略属性名,直接定义值:
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.FIELD})
Retention元注解属性值
自定义注解代码-属性定义
- 定义一个枚举类。供Demo04MyAnno这个自定义注解中定义属性时作为返回值类型
package annotation;
/**
* 定义一个枚举类。供Demo04MyAnno这个自定义注解中定义属性时作为返回值类型
*/
public enum Demo04Enum {
p1,p2;
}
- 定义该注解,用于Demo04MyAnno这个自定义注解中定义属性的返回值类型
package annotation;
/**
* 定义该注解,用于Demo04MyAnno这个自定义注解中定义属性的返回值类型
*/
public @interface Demo04MyAnno2 {}
- 自定义一个注解Demo04MyAnno
package annotation;
/**自定义注解中的属性定义:
* 自定义一个注解Demo04MyAnno,注解中的属性即抽象方法(public abstract可省):
*/
public @interface Demo04MyAnno {
//注解中定义的属性返回值为基本类型int
int age();
//注解中定义的属性返回值为字符串类型,该属性默认值为"张三",使用注解时可以不赋值
String name() default "张三";
//属性类型为枚举类型
Demo04Enum enum1();
//属性类型为注解类型
Demo04MyAnno2 anno2();
//属性类型为字符串数组
String[] strs();
}
自定义注解完整定义(元注解+属性)
package annotation;
import javax.xml.bind.Element;
import java.lang.annotation.*;
//自定义注解中元注解的定义:
/*1.表示该注解Demo05MyAnno可以作用在类上、方法上、成员变量上
由于属性名为value,且只有一个值{}赋值,所以可以省略属性名,直接定义值
@Target({ElementType.TYPE,ElementType.METHOD,ElementType.FIELD})
*/
@Target(value={ElementType.TYPE,ElementType.METHOD,ElementType.FIELD})
/*2.@Retention(RetentionPolicy.RUNTIME)表示被描述的注解,会被保留到class字节码文件中,
并被jvm读取到,一般都是使用runtime这个属性值。
另外还有两个属值SOURCE和CLASS,前者所描述注解不被保留到字节码文件,后者描述注解不被jvm读取
*/
@Retention(RetentionPolicy.RUNTIME)
/*3.@Documented注解:表示在类中被该注解标识的信息可以抽取到api文档中。
需要把类文件和自定义注解文件都复制到文件夹中操作。
*/
@Documented
/*4.@Inherited:表示该注解可被子类继承。
比如在Demo05Class类被@Inherited元注解所标识的注解Demo05MyAnno所描述,
那么Demo05Class类的子类A也被该注解所描述。
**/
public @interface Demo05MyAnno {
//注解中定义的属性返回值为基本类型int
int age();
//注解中定义的属性返回值为字符串类型,该属性默认值为"张三",使用注解时可以不赋值
String name() default "张三";
//属性类型为枚举类型
Demo04Enum enum1();
//属性类型为注解类型
Demo04MyAnno2 anno2();
//属性类型为字符串数组
String[] strs();
}
自定义注解Demo05MyAnno在类中的使用
package annotation;
//自定义注解Demo05MyAnno在类中的使用
//作用于类上的注解
@Demo05MyAnno(age=1,enum1=Demo04Enum.p1,anno2 = @Demo04MyAnno2,strs = {"aa","cc"})
public class Demo05Class {
//作用于变量上的注解
@Demo05MyAnno(age=1,enum1=Demo04Enum.p1,anno2 = @Demo04MyAnno2,strs = {"aa","cc"})
public String name="aaa";
//作用于方法上的注解
@Demo05MyAnno(age=1,enum1=Demo04Enum.p1,anno2 = @Demo04MyAnno2,strs = {"aa","cc"})
public void show(){}
}
4.3.注解架构
01) Annotation与RetentionPolicy 与ElementType
- 每 1 个 Annotation 对象,都会有唯一的 RetentionPolicy 属性;至于 ElementType 属性,则有 1~n 个。
(02) ElementType(注解的用途类型)
- "每 1 个 Annotation" 都与 "1~n 个 ElementType" 关联。当 Annotation 与某个 ElementType 关联 时,就意味着:Annotation有了某种用途。例如,若一个 Annotation 对象是 METHOD 类型,则该 Annotation 只能用来修饰方法。
public enum ElementType {
TYPE, /* 类、接口(包括注释类型)或枚举声明 */
FIELD, /* 字段声明(包括枚举常量) */
METHOD, /* 方法声明 */
PARAMETER, /* 参数声明 */
CONSTRUCTOR, /* 构造方法声明 */
LOCAL_VARIABLE, /* 局部变量声明 */
ANNOTATION_TYPE, /* 注释类型声明 */
PACKAGE /* 包声明 */
}
(03) RetentionPolicy(注解作用域策略)
- "每 1 个 Annotation" 都与 "1 个 RetentionPolicy" 关联
- a) 若 Annotation 的类型为 SOURCE,则意味着:Annotation 仅存在于编译器处理期间,编译器 处理完之后,该 Annotation 就没用了。 例如," @Override" 标志就是一个 Annotation。当它修 饰一个方法的时候,就意味着该方法覆盖父类的方法;并且在编译期间会进行语法检查!编译器处 理完后,"@Override" 就没有任何作用了。
- b) 若 Annotation 的类型为 CLASS,则意味着:编译器将 Annotation 存储于类对应的 .class 文件 中,它是 Annotation 的默认行为。 c) 若 Annotation 的类型为 RUNTIME,则意味着:编译器将 Annotation 存储于 class 文件中,并 且可由JVM读入。
5.使用注解描述配置文件并解析注解
下面使用自定义注解来描述配置文件,使用解析注解来替代加载配置文件操作来获取文件中属性
在反射中写的案例改用注解
- 需求:写一个"框架类",在不改变该类的任何代码的前提下,可以帮我们创建任意类的对象,并且执行其中的任意方法
- 下面使用自定义注解来描述配置文件,使用解析注解来替代加载配置文件来获取文件中属性:在注解中配置属性值
package annotationTest;
public class Demo01 {
public void show(){
System.out.println("Demo01...show()...");
}
}
package annotationTest;
public class Demo02 {
public void show(){
System.out.println("Demo02...show()...");
}
}
package annotationTest;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 下面使用自定义注解来描述配置文件,使用解析注解来替代加载配置文件:
* 配置文件的代码,需要获取配置文件中的属性className全类名和要执行的方法名
* methodName,配置文件中的属性在注解中可以作为属性来定义,然后在类中使用注解时直接
* 给注解的属性赋值,这样就不需要配置文件了。
*/
//设置元注解
@Target({ElementType.TYPE}) //表示该元注解标识的注解Prop_Anno的作用位置是类
@Retention(RetentionPolicy.RUNTIME)//期望注解Prop_Anno保留到runtime阶段
public @interface Prop_Anno {
String className(); //全类名
String methodName();//方法名
/**解析注解时获取注解对象其实就是在内存中生成一个该注解接口的实现类和该对象anno,
* 然后可以使用该对象anno调用注解中抽象方法className()方法和methodName()
* 获取到注解中的属性值(全类名和方法名)
* public class PropImp implements Prop_Anno{
* public String className(){
* return annotationTest.Demo01;
* }
* public String methodName(){
* return "show";
* }
* }
*/
}
package annotationTest;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Properties;
//自定义注解中两个属性:指定全类名和类中要执行的方法,就可创建任意类执行任意方法
@Prop_Anno(className="annotationTest.Demo01",methodName = "show")
public class Reflect_File_Anno {
public static void main(String[] args) throws Exception {
//1.解析注解
//1.1获取该类的字节码文件对象(获取注解定义的位置的对象)
Class<Reflect_File_Anno> cla = Reflect_File_Anno.class;
//2.通过字节码文件对象获取Prop_Anno注解对象
/**解析注解时获取注解对象其实就是在内存中生成一个该注解接口的实现类和该对象anno*/
Prop_Anno anno = cla.getAnnotation(Prop_Anno.class);
//3.调用注解对象中定义的抽象方法,获取返回值
String className = anno.className();
String methodName = anno.methodName();
System.out.println(className);//annotationTest.Demo01
System.out.println(methodName);//show
//4.加载该类进内存,并获取内存中Class对象
Class cn = Class.forName(className);
//5.使用Class对象调用newInStance方法获取该类对象,
Object obj = cn.newInstance();
//6.通过Class对象调用getMethod方法获取方法对象
Method method = cn.getMethod(methodName);
//7.方法对象调用invoke方法执行该方法(即Demo01类中的show方法)
method.invoke(obj);//要指明执行的是obj对象的show方法
//输出Demo01...show()...
}
}
6.注解案例_简单测试框架
小A同学定义了一个计算器类Calculator,类中有加减乘除四个方法,现在要测试这个类有没有Bug,如果使用Junit测试需要一个个方法测试比较麻烦,学了注解后,可以自定义一个注解Check,使用注解写一个简单的测试框架TestCheck,然后在所有要测试的方法上写上注解@Check,然后运行测试类TestCheck,这样这些方法就可以被验证有没有异常,有异常就生产一个bug.txt文档来描述异常信息。
注解案例:简单的测试框架
- 需求:当主方法执行后,会自动执行要检测所有方法(即加了Check注解的方法),判断方法是否有异常,记录到文件中。
- 技术: 1.注解 2.反射
- 先获取字节码文件对象,并调用getMethod方法获取方法对象,
- 然后方法对象调用invoke方法执行此方法对象中封装的方法
定义Calculator类
package annotationTest2;
/**
* 定义的计算器类,使用自定义注解来测试类中方法有无异常
*/
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));
}
//除法
@Check
public void div(){
System.out.println("1/0="+(1/0));//数学异常
}
public void show(){
System.out.println("无bug...");
}
}
定义注解@Check
package annotationTest2;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义Check注解,本次注解不需要传递属性(参数),注解中不需要定义属性
*/
@Target(ElementType.METHOD) //标识注解作用位置在方法上
@Retention(RetentionPolicy.RUNTIME)//标识注解保留到runtime阶段
public @interface Check { }
测试类TestCheck
package annotationTest2;
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class TestCheck {
public static void main(String[] args) throws IOException {
//1.创建计算器对象
Calculator c = new Calculator();
//2.获取class类对象(从而获取所有方法--反射技术)
Class cls = c.getClass();
//3.获取所有的Calculator类中的方法
Method[] methods = cls.getMethods();
int number=0;//记录异常的次数
//创建字符缓冲输出流,参数传递字符输出流,目的地为bug.txt
BufferedWriter bw = new BufferedWriter(new FileWriter("Annotation\\bug.txt"));
//4.遍历方法
for (Method m:methods){
//5.判断方法上是否有Check注解,使用isAnnotationPresent(Class)
if(m.isAnnotationPresent(Check.class)){
//6.有,则执行方法
try {
m.invoke(c);
} catch (Exception e) {
//7.捕获到异常
//记录到文件中
number++;
bw.write(m.getName()+"方法出现异常了");//m.getName获取方法名称
bw.newLine();//换行
bw.write("异常名称:"+e.getCause().getClass().getSimpleName());
bw.newLine();//换行
bw.write("异常原因"+e.getCause().getMessage());
bw.newLine();
bw.write(".......................");
bw.newLine();
}
}
}
bw.write("本次测试一共出现"+number+"次异常");
bw.flush();//把写入到缓冲区的数据刷新到文件bug.txt中
bw.close();
}
}
7.总结