前言
Annotation(注解)是Java5开始引入的新特征;注解其实就是添加在类、变量、方法、参数等前面的一个修饰符一个标记;从某种程度来说,通过一个标注说明当前方法/属性的意义,从而使得代码的可读性变强,且可大大提高程序的开发效率。
Java中内置的注解
我们 在了解自己定义一注解了前,先了解一下 Java 语言本身已经提供的几个现成的注解。
内置的注解 | 说明 |
---|---|
@Override | 表示当前方法覆盖超类中的方法。如果你所写的方法和超类中的方法不同的话,编译器会报错。主要用于检查。 |
@Deprecated | 表明当前的元素已经不适用。当使用了注解为@Deprecated的元素时,编译器会报出过时警告。 |
@SuppressWarnings | 关闭不当的编译器警告。阻止警告的意思。之前说过调用被 @Deprecated 注解的方法后,编译器会警告提醒,而有时候开发者会忽略这种警告,他们可以在调用的地方通过 @SuppressWarnings 达到目的 |
@SafeVarargs | 参数安全类型注解。它的目的是提醒开发者不要用参数做一些不安全的操作,它的存在会阻止编译器产生 unchecked 这样的警告。它是在 Java 1.7 的版本中加入的。 |
@FunctionalInterface | 函数式接口注解,这个是 Java 1.8 版本引入的新特性。函数式编程很火,所以 Java 8 也及时添加了这个特性。 |
元注解
在使用自定义注解之前我们先了解一下在注解中使用的元注解,元注解是用来定义其他注解(自定义注解)的注解(在自定义注解的时候,需要使用到元注解来定义我们的注解)。提供了四种元注解:@Retention、 @Target、@Inherited、@Documented。
元注解 | 说明 |
---|---|
@Retention | 用来标记自定义注解的有效范围(或可以理解为注解的生命周期) |
@Target | 指定Annotation用于修饰哪些程序元素(表示该注解用于什么地方。如果不明确指出,该注解可以放在任何地方) |
@Inherited | 表明注解可以被javadoc此类的工具文档化 |
@Documented | 表示注解里的内容可以被子类继承,比如父类中某个成员使用了上述@From(value),From中的value能给子类使用到。 |
主要介绍一下一下两种元注解:
@Retention
@Rentention 它的取值有以下三种
RetentionPolicy.SOURCE: 只在源代码中保留 一般都是用来增加代码的理解性或者帮助代码检查之类的,比如我们的Override;
RetentionPolicy.CLASS: 默认的选择,能把注解保留到编译后的字节码class文件中,仅仅到字节码文件中,运行时是无法得到的;
RetentionPolicy.RUNTIME: 注解不仅 能保留到class字节码文件中,还能在运行通过反射获取到,这也是我们最常用的。
@Target
@Target 包含一个名为”value“的成员变量,该value成员变量类型为ElementType[ ],ElementType为枚举类型,值有如下几个:
ElementType.TYPE: 能修饰类、接口或枚举类型
ElementType.FIELD: 能修饰成员变量
ElementType.METHOD: 能修饰方法
ElementType.PARAMETER: 能修饰参数
ElementType.CONSTRUCTOR: 能修饰构造器
ElementType.LOCAL_VARIABLE: 能修饰局部变量
ElementType.ANNOTATION_TYPE:能修饰注解
ElementType.PACKAGE: 能修饰包
备注:注解中参数成员只支持int、short、long、byte、char、float、double、boolean八中基本数据类型和String、Enum、Class、annotations以及它们的数组类型;注解中参数成员默认修饰符是public,并且注解中不能定义方法
自定义注解
1、注解声明
类型 @interface(是定义注解用的关键字)注解名 {
参数名() default(默认值,其中默认值是可选的, 可以定义, 也可以不定义.);
}
参数的类型只能是public或者不写两种访问修饰符
注解参数必须有确切的值,所以要么在定义注解的默认值中指定,要么在使用注解时指定,非基本类型的注解元素的值不可为null。
因此, 使用空字符串或0作为默认值是一种常用的做法
EX:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestName {
int value();
int Id() default 0; //如果不需要需要赋值可以省略
}
注意1:定义好的注解,如果我们直接来使用,是没有任何效果的,因为注解只是一段代码,并没有关联上我们的控件,所以我们要编写一个工具类来做关联,而编写关联注解的形式主要有两种,分别为运行时注解和编译时注解
注意2:如果注解中的值不是value,那么在进行注解是时候,需要给出对应的值的名字,假如我们在注解中做了如下定义:
EX1:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestName{
int value();
}
可直接如下使用:
@TestName(R.id.bt)
private Button bt;
EX2:
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestName{
int id();
}
需按如下方式使用:
@TestName(id=R.id.bt)
private Button bt;
关联注解主要有两种方案,分别为:运行时注解和编译时注解
2.1.1、关联注解之运行时注解
运行时注解:在代码运行的过程中通过反射机制找到我们自定义的注解,然后做相应的事情。
备注:反射:对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性。
声明完注解,接着就需要完成对注解的关联(已1、注解声明中的声明为例),如下例子:
1、注解声明
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestName{
int value();
}
2、注解关联(采用运行时注解方式)
public class AnnotateUtils {
public static void initTestName(Activity activity) {
try {
Class<?> clazz = activity.getClass();
Field[] fields = clazz.getDeclaredFields();
for(Field field:fields) {
if (field.isAnnotationPresent(ViewInject.class)) {
TestName inject = field.getAnnotation(TestName.class);
field.setAccessible(true);
field.set(activity, activity.findViewById(id));//给我们要找的字段设置值
}
}
if (activity.getClass().isAnnotationPresent(ContentView.class)) {
ContentView contentView = activity.getClass().getAnnotation(
ContentView.class);
int layoutResID = contentView.value();// 获取布局的资源id
activity.setContentView(layoutResID); // 设置布局内容
injectViews(activity); // 绑定view
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
}
}
}
3、使用注解
...
@TestName(id=R.id.bt)
private Button bt;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AnnotateUtils.initTestName(this);
}
...
2.2.1、关联注解之编译时注解
编译时注解就是在编译的过程中用一个javac注解处理器来扫描到我们自定义的注解,生成我们需要的一些文件(通常是java文件)。
和运行时注解的解析不一样,编译时注解的解析需要我们自己去实现一个注解处理器。
注解处理器所做的工作,就是在代码编译的过程中,找到我们指定的注解。然后让我们更加自己特定的逻辑做出相应的处理(通常是生成JAVA文件)。
注解处理器(Annotation Processor)是javac的一个工具,它用来在编译时扫描和处理注解(Annotation)。一个注解的注解处理器,以Java代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。而且这些生成的Java文件同咱们手动编写的Java源代码一样可以调用。(注意:不能修改已经存在的java文件代码)。
2.2.2、注册注解处理器
将自定义处理器注册到javac中。你必须提供一个.jar文件。就像其他.jar文件一样,你打包你的注解处理器到此文件中。并且,在你的jar中,你需要打包一个特定的文件javax.annotation.processing.Processor到META-INF/services路径下。在javax.annotation.processing.Processor文件里面写上我们自定义注解处理器的全称(包加类的名字)如果有多个注解处理器换行写入就可以。
该文件的创建方式,可以手动创建和谷歌自带的注册处理器的库(@AutoService(Processor.class)
)来创建;
由于手动创建方式比较繁琐,所有自动放弃;而是采用简单的谷歌自带的注册处理器的库来创建;使用@AutoService(Processor.class)来创建javax.annotation.processing.Processor,我们只需要在我们自定义的注解处理器类前面加上@AutoService(Processor.class)这个注解,在打包的时候就会自动生成javax.annotation.processing.Processor文件,写入相的信息。不需要我们手动去创建。
使用@AutoService(Processor.class),首先必须加上下面的依赖。
implementation 'com.google.auto.service:auto-service:1.0-rc3'
其次在自定义的注解处理器类前面加上@AutoService(Processor.class)这个注解;
@AutoService(Processor.class)
public class TestProcessor extends AbstractProcessor {
...
}
2.2.3、定义注解处理器类
定义的注解处理器类一定要继承AbstractProcessor,否则找不到我们需要的注解。在这个类里面找到我们需要的注解。做出相应的处理
首先简单介绍一下关于AbstractProcessor里面的一些函数
public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env){ }
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
@Override
public Set<String> getSupportedAnnotationTypes() { }
@Override
public SourceVersion getSupportedSourceVersion() { }
}
- init(ProcessingEnvironment env):
每一个注解处理器类都必须有一个空的构造函数。
然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入ProcessingEnviroment参数。
ProcessingEnviroment提供很多有用的工具类Elements,Types和Filer。
后面我们将看到详细的内容。
- process(Set<? extends TypeElement> annotations, RoundEnvironment env) :
这相当于每个处理器的主函数main()。你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。
输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素。后面我们将看到详细的内容。
- getSupportedAnnotationTypes():
这里你必须指定,这个注解处理器是注册给哪个注解的。
注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。
换句话说,你在这里定义你的注解处理器注册到哪些注解上。
- getSupportedSourceVersion():
用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。
然而,如果你有足够的理由只支持Java 6的话,你也可以返回SourceVersion.RELEASE_6。
我推荐你使用前者。
注解处理器的核心是process()方法(需要重写AbstractProcessor类的该方法),而process()方法的核心是Element元素。Element 代表程序的元素,在注解处理过程中,编译器会扫描所有的Java源文件,并将源码中的每一个部分都看作特定类型的Element。它可以代表包、类、接口、方法、字段等多种元素种类。所有Element肯定是有好几个子类。如下所示。
Element子类 | 说明 |
---|---|
TypeElement | 类或接口元素 |
VariableElement | 字段、enum常量、方法或构造方法参数、局部变量或异常参数元素 |
ExecutableElement | 类或接口的方法、构造方法,或者注解类型元素 |
PackageElement | 包元素 |
TypeParameterElement | 类、接口、方法或构造方法元素的泛型参数 |
自定义处理器的过程中我们除了要了解Element类和他的子类的用法,还有四个帮助类也是需要我们了解的。Elements、Types、Filer、Messager。因为这四个帮助类都可以在init()函数里面通过ProcessingEnvironment获取到。
注解解析器帮助类 | 说明 |
---|---|
Elements | 用来处理Element的工具类 |
Types | 用来处理TypeMirror的工具类 |
Filer | 用于创建文件(比如创建class文件) |
Messager | 用于输出,类似printf函数 |
在init()函数中如下的代码获取
/**
* 获取到Types、Filer、Messager、Elements
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mTypeUtils = processingEnvironment.getTypeUtils();
mFiler = processingEnvironment.getFiler();
mMessager = processingEnvironment.getMessager();
elementUtils = processingEnv.getElementUtils();
...
}
首先声明完注解,接着就需要完成对注解的关联(已1、注解声明中的声明为例),如下例子:
1、注解声明
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestName{
int value();
Class type();
}
2、初始化处理器(采用编译时注解方式)
注解处理器的写法有固定套路的;
a、注册注解处理器(这个注解器就是我们第二步自定义的类)。b、自定义注解处理器类继承AbstractProcessor。
@AutoService(Processor.class)
public class FactoryProcessor extends AbstractProcessor {
private Types typeUtils;
private Elements elementUtils;
private Filer filer;
private Messager messager;
...
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
messager.printMessage(Diagnostic.Kind.NOTE,"日志开始---------------");
// 遍历所有被注解了@TestName的元素
Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(TestName.class);
for (Element element:elementsAnnotatedWith) {
generateCode();
...
}
messager.printMessage(Diagnostic.Kind.NOTE,"日志结束---------------");
}
...
}
3、生成文件
生成文件,通常是生成一个java文件。直接调用帮助类Filer的createSourceFile()函数就可以创建一个java文件。
之后就是在这个java文件里面写入我们需要的内容了
public void generateCode(Elements elementUtils, Filer filer) throws IOException {
// 检查被注解为@TestName的元素是否是一个类
if(element.getKind() == ElementKind.CLASS){
TypeElement typeElement = (TypeElement) element;
PackageElement packageElement = elementUtils.getPackageOf(element);
String packagePath = packageElement.getQualifiedName().toString();
String className = typeElement.getSimpleName().toString();
try {
JavaFileObject sourceFile = filer.createSourceFile(
packagePath +
"." +
className +
"_ViewBinding", typeElement);
Writer writer = sourceFile.openWriter();
writer.write("package "+packagePath +";\n");
writer.write("import "+packagePath+"."+className+";\n");
writer.write("public class "+className+"_ViewBinding"+" { \n");
writer.write("\n");
writer.append(" public "+className +" targe;\n");
writer.write("\n");
writer.append("}");
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}