Java-注解
Posted 茶碗儿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java-注解相关的知识,希望对你有一定的参考价值。
概念
- Jdk1.5之后引入注解概念
- 用来说明,注释
作用
- 编译检查
如:@Override,检查方法是否继承父类- 编写文档
如:@author、@version、@since、@see、@link、@code、@param、@return、@exception、@throws等- 代码分析
使用反射对代码进行分析
JDK内置注解
- @Override:检查方法是否继承父类
- @Deprecated:表示已过时
- @SuppressWarnings:压制警告,标注此注解后就不提示警告了
一般传递参数all:@SuppressWarnings("all")
案例:
//压制警告
@SuppressWarnings("all")
public class Demo
/**
* 继承父类
* @return
*/
@Override
public String toString()
return super.toString();
/**
* 已过时
*/
@Deprecated
public void show()
//有缺陷
public void show2()
public void tt()
//show方法有删除线
show();
show2();
自定义注解
格式:
- 元注解
- public @interface 注解名称
属性列表
本质:
- 注解的本质就是接口,默认继承Annotation接口
属性:接口中的抽象方法
- 属性的返回值类型有下列取值
基本数据类型
String
枚举
注解
以上类型的数组- 定义了属性,在使用时需要给属性赋值
如果使用属性时设置了default默认初始值,则使用注解时可以不进行属性赋值
如果只有一个名为value的属性,使用注解时value可以省略,直接赋值即可
数组赋值时,值使用 包裹,如果只有一个值,大括号可以省略
元注解:用于描述注解的注解
- @Target:描述注解能够作用的位置
ElementType的取值:
TYPE:作用于类上
FIELD:作用于成员变量上
METHOD:作用于方法上- @Retention:描述注解能够保留的阶段
RetentionPolicy的取值:
SOURCE:当前被描述的注解,不会保留到class字节码文件中
CLASS:当前被描述的注解,会保留到class字节码文件中,但不会被JVM读取到
RUNTIME:当前被描述的注解,会保留到class字节码文件中,并被JVM读取到(自定义注解一般都使用RUNTIME)- @Documented:描述注解是否会被抽取到api文档中
- @Inherited:描述注解是否被子类继承
解析注解
在程序中使用(解析)注解:获取注解中定义的属性值
- 获取注解定义位置(类,方法,成员变量)的对象
- 获取指定的注解
getAnnotation(自定义注解.class)- 调用注解中的抽象方法获取配置的属性值
案例:使用自定义注解,加载配置,并运行程序
- 创建类和方法待用
public class Prop1
public void show()
System.out.println("pro1--show");
- 自定义注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAn
String className();
String methodName();
- 使用注解加载配置
//@MyAn(className = "类的路径",methodName = "方法名")
@MyAn(className = "com.annotation.Prop1",methodName = "show")
public class Test
public static void main(String[] args) throws Exception
//获取该类字节码文件对象
Class<Test> tc = Test.class;
//获取注解对象
//其实就是在内存中生成了一个该注解接口的子类实现对象
MyAn an = tc.getAnnotation(MyAn.class);
//获取注解对象中定义的抽象方法,获取返回值
String className = an.className();//类的路径
String methodName = an.methodName();//方法名
//加载该类进内存
Class<?> ac = Class.forName(className);
//创建类的对象
Object obj = ac.getDeclaredConstructor().newInstance();
//根据方法名,获取方法对象
Method method = ac.getMethod(methodName);
//调用方法
method.invoke(obj);
- 运行结果:
pro1--show
练习
- 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface 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("大秦万年……");
- 简易测试框架
/**
* 简易测试框架
* 1.当主方法执行后,会自动检测加了@Check的方法
* 2.判断方法是否有异常
* 3.将异常记录到文件中
*/
public class TestCheck
public static void main(String[] args) throws IOException
//创建计算器对象
Calculator cal = new Calculator();
//获取字节码文件对象
Class<? extends Calculator> ac = cal.getClass();
//获取所有成员方法
Method[] methods = ac.getDeclaredMethods();
//记录异常出现的次数
int count = 0;
//异常日志写入bug.txt
BufferedWriter bw = new BufferedWriter(new FileWriter("bug.txt"));
//遍历方法
for (Method method : methods)
//判断方法上是否有@Check注解
if (method.isAnnotationPresent(Check.class))
try
//如果有@Check注解,执行方法
method.invoke(cal);
catch (Exception e)
//捕获异常,记录文件信息
count++;//异常次数+1
bw.write(method.getName() + "出异常了");
bw.newLine();//换行
//getCause()异常原因,getSimpleName()简短名称
bw.write("异常的名称是:" + e.getCause().getClass().getSimpleName());
bw.newLine();
//getMessage()获取具体原因
bw.write("异常的原因是:" + e.getCause().getMessage());
bw.newLine();
bw.write("-----------------");
bw.newLine();
bw.write("本次测试共出现【"+count+"】次异常。");
//刷新流
bw.flush();
//关闭流
bw.close();
- 运行结果:加了@Check注解的方法,有3个运行正常
1+0=1
1-0=1
1*0=0
- 日志记录
div出异常了
异常的名称是:ArithmeticException
异常的原因是:/ by zero
-----------------
本次测试共出现【1】次异常。
总结
- 一般我们会使用注解,而不是自定义注解
- 注解用在哪里?
编译器
给解析程序用- 注解不是程序的一部分,而是一个标签
拓展
小贴士
- 编译命令:javac **.java
- 反编译命令:javap **.class
- 生成api文档命令:Javadoc **.java
- 接口中的方法都是抽象方法
- 定义常量时,可以设置默认值:String name() default "abc"
枚举
- 定义枚举时要使用Enum关键字;
- 命名时类名尽量要带Enum , 常量全部大写,多个单词之间用_分割;
- 枚举是一种特殊的常量类;
- 默认的构造方法必须是私有的;
- 枚举里的常量相当于枚举对象;
- 常量之间用逗号分隔,用分号结尾。
深入理解Java注解——注解基础
一直以来对Java注解的理解都不是特别深刻,但是在多年的软件开发生涯中接触了不少注解相关的东西,所以有必要深入理解一下Java注解知识,通过本篇博客记录学习Java注解的一些知识点。
深入理解Java注解(二)——JavaPoet使用
深入理解Java注解(三)——编译时注解实战
什么是Java注解
举个例子,在Java开发中,我们会使用@Override
标记一个被子类复写的方法,使用@Deprecated
标记一个方法或者一个类表示方法或类已被弃用,不再推荐使用。这里的@Override
@Deprecated
就是Java注解,查看@Override
源码如下:
package java.lang;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override
下面对以上代码做如下说明:
-
使用
@interface
声明一个注解,比如上面的代码中使用public @interface Override
声明了@Override
这个注解。 -
在
@Override
这个注解上又使用了@Target
和@Retention
做了注解,这种针对注解的注解称为元注解。 -
@Target
表示注解作用的目标,它的取值是一个ElementType
数组,ElementType
是一个枚举类型:public enum ElementType /** Class, interface (including annotation type), or enum declaration */ TYPE, /** Field declaration (includes enum constants) */ FIELD, /** Method declaration */ METHOD, /** Formal parameter declaration */ PARAMETER, /** Constructor declaration */ CONSTRUCTOR, /** Local variable declaration */ LOCAL_VARIABLE, /** Annotation type declaration */ ANNOTATION_TYPE, /** Package declaration */ PACKAGE, /** * Type parameter declaration * * @since 1.8 */ TYPE_PARAMETER, /** * Use of a type * * @since 1.8 */ TYPE_USE
关于
@Taget
的取值,使用比较多的分别是TYPE
FIELD
METHOD
,TYPE
表示注解作用在类、接口或枚举类上;FIELD
表示注解作用在类的某个成员变量上,包括枚举类中的常量;METHOD
则表示注解作用在方法上。其他类型的取值作用范围可以查看源码中对应的注释。需要注意的是,@Target
的取值是一个ElementType
数组,这表示使用@Target
这个元注解去注解其他注解时,可以有多个取值,比如这种方式:@Target(value=CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE)
。 -
@Retention
表示注解的保留范围,它的取值是一个RetentionPolicy
枚举类型,其源码如下:public enum RetentionPolicy /** * Annotations are to be discarded by the compiler. */ SOURCE, /** * Annotations are to be recorded in the class file by the compiler * but need not be retained by the VM at run time. This is the default * behavior. */ CLASS, /** * Annotations are to be recorded in the class file by the compiler and * retained by the VM at run time, so they may be read reflectively. * * @see java.lang.reflect.AnnotatedElement */ RUNTIME
SOURCE
表示注解保留到源码阶段,一旦源码被编译成class文件,则注解就不存在了;CLASS
表示注解保留到字节码阶段,当源码被编译成字节码时,字节码中依然有注解,但是一旦字节码被虚拟机解释执行,则注解不存在;RUNTIME
表示注解保留到运行时,即虚拟机解释执行字节码时注解依然存在。
通过以上的代码及解释,对注解有个大概的了解了,我个人理解是:注解是使用@interface
标记的一个类(实际上注解类就是一个继承了Annotation接口的接口),它可以作用在类、方法或变量上,作为一种修饰。
但是,只作为修饰是不够的,注解的强大之处在于使用很少的代码实现非常强大的功能,在后续篇幅中会详细记录。
自定义注解
上面的代码中主要是使用JDK中提供的@Target
和@Deprecated
元注解做例子说明注解是个啥,实际开发中我们可以自定义自己的注解,比如下面的代码定义了一个自定义的注解:
// 表示自定义注解作用在类、成员变量和方法上
@Target(ElementType.TYPE, ElementType.FIELD, ElementType.METHOD)
// 表示注解保留到程序运行时
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation
String name() default "";
上面的自定义注解代码中,比较难以理解的是花括号中的String name() default "";
这句,上面说了实际上使用@interface
定义的注解就是一个继承了Annotation
接口的接口。
在Java中,定义接口时可以在接口中定义方法但不能有实现,可以在接口中定义某个成员变量,这个变量默认是public static
的,但是注解中的这段String name() default "";
即不像是声明一个变量也不像是声明一个方法,而且还有default ""
这种不常见的写法。
为了理解这种写法,还是先来看看上面写的自定义注解怎么使用吧,以下代码在一个类和类的成员变量及类的成员方法上使用了自定义注解:
@MyAnnotation(name = "zhangsan")
public class Person
@MyAnnotation
String name;
@MyAnnotation
public void sayHello()
System.out.println("hello, this is " + name);
由于@MyAnnotation
注解被元注解@Target
修饰了:@Target(ElementType.TYPE, ElementType.FIELD, ElementType.METHOD)
,所以在上面的Person类中,可以在类及类的成员变量和成员方法上都使用@MyAnnotation
注解,而且在使用该注解时,可以指定name的值如:@MyAnnotation(name = "zhangsan")
,也可以不指定name的值,没有指定name的值时,使用的就是定义注解时的默认值了。
这里再回到定义自定义注解时的这种写法:String name() default "";
,这种写法我觉得可以理解成是在接口中定义了一个方法,方法名为name
,返回值为String
,且方法的默认返回值为""
。
注意事项:
在注解中定义的方法,其返回值是有要求的,注解中的方法并非支持所有类型的返回值,只支持如下几种数据类型:
(1)8种基本数据类型;
(2)String、枚举、Class;
(3)其他注解类型
(4)以上类型的一维数组
下面的代码展示了目前可以定义在注解中的返回值类型:
// 表示自定义注解作用在类、成员变量和方法上
@Target(ElementType.TYPE, ElementType.FIELD, ElementType.METHOD)
// 表示注解保留到程序运行时
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation
String name() default "";
Target target();
int age();
float score() default 1f;
double[] lengths() default 10, 20;
Class<StringBuilder> cls();
// StringBuilder sb(); // 非法,IDE会提示"Invalid type 'StringBuilder' for annotation member"
另外,如果一个自定义注解中只定义了一个方法,且方法名为value,比如下面的代码:
public @interface MyAnnotation
int value();
那么在使用该注解时,可以不指定value直接在圆括号内写方法返回值,比如:
@MyAnnotation(10)
public void sayHello()
System.out.println("hello, this is " + name);
但是如果注解中定义的方法名不为value
或者定义的方法不止一个,那就必须在使用注解时按这种格式写@MyAnnotation(xxx = XXX):
@MyAnnotation(value = 10)
public void sayHello()
System.out.println("hello, this is " + name);
请注意,到目前为止,还只是说明了自定义注解怎么定义,怎么使用在一个类上面,但是上面的示例代码中,在Person类中使用自定义注解还并不能产生任何效果,关于注解的解析,在后文会做更详细的说明,主要分为编译时注解和运行时注解。
注解的解析
注解定义好了之后必须修饰在方法、类或者某个变量上,但是这些仅仅是第一步,最关键的其实是注解的解析,必须通过某些方法解析出注解上携带的数据,才能让注解发挥作用,对注解的解析主要分两种,一种是编译时注解,主要在代码编译过程中解析注解并生成新代码,从而达到某个功能;另一种是运行时注解,通过反射将注解中的信息提取出来。实际开发过程中,以及某些知名的开源库(如ButterKnife)中都是使用的编译时注解这种方式,下面就记录下这两种不同的解析注解的方式。
运行时注解
运行时注解主要通过Java的反射机制,在程序运行过程中解析出注解中携带的信息,下面的代码展示了使用反射解析注解的方法:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
@Target(ElementType.TYPE, ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface MyAnnotation
String value() default "";
@MyAnnotation("zhangsan")
class Person
@MyAnnotation("haha")
public void sayHello()
System.out.println("hello");
public class Test
public static void main(String[] args)
MyAnnotation myAnnotation = Person.class.getAnnotation(MyAnnotation.class);
System.out.println(myAnnotation.value()); // 输出"zhangsan"
try
Method m = Person.class.getDeclaredMethod("sayHello");
MyAnnotation annotation = m.getAnnotation(MyAnnotation.class);
System.out.println(annotation.value()); // 输出"haha"
catch (NoSuchMethodException e)
e.printStackTrace();
使用反射解析注解中的信息这种方法并不常用,主要是因为反射效率低,而且定义注解时需要设置保留策略为RetentionPolicy.RUNTIME
即注解保留到代码运行时,若将上面的策略改为SOURCE
或者CLASS
后再次运行程序,会发现代码会抛出空指针异常,这是因为使用SOURCE
或者CLASS
这种策略时,代码在运行过程中已经没有注解了,通过Person.class.getAnnotation(MyAnnotation.class);
获取注解得到的是null对象。运行时注解的方式由于效率低,用得不多,就不花过多篇幅去记录了,主要的精力还是放到编译时注解这种方式上。
编译时注解
编译时注解是程序在编译过程中解析注解信息的,这种解析注解的方式较运行时注解通过反射解析更为麻烦一些,但是优点是没有反射带来的性能损耗,Android界大名鼎鼎的ButterKinfe库就是使用编译时注解实现的,其可以通过@BindView(id = R.id.xxx)
这种简单的注解修饰达到省去写大量findViewById()
这种没有营养的代码,下面就来看看编译时注解是怎么实现的吧。
本篇中使用Android Studio创建一个Android工程用于演示如何使用编译时注解。
-
使用Android Studio创建一个空的Android工程。
-
在项目上右键,创建一个新的Java Library命名为annotation,注意下图左侧,选择Java or Kotlin Library即可,在这个Lib中我们会创建一个自定义的注解,命名为MyAnnotation。
由于我已经创建了annotation这个Module,所以上图中会有提示"Module annotation already exists" -
在上面创建的MyAnnotation类中编辑如下代码:
package com.example.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.FIELD) @Retention(RetentionPolicy.CLASS) public @interface MyAnnotation String value() default "";
-
按照第二步的方法再创建一个名为processor的Java Library,该Lib的作用主要是创建自定义的注解处理器,如下图所示:
在创建的processor module下编辑build.gradle
文件,添加对annotation库的依赖,如下代码所示:plugins id 'java-library' java sourceCompatibility = JavaVersion.VERSION_1_7 targetCompatibility = JavaVersion.VERSION_1_7 dependencies implementation project(':annotation')
再编辑MyProcessor类,其代码如下:
import com.example.annotation.MyAnnotation; import java.util.HashSet; import java.util.Set; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Messager; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.SourceVersion; import javax.lang.model.element.TypeElement; import javax.tools.Diagnostic; public class MyProcessor extends AbstractProcessor // 打印日志用 private Messager messager; @Override public synchronized void init(ProcessingEnvironment processingEnv) super.init(processingEnv); messager = processingEnv.getMessager(); messager.printMessage(Diagnostic.Kind.NOTE, "---------->init"); @Override public Set<String> getSupportedAnnotationTypes() // 返回支持的注解类型 Set<String> types = new HashSet<>(); types.add(MyAnnotation.class.getCanonicalName()); return types; @Override public SourceVersion getSupportedSourceVersion() // 返回支持的源代码版本 return SourceVersion.latestSupported(); @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) // 注解处理主要在该方法中 messager.printMessage(Diagnostic.Kind.NOTE, "---------->process"); return true;
-
在项目的app module下依赖annotation和processor,编辑app module下的
build.gradle
文件,添加对这两个Lib的依赖:dependencies ... implementation project(':annotation') annotationProcessor project(':processor')
注意对processor的依赖一定要使用
annotationProcessor
而不是implementation
,只有使用annotationProcessor
才能让注解处理器生效。 -
注册注解处理器。注册注解处理器有两种方法,第一种是直接在processor module的
src/main
目录下创建resources/META-INF/services
目录,然后在该目录下新建一个文件,文件名固定为javax.annotation.processing.Processor
,文件内容则是MyProcessor类的全路径,比如该例子中的路径是:com.example.processor.MyProcessor
,如果你的项目中有多个Processor类,依次换行添加到javax.annotation.processing.Processor
这个文件中即可。另一种注册注解处理器的方法是使用Google的AutoService,其github地址为:https://github.com/google/auto/tree/master/service。 -
在Android Studio中构建项目,点击菜单栏上的类似锤子的图标后,在
Build
视图中可以看到上面MyProcessor类中输出的日志信息:
出现以上日志打印证明自定义的注解处理器可以正常工作了,项目在编译过程中就会自动调用我们编写的注解处理器完成对注解的解析工作,但是MyProcessor类中的process方法还没有具体的实现,一般会在该方法中处理注解的解析,以及Java代码的生成等操作,关于这些内容,会放到下一篇中记录。
本篇中编译时注解的Demo可以在这里查看:https://github.com/yubo725/test-apt/tree/v0.1
以上是关于Java-注解的主要内容,如果未能解决你的问题,请参考以下文章