Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。Java 注解是从 Java1.5 开始添加到 Java 的。(比较抽象的概念)
初学者可以这样理解注解:想像代码具有生命,注解就是对于代码中某些鲜活个体的贴上去的一张标签。简化来讲,注解如同一张标签。(引用某篇博主观点,通俗易懂!!)
https://blog.csdn.net/shengzhu1/article/details/81271409
下面创建了一个注解,那么注解的的使用方法是什么呢?
@interface Test{ } @Test() public class Cmath{ }
创建一个类 Cmath,然后在类定义的地方加上 @Test 就可以用 Testn 注解这个类了。
你可以简单理解为将 Test 这张标签贴到 Camth这个类上面。
不过,要想注解能够正常工作,还需要介绍一下一个新的概念那就是元注解。
元注解: 注解的注解
元注解是可以注解到注解上的注解,或者说元注解是一种基本注解,但是它能够应用到其它的注解上面。
如果难于理解的话,你可以这样理解。元注解也是一张标签,但是它是一张特殊的标签,它的作用和目的就是给其他普通的标签进行解释说明的。
查看Target源码: 注解的作用目标(修饰方法、类、还是属性?)
常见的修饰:
- ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上
- ElementType.FIELD:允许作用在属性字段上
- ElementType.METHOD:允许作用在方法上
- ElementType.PARAMETER:允许作用在方法参数上
- ElementType.CONSTRUCTOR:允许作用在构造器上
- ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上
- ElementType.ANNOTATION_TYPE:允许作用在注解上
- ElementType.PACKAGE:允许作用在包上
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Target { /** * Returns an array of the kinds of elements an annotation type * can be applied to. * @return an array of the kinds of elements an annotation type * can be applied to */ ElementType[] value(); }
查看Retention源码:注解的生命周期(保存到运行时还是编译时或者永久?) 默认注解保存到编译时期
- RetentionPolicy.SOURCE:当前注解编译期可见,不会写入 class 文件
- RetentionPolicy.CLASS:类加载阶段丢弃,会写入 class 文件
- RetentionPolicy.RUNTIME:永久保存,可以反射获取(被保存在class字节码文件中,并被JVM读取)
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Retention { /** * Returns the retention policy. * @return the retention policy */ RetentionPolicy value(); }
@Decumented:描述注解是否被抽取到api文档中
@Inherited:描述注解是否被子类继承
注解的属性
注解的属性也叫做成员变量。注解只有成员变量,没有方法。注解的成员变量在注解的定义中以“无形参的方法”形式来声明,其方法名定义了该成员变量的名字,其返回值定义了该成员变量的类型。
@interface Test{ String value(); int id(); } @Test(value ="数学",id=20)
上面代码定义了 Test 这个注解中拥有 id 和 value两个属性。在使用的时候,我们应该给它们进行赋值。
value这个参数名字,对所有注解来说,比较特殊,是默认的注解参数名字。
赋值的方式是在注解的括号内以 value=" "形式,多个属性之前用 ,隔开。(有一个参数时,可以直接写"数学")
@interface Test{ String value() default "y"; int id() default 4; } @Test()
属性可以定义默认值default,定义默认值以后,Test里面可以不写参数
注解属性的返回值类型(2020/1/21):
1.基本数据类型 2.String 3.枚举 4.注解 5.以上类型的数组
定义了属性后,使用的时候需要给属性赋值
1.如果用了default关键字给属性默认初始化值,则使用该注解时,可以不进行属性的赋值
2.如果只有一个属性,可以直接定义值
3.数组赋值时,使用 { }包裹,如果数组中只有一个值,可以省略{ }
注解的作用:
注解到底有什么用?
我们不妨将目光放到 Java 官方文档上来。
文章开始的时候,我用标签来类比注解。但标签比喻只是手段,而不是目的。为的是让大家在初次学习注解时能够不被那些抽象的新概念搞懵。既然现在,我们已经对注解有所了解,我们不妨再仔细阅读官方最严谨的文档。
注解是一系列元数据,它提供数据用来解释程序代码,但是注解并非是所解释的代码本身的一部分。注解对于代码的运行效果没有直接影响。
注解有许多用处,主要如下:
- 提供信息给编译器: 编译器可以利用注解来探测错误和警告信息
- 编译阶段时的处理: 软件工具可以用来利用注解信息来生成代码、Html文档或者做其它相应处理。
- 运行时的处理: 某些注解可以在程序运行的时候接受代码的提取 (反射)
常见的注解测试:Override Deprecated SuppressWarnning
自定义注解练习
练习1测试♥:找出所有方法的DevelopInfo注解(用于程序员的老大查看每个方法的开发人员与日期√,接口还是要注明!)
import java.lang.annotation.*; import java.lang.reflect.Method; import java.util.Arrays; /** * 自定义注解类型 记录开发人员的 姓名String 开发日期String * value这个参数名字,对于所有注解来说,比较特殊,例如: * @DevelopInfo("张三") 就是把"张三"传给注解默认的参数名字了value * 也可以把参数的名字写出来,如@DevelopInfo(value = "张三") * */ @Target({ElementType.METHOD}) //Target限制修饰的类型,说明DevelopInfo只能修饰方法 @Retention(RetentionPolicy.RUNTIME) // 默认注解在编译时期存在,说明DevelopInfo注解信息可以保留到运行的时候 @interface DevelopInfo{ // 用来修饰类,方法,变量,方法参数... String[] value() default "nobody"; // 注解的参数 参数名字叫 value String date(); } /** * 新定义一个修饰方法的注解接口,可以保留到运行的时候 */ @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @interface Check{ int value(); } class CMath{ //@DevelopInfo(value = "张三", date = "2019-11-2") @DevelopInfo(value = {"张三", "李四"}, date = "2019-11-2") @Check(10) int sum(int a, int b){ return a + b; } @DevelopInfo(value = {"高洋", "刘硕"}, date = "2019-10-2") int minor(int a, int b){ return a - b; } @DevelopInfo(value = "张航", date = "2019-9-12") //数组中可以多个值{" "," "},也可以传入一个值 "" int div(int a, int b){ return a / b; } @DevelopInfo(value = "吴雷", date = "2019-9-12") int mix(int a, int b){ return a * b; } } public class 注解 { public static void main(String[] args) { /** * 通过Java反射,获取CMath这个类所有方法的注解开发信息 */ Class<CMath> c = CMath.class; /** * 获取所有方法的method * getField(只能public) getDeclaredField(公有,私有,保护) */ Method[] methods = c.getDeclaredMethods(); for (int i = 0; i < methods.length; i++) { Annotation[] anns = methods[i].getDeclaredAnnotations(); //获取方法的注解 if(anns != null){ // 遍历methods[i]当前方法的所有注解 for (int j = 0; j < anns.length; j++) { if(anns[j] instanceof DevelopInfo){ //判断注解是否实现了DevelopInfo接口 DevelopInfo di = (DevelopInfo)anns[j]; //把注解类型转化为接口类型 System.out.println("方法名:" + methods[i].getName() + " 开发者:" + Arrays.toString(di.value()) + " 开发日期:" + di.date()); break; } } } } } }
练习2 解析注解,获取注解中属性的值来获取某类的方法(同配置文件获取某类的方法) 配置文件获取某类的某方法
主类:
package test; import java.lang.reflect.Method; /** * @since 2020/1/21 */ @Pro(className = "test.Person",methodName = "person") public class ReflectTest3 { public static void main(String[] args) throws Exception { //提供一个模板(写一个框架类),可以创建任意类的对象,但是不能改变该类的对象的代码,但是不能改变框架的代码 //1.解析注解:1.获取该类的字节码文件对象 2.获取注解 Class<ReflectTest3> cls=ReflectTest3.class; Pro p= cls.getAnnotation(Pro.class); //2.调用注解对象中的属性(抽象方法),获得返回值 String className=p.className();//获取了Pro注解的className属性的值,即"test.Person" String methodName=p.methodName();//获取了Pro注解的methodName属性的值,即"person" System.out.println(className); System.out.println(methodName); /** * 下面的步骤同配置文件应用反射的步骤,对获取到的类应用反射获取其方法 * 好处:只需要改变该注解属性的值和就能获得不同类的不同方法 */ //3.加载该类进内存 Class c=Class.forName(className); //4.创建对象 Object obj= c.newInstance(); //5.获取方法对象 Method method=c.getMethod(methodName); //6.执行方法 method.invoke(obj,null); } }
Pro注解:
package test; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @since 2020/1/21 * 该注解使用来描述执行的类名、方法名 */ @Target({ElementType.TYPE})//作用于类上 @Retention(RetentionPolicy.RUNTIME)//保留在运行阶段 public @interface Pro { String className(); String methodName(); }
注:注解与配置文件原理相同,注解只需要改该类的注解中要获取的类和方法
练习3 自动化测试♥ Junit单元测试例子
开发人员写的代码:
package Testtt; /** * 描述: 开发人员写好的功能代码 * * @Author */ public class CMath { public int sum(int a, int b){ return a + b; } public int minor(int a, int b){ return a - b; } public int div(int a, int b){ return a / b; } public int mix(int a, int b){ return a * b; } }
测试人员写测试接口进行测试:
package Testt; import Testtt.CMath; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 描述: 测试用例类,测试人员写的 * @Author hui */ public class TestCaseUnit { @TestCase("CMath.sum") public boolean testsum(){ CMath math = new CMath(); int ret = math.sum(10, 20); return ret == 30; } @TestCase("CMath.minor") public boolean testminor(){ CMath math = new CMath(); int ret = math.minor(10, 20); return ret == -10; } @TestCase("Cmath.mix") public boolean testmix(){ CMath math=new CMath(); int ret=math.mix(10,20); return ret==100; } } @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @interface TestCase{ String value();//存储测试的函数接口名称 }
注:写boolean类型的测试函数,并传入实际参数,通过每个测试的返回值测试开发人员写的代码;写TestCase接口,在主函数中通过注解来测试
主函数类:
package Testt; import java.lang.reflect.Method; /** * 描述:主类进行测试 * @Author hui */ public class Mainn { public static void main(String[] args) throws Exception { /** * 编写代码,输出TestCaseUnit里面所有的测试用例结果 * CMath.sum 测试结果:成功! 失败! */ Class<TestCaseUnit> c = TestCaseUnit.class; Object obj = c.newInstance(); Method[] methods = c.getDeclaredMethods(); for (int i = 0; i < methods.length; i++) { TestCase tc = methods[i].getDeclaredAnnotation(TestCase.class); if (tc != null) { System.out.print(tc.value() + " "); //得到注解的内容 System.out.print("测试结果:"); if ((boolean) methods[i].invoke(obj)) { .//通过反射调用测试类的方法,通过返回值确定函数是否正确 System.out.println("成功!"); } else { System.out.println("失败!"); } } } } }
注:通过注解得到测试人员写的测试函数的值(如: Cmath.add 测试结果:成功!),通过反射调用测试类的方法,输出每一个方法的测试结果,如果true,输出测试成功!
结果:
练习4:通过注解测试后将异常写入到一个文件中
package AnnotationExample; /** * @since 2020/1/22 */ import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.lang.reflect.Method; public class CalculatorTest { public static void main(String[] args)throws IOException { Calculator c=new Calculator(); Class cls=c.getClass(); Method[] m=cls.getMethods(); int count=0; //创建一个文件将异常导入到文件中 BufferedWriter bw = new BufferedWriter(new FileWriter("bug.txt")); for(Method method:m){ if(method.isAnnotationPresent(Check.class)){ //如果有check注解,就执行该方法 try { method.invoke(c); } catch (Exception e) { //将异常记录到文件中 count++; bw.write(method.getName()+"方法出异常了"); bw.newLine(); bw.write("异常的名称:"+e.getCause()); bw.newLine(); bw.write("异常的原因:"+e.getCause().getMessage()); bw.newLine(); bw.write("--------------"); bw.newLine(); } } } bw.write("本次测试一共出现"+count+"次异常"); bw.flush(); bw.close(); } }
被测试的Calculator类:
package AnnotationExample; /** * @since 2020/1/22 */ 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)); } }
另一种获取注解的方式:
package AnnotationExample; /** * @since 20200/1/22 */ import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.lang.annotation.Annotation; import java.lang.reflect.Method; public class CalculatorTest2 { public static void main(String[] args)throws IOException { Calculator c=new Calculator(); Class cls=c.getClass(); Method[] m=cls.getMethods(); int count=0; //创建一个文件将异常导入到文件中 BufferedWriter bw = new BufferedWriter(new FileWriter("bug2.txt")); for(int i=0;i<m.length;i++){ Annotation[] a=m[i].getDeclaredAnnotations(); for(int j=0;j<a.length;j++) { if (a[j] instanceof Check) { //如果有check注解,就执行该方法 try { m[i].invoke(c); } catch (Exception e) { //将异常记录到文件中 count++; bw.write(m[i].getName() + "方法出异常了"); bw.newLine(); bw.write("异常的名称:" + e.getCause()); bw.newLine(); bw.write("异常的原因:" + e.getCause().getMessage()); bw.newLine(); bw.write("--------------"); bw.newLine(); } } } } bw.write("本次测试一共出现"+count+"次异常"); bw.flush(); bw.close(); } }
注:获取注解应该是某方法的注解,不能写成该类的cls.getAnnotations( )
bug.txt文件中同bug.txt2: