开启成长之旅!这是我参加「日新方案 2 月更文应战」的第 2 天,点击查看活动概况

前文提到注解依照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-单例代码规范检查