likes
comments
collection
share

APT-单例代码规范检查

作者站长头像
站长
· 阅读数 18

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

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

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

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

singleton-annotation 注解模块

新建singleton-annotation Java模块

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

APT-单例代码规范检查

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

APT-单例代码规范检查

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

APT-单例代码规范检查

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

    APT-单例代码规范检查

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

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

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

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

    APT-单例代码规范检查

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

    APT-单例代码规范检查

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

singleton-processor 注解处理模块

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

APT-单例代码规范检查

添加注解处理器声明

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

    APT-单例代码规范检查

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

APT-单例代码规范检查

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

扩展

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

APT-单例代码规范检查

转载自:https://juejin.cn/post/7196977951970918460
评论
请登录