APT-单例代码规范检查
前文提到注解按照Retention可以取值可以分为SOURCE,CLASS,RUNTIME三类,在定义注解完成后,可以结合APT进行注解解析,读取到注解相关信息后进行一些检查和设置。
接下来我们实现一个单例注解来修饰单例类,当开发人员写的单例类不符合编码规范时,在编译过程中抛出异常。大家都知道,单例类应该具有以下特点:
- 构造器私有
- 具有public static修饰的getInstance方法
打开Android Studio,新建SingletonAnnotationDemo工程,随后在该工程中进行注解的定义和APT的开发,一般情况下注解和与之关联的APT都会以单独的Module声明在项目中,下面我们开始实践吧。
singleton-annotation 注解模块
新建singleton-annotation Java模块
打开新建的SingletonAnnotationDemo项目,在右上角切换至Project视图,如下图所示:
切换完成后,在项目名称上右键单击,在弹出的菜单中依此选择new->Module,如下图所示:
选择Module条目后,弹出如下对话框,依次操作如下图所示:
其中标记1表明我们创建的是Java或者Kotlin模块,标记2位置填写模块名称,这里输入singleton-annotation,标记3位置输入打算创建的类名,这里填写Singleton,标记4位置用于选择模块语言类型,这里选择java即可。
至此创建singleton-annotation模块完成,等待Android Studio构建完成即可。
新建Singleton注解
打开新建的singleton-annotation模块,进入Singleton.java文件中将其修改为注解,如上文描述,该注解运行在编译期,故Retention为SOURCE,作用在类上,故其Target取值为TYPE,完整代码如下:
package com.poseidon.singleton_annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface Singleton {
}
依赖singleton-annotation模块
在app模块添加对singleton-annotation模块的依赖,操作方式有两种:
手动添加singleton-annotation依赖
打开app模块的build.gradle文件,在其内部手动添加依赖,如下所示:
dependencies {
...
// 添加singleton-annotation模块依赖
implementation project(path: ':singleton-annotation')
}随后重新同步项目即可
使用AS菜单添加singleton-annotation依赖
在app模块右键选择Open Module Settings,在随后弹出的弹窗中添加singleton-annotation模块,操作指导如下图所示:
选择Open Module Settings后弹框如下图所示,选择dependencies,代表依赖管理,随后在右侧的Module列表中选择你要操作的模块,这里选择app,最后点击选择app模块后,其右侧依赖列表中的加号,选择Module Dependency,代表添加模块依赖
Module Dependency:模块依赖,一般用于添加项目中的其他模块作为依赖
Library Dependency:库依赖,一般用于添加上传到maven,google,jcenter等位置的开源库
JAR/AAR Dependency:一般用于添加本地已有的jar或aar文件作为依赖时使用
选择添加模块依赖后,弹出窗体如下图所示:
在上图中标记1的位置勾选要添加的模块,在2的位置选择依赖方式,随后点击OK等待同步完成即可。
singleton-processor 注解处理模块
与创建singleton-annotation模块一样,以相同的方式创建一个名为singleton-processor的模块,其内部有一个Processor类,创建完成后,项目模块如下图所示(Processor类为创建模块时输入的类名,AS自动生成的):
添加注解处理器声明
将Processor.java类作为我们的注解处理器类,为了Android Studio能识别到该类,我们需要对该类进行声明,通常有两种声明方式:
手动声明
手动声明的主要实现方式是在main目录下创建resources/META-INF/services目录,在该目录下创建javax.annotation.processing.Processor文件,其内容如下所示:
com.poseidon.singleton_processor.Processor
可以看到其内部写的是注解处理器类完整路径(包名+类名),当有多个注解处理器类时,可以写多行,每次放置一条注解处理器信息即可
借助AutoService库自动声明
除了手动声明外,我们可以借助auto-service库进行注解处理器声明,其本身也是依赖注解实现,在singleton-processor模块的build.gradle中添加auto-service库依赖,如下所示:
dependencies {
implementation 'com.google.auto.service:auto-service:1.0'
annotationProcessor 'com.google.auto.service:auto-service:1.0'
}依赖添加完成后,使用@AutoService注解修饰我们的注解处理器类,代码如下:
@AutoService(Processor.class)
public class Processor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}随后运行该项目,可以看到在singleton-processor模块的build目录中自动生成了META-INF相关的目录,如下图所示:
其中javax.annotation.processing.Processor文件内容和我们手动添加时的内容一致。
当然也可以参考上文在Library Dependency窗口添加auto-service依赖,大家可以自行探索下
依赖singleton-processor模块
与依赖singleton-annotation模块时方法类似,由于singleton-processor模块是注解处理模块,故依赖方式应使用annotationProcessor,在app模块的build.gradle文件的dependencies块中添加代码如下:
annotationProcessor project(path: ':singleton-processor')
至此我们已经完成了新增模块的依赖以及注解的声明,接下来我们来看看注解处理器的实现。
注解处理器代码实现
在前文中我们已经将singleton-processor模块的Processor类声明为注解处理器,接下来我们来看下如何在注解处理器中处理我们的@Singleton注解,并对使用该注解的单例类完成检查。
自定义注解处理器一般继承自AbstractProcessor,AbstractProcessor是一个抽象类,其父类是Processor,在类编译成.class文件前,遍历整个项目里的所有代码,在获取到对应注解后,回调注解处理器的process方法,以便对注解进行处理。
当继承AbstractProcessor时,我们一般重写下列函数:
函数名称 | 函数说明 |
---|---|
void init(ProcessingEnvironment processingEnv) | 初始化处理器环境,这里可以缓存处理器环境,在process中发生异常等,可以打断通过缓存的变量打断编译执行 |
boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) | 处理方法,类或成员等的注释,并返回该处理器的处理结果。 如果返回true ,则表明注解被当前处理器处理,并且不会要求后续处理器继续处理; 如果返回false ,则表示未处理传入的注解,继续传递给后续处理器处理. RoundEnvironment参数用于查找使用了指定注解的元素,这里的元素有多种,方法,成员,类等,和ElementType取值范围一致 |
Set getSupportedAnnotationTypes() | 获取注解处理器要处理的注解类型,如果在注解处理器类上使用了@SupportedAnnotationTypes注解修饰,则这里返回的Set应和注解取值一致 |
SourceVersion getSupportedSourceVersion() | 注解处理器支持的Java版本,如果在注解处理器类上使用了@SupportedSourceVersion注解修饰,则这里返回的取值应该和注解取值一致 |
下面我们按照上述描述重写Processor代码如下:
@AutoService(Processor.class)
public class Processor extends AbstractProcessor {
// 注解处理器运行环境
private ProcessingEnvironment mProcessingEnvironment;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mProcessingEnvironment = processingEnv;
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
@Override
public SourceVersion getSupportedSourceVersion() {
// 支持到最新的java版本
return SourceVersion.latestSupported();
}
}
由于该处理器主要处理的是@Singleton注解,故getSupportedAnnotationTypes实现如下(singleton-processor模块依赖singleton-annotation模块):
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> hashSet = new HashSet<>();
// 添加注解类的完整名称到HashSet中
hashSet.add(Singleton.class.getCanonicalName());
return hashSet;
}
随后我们来看下process函数的实现,process内部逻辑实现一般分为三步:
- 获取代码中被使用该注解的所有元素,这里的元素指的是组成程序的元素,可能是程序包,类本身、类的变量、方法等
- 筛选符合要求的元素,根据注解的使用场景筛选第一步中得到的所有元素,比如Singleton这个注解作用于类,就从第一步的结果中筛选出所有的类元素
- 遍历筛选出的元素,按照预设规则进行检查
按照上述步骤实现的Singleton注解处理器的process函数如下所示:
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 1.通过RoundEnvironment查找所有使用了Singleton注解的Element
// 2.随后通过ElementFilter获取该元素里面的所有类元素
// 3.遍历所有的类元素,针对自己关注的方法字段进行处理
for (TypeElement typeElement: ElementFilter.typesIn(roundEnvironment.getElementsAnnotatedWith(Singleton.class))) {
// 检查构造函数
if (!checkPrivateConstructor(typeElement)) {
return false;
}
// 检查getInstance方法
if (!checkGetInstanceMethod(typeElement)) {
return false;
}
}
return true;
}
ElementFilter.typesIn就是用来筛选查找出来的结果中的类元素,在ElementFilter类内部定义了五个元素组,如下所示:
- CONSTRUCTOR_KIND:构造器元素组
- FIELD_KINDS:成员变量元素组
- METHOD_KIND:方法元素组
- PACKAGE_KIND:包元素组
- MODULE_KIND:模块元素组
- TYPE_KINDS:类元素组
其中类元素组囊括的最多,包括CLASS,ENUM,INTERFACE等
checkPrivateConstructor
public boolean checkPrivateConstructor(TypeElement typeElement) {
// 通过typeElement.getEnclosedElements()获取在此类或接口中直接声明的字段,方法等元素,随后使用ElementFilter.constructorsIn筛选出构造方法
List<ExecutableElement> constructors = ElementFilter.constructorsIn(typeElement.getEnclosedElements());
for (ExecutableElement constructor : constructors) {
// 判断构造方式是否是Private修饰的
if (constructor.getModifiers().isEmpty() || !constructor.getModifiers().contains(Modifier.PRIVATE)) {
mProcessingEnvironment.getMessager().printMessage(Diagnostic.Kind.ERROR, "constructor of a singleton class must be private", constructor);
return false;
}
}
return true;
}
checkPrivateConstructor实现逻辑如上,代码比较简单,不做赘述。
checkGetInstanceMethod
public boolean checkGetInstanceMethod(TypeElement typeElement) {
// 通过ElementFilter.constructorsIn筛选出该类中声明的所有方法
List<ExecutableElement> methods = ElementFilter.methodsIn(typeElement.getEnclosedElements());
for (ExecutableElement method : methods) {
System.out.println(TAG+method.getSimpleName());
// 检查方法名称
if (method.getSimpleName().contentEquals("getInstance")) {
// 检查方法返回类型
if (mProcessingEnvironment.getTypeUtils().isSameType(method.getReturnType(), typeElement.asType())) {
// 检查方法修饰符
if (!method.getModifiers().contains(Modifier.PUBLIC)) {
mProcessingEnvironment.getMessager().printMessage(Diagnostic.Kind.ERROR, "getInstance method should have a public modifier", method);
return false;
}
if (!method.getModifiers().contains(Modifier.STATIC)) {
mProcessingEnvironment.getMessager().printMessage(Diagnostic.Kind.ERROR, "getInstance method should have a static modifier", method);
return false;
}
}
}
}
return true;
}
checkGetInstanceMethod实现逻辑如上,可以看出当不满足我们预设条件时会通过printMessage向外抛出异常,中断编译执行。
使用Singleton注解,查看注解处理器效果
在app模块中添加SingleTest.java并应用注解,代码如下:
@Singleton
public class SingletonTest {
private SingletonTest(){}
private static SingletonTest getInstance(){
return new SingletonTest();
}
}
可以看到该代码存在问题,我们要求getInstance方法要用public static修饰,这里使用的是private,运行程序,看我们的注解处理器是否能发现该问题并打断程序执行,运行结果如下图:
可以看到程序确实停止运行,并抛出了编译时异常,至此我们自定义编译时注解的操作就学习完了。
扩展
在注解使用方法一节中,我们提到编译时注解即Retention=RetentionPolicy.SOURCE的注解仅在源码中保留,接下来我们验证一下,反编译前文中通过注解处理器检查正常运行的apk,找到SingletonTest类,可以看到在其字节码文件中确实不存在注解代码,如下图所示:
链接:https://juejin.cn/post/7196977951970918460
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。