注册

APT-单例代码规范检查

前文提到注解按照Retention可以取值可以分为SOURCE,CLASS,RUNTIME三类,在定义注解完成后,可以结合APT进行注解解析,读取到注解相关信息后进行一些检查和设置。


接下来我们实现一个单例注解来修饰单例类,当开发人员写的单例类不符合编码规范时,在编译过程中抛出异常。大家都知道,单例类应该具有以下特点:



  • 构造器私有
  • 具有public static修饰的getInstance方法

打开Android Studio,新建SingletonAnnotationDemo工程,随后在该工程中进行注解的定义和APT的开发,一般情况下注解和与之关联的APT都会以单独的Module声明在项目中,下面我们开始实践吧。


singleton-annotation 注解模块


新建singleton-annotation Java模块

打开新建的SingletonAnnotationDemo项目,在右上角切换至Project视图,如下图所示:


1-7-2-1


切换完成后,在项目名称上右键单击,在弹出的菜单中依此选择new->Module,如下图所示:


1-7-2-2


选择Module条目后,弹出如下对话框,依次操作如下图所示:


1-7-2-3


其中标记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模块,操作指导如下图所示:


    1-7-2-4


    选择Open Module Settings后弹框如下图所示,选择dependencies,代表依赖管理,随后在右侧的Module列表中选择你要操作的模块,这里选择app,最后点击选择app模块后,其右侧依赖列表中的加号,选择Module Dependency,代表添加模块依赖



    Module Dependency:模块依赖,一般用于添加项目中的其他模块作为依赖


    Library Dependency:库依赖,一般用于添加上传到maven,google,jcenter等位置的开源库


    JAR/AAR Dependency:一般用于添加本地已有的jar或aar文件作为依赖时使用



    1-7-2-5


    选择添加模块依赖后,弹出窗体如下图所示:


    1-7-2-6


    在上图中标记1的位置勾选要添加的模块,在2的位置选择依赖方式,随后点击OK等待同步完成即可。




singleton-processor 注解处理模块


与创建singleton-annotation模块一样,以相同的方式创建一个名为singleton-processor的模块,其内部有一个Processor类,创建完成后,项目模块如下图所示(Processor类为创建模块时输入的类名,AS自动生成的):


1-7-2-7


添加注解处理器声明

将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相关的目录,如下图所示:


    1-7-2-8


    其中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内部逻辑实现一般分为三步:



  1. 获取代码中被使用该注解的所有元素,这里的元素指的是组成程序的元素,可能是程序包,类本身、类的变量、方法等
  2. 筛选符合要求的元素,根据注解的使用场景筛选第一步中得到的所有元素,比如Singleton这个注解作用于类,就从第一步的结果中筛选出所有的类元素
  3. 遍历筛选出的元素,按照预设规则进行检查

按照上述步骤实现的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,运行程序,看我们的注解处理器是否能发现该问题并打断程序执行,运行结果如下图:


1-7-2-9


可以看到程序确实停止运行,并抛出了编译时异常,至此我们自定义编译时注解的操作就学习完了。


扩展


在注解使用方法一节中,我们提到编译时注解即Retention=RetentionPolicy.SOURCE的注解仅在源码中保留,接下来我们验证一下,反编译前文中通过注解处理器检查正常运行的apk,找到SingletonTest类,可以看到在其字节码文件中确实不存在注解代码,如下图所示:


1-7-2-10


作者:小海编码日记
链接:https://juejin.cn/post/7196977951970918460
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册