参考文章:
Android注解分类
Android之注解问题
注解分类
注解可分为 三类:
一、标准注解 【Java Api中默认定义的注解】
包括Override/Descrecated/SuppressWarnings,是java自带的几个注解。他们由编译器来识别,不会进行编译,不影响代码运行。
标准注解根据使用场景不同,又可分为三类:
1.编译相关注解
编译相关相关注解是给编译器使用的。比如我们最常见的@Override/@Deprecated/@SuppressWarnings,其它的还有@SafeVarargs/@Generated/@FunctionalInterface
其含义也附上:
@Override:编译器会检查被注解的方法是否真的重载了一个来自父类的方法、如果没有,编译器将会给出错误提示。
@Deprecated:可以用来修饰任何不再鼓励使用或已被弃用的属性、方法等。
@SuppressWarnings:可用于除了包之外的其他声明项中,用来抑制某种类型的警告。
@SafeVarargs:用于方法和构造函数,用来断言不定长参数可以安全使用。
@Generated:一般是给代码生成工具使用,用来表示这段代码不是开发者手动编写的,而是工具生成的。被@Generated修饰的代码一般不建议手动修改它。
@FunctionalInterface:用来修饰接口,表示对应的接口是带单个方法的函数式接口。
2.资源相关注解
有四个,一般在JavaEE领域,Android开发中很少用到【本人从来没用过】
@PostConstruct:用在控制对象生命周期的环境中,例如Web容器和应用服务器,表示在构造函数之后应该立即调用被该注解修饰的方法。
@PreDestory:表示在删除一个被注入的对象之前应该立即调用被该注解修饰的方法。
@Resource:用于Web容器的资源注入,表示单个资源。
@Resources:用于Web容器的资源注入,表示一个资源数组。
3.元注解
元注解,就是用来定义和实现注解的注解。这也是我们自定义注解时,基本都会用到的注解.总共有五种。【so,这是注解里第一个我们必须牢牢掌握的点】
- @Target
- @Retention
- @Documented
- @Inherited
- @Repeatable
①@Target
用来指定注解所适用的对象范围
取值:
//注解参数取值是一个ElementType类型的数组。
元素类型 适用于
ANNOTATION_TYPE 注解类型声明
CONSTRUCTOR 构造函数
FIELD 实例变量
LOCAL_VARIABLE 局部变量
METHOD 方法
PACKAGE 包
PARAMETER 方法参数或者构造函数的参数
TYPE 类(包含enmu)和接口(包含注解类型)
TYPE_PARAMETER 类型参数
TYPE_USER 类型的用图
ex:
@Target({ElementType.TYPE,ElementType.PACKAGE})
public @interface CrashReport
②Retention:用来指定注解的访问范围
也就是这个注解保留到什么级别;默认的是CLASS类型。
值有三个:
RetentionPolicy.SOURCE:
该类型的注解信息只会保留在.java源码里,编译之后被抛弃。不会保留在编译好的.class文件中。 (使用场景:APT)
RetentionPolicy.CLASS:
该注解的注册信息会保留在.java源码里和.class文件里,在执行的时候,会被java虚拟机丢弃,不会加载到虚拟机中。(使用场景:字节码插桩)
RetentionPolicy.RUNTIME:
java虚拟机在运行期也保留注解信息,可以通过反射机制读取注解的信息(.java源码,.class文件和执行的时候都有注解的信息,所以这个是保留时间最长的,上面两个在运行时就不起作用了),
(使用场景:反射,运行时动态获取注解信息)
③@Documented:表示被修饰的注解应该被包含在被注解项的文档中
例如JavaDoc生成的文档
④Inherited:表示该注解可以被子类继承
⑤Repeatable:表示这个注解可以在同一个项上面应用多次
这个注解是java8才引入的,前面四个元注解在java5中就已经引入了。
二、运行时注解
定义运行时注解,只需要在声明注解时指定@Retention(RetentionPolicy.RUNTIME)
即可。
运行时注解一般和反射机制配合使用,相比编译时注解性能比较低,但灵活性好,实现相比后者更加简单。
三、编译时注解
@Retention(RetentionPolicy.CLASS)
编译时注解能够自动处理java源文件并生成更多的源码、配置文件、脚本或其他可能想要生成的东西。这些操作需要通过注解处理器来完成。
Java编译器继承了注解处理、通过在编译期间调用javac -processor命令就可以调起注解处理器,它能够允许我们实现编译时注解的功能,从而提高函数库的性能。下面会仔细分析。
此外,有关运行时注解及编译时注解,还需要深入实践。
运行时注解
对应 RetentionPolicy.RUNTIME
编译时注解
对应 RetentionPolicy.SOURCE
简述:也有人称其为代码生成;在编译时对注解做处理,通过注解,获取必要信息,在项目中生成代码,运行时调用,和直接运行手写代码没有任何区别。在第三方库中还是很常用的。比如ButterKnife(虽然已经不再更新了),还有ARouter通过编译时注解生成路由表(因为需要自己写一个简易的路由框架,所以才来查漏补缺),Tinker通过编译时注解生成Application的代理类。编译时注解和运行时注解定义的方式是完全一样的,不同的是它们对于注解的处理方式。运行时注解是在程序运行时通过反射获取注解然后处理的,编译时注解是程序在编译期间通过注解处理器处理的。
想要使用编译时注解来自动生成代码,我们还需要理解APT和annotationProcessor
参考文章1
参考文章2
APT
APT,就是Annotation Processing Tool的简称,叫做注解处理器。可以在代码编译期间对注解进行处理,并且自动生成.java文件,减少手动的代码输入。由Javac帮我们自动生成模块代码。
换种说法就是:
APT可以用来在编译时扫描和处理注解。通过APT可以获取到注解和被注解对象的相关信息,在拿到这些信息后我们可以根据需求来自动的生成一些代码,省去了手动编写。
需要注意一点,这里生成的java文件不能修改,并且会跟手动编写的java代码一样被javac编译。
** 获取注解和生成代碼都是在代码编译时完成的,相比反射在运行时处理注解大大提高了程序性能。 APT的核心是AbstractProcessor类。**
工作流程:
javac 在编译java源文件的时候,获取到源文件中注解的信息,吊起注解处理器进行处理。【注解处理器需要我们自己写】
AbstractProcessor示例
public class MyProcessor extends AbstractProcessor{
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment){
super.init(processingEnvironment);
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment){
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes(){
return super.getSupportedAnnotationTypes();
}
@Override
public SourceVersion getSupportedSourceVersion(){
return super.getSupportedSourceVersion();
}
}
可以看出一共有四个方法:
①init(~):每一个注解处理器类都必须有一个空的构造函数。这个特殊的init()方法,会被注解处理工具调用,并输入ProcessingEnvironment参数。ProcessingEnviroment提供了很多有用的工具类Elements,Types和Filer.
②process(~):相当于每个处理器的主函数main().在这里写我们的扫描、评估和处理注解的代码,以及生成java文件。输入参数RoundEnvironment,可以查询出包含特定注解的被注解元素。
③getSupportedAnnotationTypes():这里我们必须指定,这个注解处理器是注册给哪个注解的。此函数返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全程。换句话说,这里定义我们的注解处理器注册到哪些注解上。
④getSupportedSourceVersion():用来指定使用的java版本。通常这里返回SourceVersion.latestSupported().
//实操就可以参照自定义的路由框架了。
虽然个人很少用到APT,但实际上此技术早已被广泛运用在Java框架中,包括Android项以及Java后台项目。但看android这一块,ButterKnife、EventBus、Dagger2以及ARouter等都运用到了APT技术,要想了解、探究以及后面我们开发类似框架,APT是我们必须要掌握的!! (公司几个大佬做的网络框架、路由框架都用到了这个技术,所以,这个真的真的很重要~)
android-apt替代者
Android Gradle插件2.2版本发布的时候,android-apt作者在官网证实了后续将不再维护android-apt(时过境迁,Butter Knife也走到了终点)。Android Gradle插件提供了名为annotationProcessor的功能来完全代替android-apt。
** annotationProcessor和APT区别:**
Android官方的annotationProcessor同时支持javac和jack编译方式,而android-apt只支持javac方式。
Jack编译方式:
Jack(Java Android Compiler Kit)是新的Android编译工具,从Android6.0开始加入,替换原有的编译工具,例如javac,ProGuard,jarjar和dx。它主要负责将java代码编译成dex包,并支持代码压缩,混淆等。
注解处理器工作原理
通常情况下,项目在编译期时,会处理javac编译项目的源代码。这时候如果需要处理项目中的注解,那么就需要引入另外两个角色: 注解工程 和 注解处理器工程。
在注解工程中,定义注解。定义完成后,便可以在Android工程中依赖注解工程,在工程中的类、方法或者属性上引入注解进行标记。标记好了之后,就需要另外一个工程来处理这些注解。这便引入了注解处理器工程。
注解处理器工程中有两个最重要的部分:
1.META-INF目录注册我们的注解处理器。在属性文件中注册我们自定义的注解处理器
2.声明我们自定义的注解处理器
之后android工程依赖注解处理器工程。通过annotationProcessor或者kapt依赖。
之后javac就可以找到自定义的注解处理器,将项目中找到所有使用了我们定义的编译期注解的类、方法或者属性,传递给注解处理器。这时候注解处理器就可以在编译期开始工作了。
注解相关问题
我们不管需要知道如何使用注解,注解一些相关的概念以及原理也是需要明白的!毕竟,面试会用到。
APT相关问题
这篇文章中主要围绕了四个问题进行分析理解:
①注解处理器processor为什么要在META-INF注册
②注解处理器是如何被系统调用的
③注解申明和注解处理器为什么要分module处理
④apt项目会不会增加apk体积
文章介绍的很详细,但是个人觉得还是要做一下简单总结:
第一个问题:
在编译时,java编译器(javac)会去META-INF中查找实现了AbstractProcessor的子类,并且调用该类的process函数,最终生成.java文件。就像activity需要在AndroidManifest.xml中注册一样,我们的注解处理器也需要在META-INF中注册,这样javac才知道要调用哪个注解处理器来处理注解。
第二个问题:
其实就是在引入注解处理器子项目的build.gradle中,通过annotationProcessor来引入我们注解的module。(在kotlin中对应的关键字是kapt)
在我们编写好我们自己的AbstractProcessor之后,需要做两件事情:
1.在META-INF目录下注册Processor
2.在项目中使用注解的地方添加apt工具:annotationProcessor(kapt)
APT四要素:
**注解处理器(AbstractProcess) + 代码处理(javaPoet) + 处理器注册(AutoService)(一般我自己写了,能少用框架就少用) + apt(annotationProcessor) **
第三个问题:
注解处理器需要继承AbstractProcessor类,但是此类是JDK中的类,不再android sdk中,所以需要放在单独的java lib中。而processor中需要依赖自定义注解,把annotation抽成一个独立的lib,便于维护。
第四个问题:
apt项目不会增加项目体积。因为这个lib只在编译期用到,是不会打包进apk的。
而我们不将注解声明和注解处理放在一起也有其中的一部分原因。对于调用者来说,只是想使用这个注解,而不希望已经编译好的项目中引进注解处理器相关的内容,所以为了不引入没必要的问题导致包体积增大,我们一般选择将注解声明和注解处理分开处理。
javapoet
对于自动生成的代码,用字符串拼接的方法,比较耗时耗力。已经有开源框架支持我们优雅的生成我们所需的代码,这就是javapoet:
javapoet
还有kotlinpoet,也可一试:
kotlinpoet
这里主要还是以javapoet为主进行学习:
javapoet使用指南
使用案例大全
javapoet常用类
- MethodSpec : 生成方法
- ParameterSpec : 生成参数
- AnnotationSpec : 生成注解
- FieldSpec : 生成成员变量
- ClassName : 通过包名和类名生成对象(同时会自动导包,很重要!)
- ParameterizedTypeName : 通过MainClass和includeClass生成包含泛型的Class
- TypeSpec : 生成类、接口、枚举对象的类
- JavaFile : 控制生成Java文件
常用方法
①设置修饰关键字
addModifiers(Modifier... modifiers)
Modifier是一个枚举对象,枚举值修饰关键字public/protected/private/static/final等
所有javapoet创建的对象都必须设置修饰符(包括方法、类、接口、枚举、参数、变量)
②设置注解对象
addAnnotation(AnnotationSpec annotationSpec)
addAnnotation(ClassName annotation)
addAnnotation(Class<?> annotation)
该方法即为类或方法或参数设置注解,参数即可以是AnnotationSpec,也可以是ClassName,还可以直接传递Class对象。
一般情况下,包含复杂属性的注解一般用AnnotationSpec,如果单纯添加基本注解,无其他附加属性可以直接使用ClassName或者Class即可。
③设置注释
addJavadoc(CodeBlock block)
addJavadoc(String format, Object... args)
在编写类、方法、成员变量时,可以通过addJavadoc来设置注释,可以直接传入String对象,或者传入CodeBlock(代码块)。
这篇文章介绍的真的很详细,代码就不一点点拷了。我们只提几点比较重要又容易被忽视的点:
1.TypeSpec:
要生成类、接口、枚举,必须得通过TypeSpec。分别对应:
classBuilder/interfaceBuilder/enumBuilder
如果要继承父类、实现接口,则需要:
继承类:
.superclass(ClassName className)
实现接口
.addSuperinterface(ClassName className)
2.FieldSpec
生成成员变量。
实例化:
initializer(String format, Object... args)
ex:
public Activity mActivity = new Activity;
initializer方法中的内容就是“=”后面的内容,如下:
ClassName activity = ClassName.get("android.app", "Activity");
FieldSpec spec = FieldSpec.builder(activity, "mActivity")
.addModifiers(Modifier.PUBLIC)
.initializer("new $T", activity)
.build();
3.MethodSpec
方法体
添加方法体,也就是方法里的内容,有两个方法:
addCode/addStatement.
区别:
不同的是使用addStatement()方法时,你只需要专注于该段代码的内容,至于结尾的分号和换行它都会帮你做好。 而addCode()添加的方法体内容就是一段无格式的代码片,需要开发者自己添加其格式。
方法体模板
就是上面两个方法的重载:
addCode(String format, Object... args)
addStatement(String format, Object... args)
对应format有三种特定的占位符:
①$T
$T 在JavaPoet代指的是TypeName,该模板主要将Class抽象出来,用传入的TypeName指向的Class来代替。
ex:
ClassName bundle = ClassName.get("android.os", "Bundle");
addStatement("$T bundle = new $T()",bundle)
对应的生成内容就是:
Bundle bundle = new Bundle();
②$N
$N在JavaPoet中代指的是一个名称,例如调用的方法名称,变量名称,这一类存在意思的名称
ex:
addStatement("data.$N()",toString)
对应的生成内容就是:
data.toString();
③$S
S的地方
ex:
.addStatement("super.$S(savedInstanceState)","onCreate")
emmm,感觉就$T用的比较多吧。因为可以导入对应类的包!
抛出异常:
.addException(TypeName name)
此外,对于生成<? extends A>
这种带通配符的泛型,还需要用到:
WildcardTypeName.subtypeOf()
输出到文件
如果是拼接字符串生成类,那么输出到文件中基本如下:
这里的"code"就是我们拼接的类信息。
而对于javapoet生成的类,输出到文件中,则更方便:
实例
最后用本人写的实例项目作为入门结束标志:
/**
* Create by rye
* at 2021/1/23
*
* @description: 注解处理器 /这个时候需要在compiler模块下main包新建resource目录,
* 创建Processor文件。其内容为该注解处理器的全类名;
* 如果有多个注解处理器,在META-INF中的Processor处理器中继续添加即可。
*/
@SupportedSourceVersion(SourceVersion.RELEASE_8)
//要处理的注解
@SupportedAnnotationTypes("com.rye.router_annotation.Route")
@SupportedOptions("moduleName")
public class RouterProcessor extends AbstractProcessor {
private String moduleName;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
Map<String, String> options = processingEnvironment.getOptions();
moduleName = options.get("moduleName");
}
/**
* 编译器找到要处理的注解后,会回调此方法
*
* @param set
* @param roundEnvironment
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// String code = "package com.enjoy.routers;\n" +
// "import android.app.Activity;\n" +
// "import com.dawn.zgstep.ui.activity.ProxyActivity;\n" +
// "import com.rye.router_api.api.IRouterLoad;\n" +
// "import java.util.Map;\n" +
// "\n" +
// "public class " + moduleName + "Router implements IRouterLoad{\n" +
// " @Override\n" +
// "public void loadInfo(Map<String,Class<? extends Activity>> routers ) {\n" +
// " routers.put(\" /food/main\",ProxyActivity.class);\n" +
// " }\n" +
// "} \n";
// //随机router存放目录;文件工具
// Filer filer = processingEnv.getFiler();
// try {
// //将代码输入到文件中
// JavaFileObject sourceFile = filer.createSourceFile("com.enjoy.routers." + moduleName + "Router");
// OutputStream os = sourceFile.openOutputStream();
// os.write(code.getBytes());
// os.close();
// } catch (IOException e) {
// e.printStackTrace();
// }
buildClass();
return false;
}
private void buildClass() {
//生成loadInfo参数
ClassName typeName = ClassName.get("android.app", "Activity");
ClassName proxyClassName = ClassName.get("com.dawn.zgstep.ui.activity","ProxyActivity");
ParameterizedTypeName subType = ParameterizedTypeName.get(ClassName.get(Class.class),
WildcardTypeName.subtypeOf(typeName));
ParameterSpec paramRouters = ParameterSpec.builder(ParameterizedTypeName.get(ClassName.get(Map.class), ClassName.get(String.class), subType), "routers")
.build();
//生成loadInfo方法
MethodSpec loadInfo = MethodSpec.methodBuilder("loadInfo")
.addModifiers(Modifier.PUBLIC)
.returns(void.class)
.addParameter(paramRouters)
.addStatement("routers.put(\" /food/main\",$T.class)",proxyClassName)
.build();
//生成类
TypeSpec clazz = TypeSpec.classBuilder(moduleName)
.addMethod(loadInfo)
.addModifiers(Modifier.PUBLIC)
.build();
JavaFile file = JavaFile.builder("com.enjoy.routers", clazz)
.build();
try {
file.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(file);
}
}
这里buildClass()方法和被注释掉的代码生成的代码是一样的: