Android進階——Java註解實戰之APT構建模組化的第一步

Android架構木木發表於2019-05-10

前言

APT的學習要花點時間去掌握和實踐的,短時間內只能掌握知識點,更多的是在實戰中去實踐。其實,APT就是一種工具而已,只要用多了,自然就會熟練了,不過要想實踐之前,還是必須把基礎知識學好才能實戰進入開發。文章會從基礎用例講解知識點,然後再通過實戰進行實踐

APT簡介

APT(Annotation Processing Tool)是一種處理註解的工具,它會對原始碼中的註解進行額外的處理,比如在編譯時生成一些重複性操作的Java程式碼,或者不需要程式設計師去關心的Java程式碼等。 在使用APT的過程中會涉及到下面兩個第三方庫的使用

  1. AutoService:這個庫的主要作用是註冊註解,並對其生成META-INF的配置資訊
  2. JavaPoet:這個庫的主要作用是幫助我們通過類呼叫的形式來生成Java程式碼

APT主要過程包括初始化過程和註解處理過程

  1. 初始化過程:獲取APT提供的工具類,為後面的註解處理提供幫助
  2. 註解處理過程:獲取註解的元素,對元素進行額外處理,可用JavaPoet生成Java程式碼

APT流程

1、定義註解

該註解是可以在我們的專案中使用到的,且規定為註解元素的型別為Type,和在編譯時生效

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ModuleWrapper {

}
複製程式碼

2、定義Processor

Processor會在編譯期對註解進行解析,取出對應的元素進行處理。至於AutoService則是固定的寫法,加個註解即可

@AutoService(Processor.class)
public class ModuleProcessor extends AbstractProcessor {

    private Filer filerUtils; // 檔案寫入
    private Elements elementUtils; // 操作Element工具類
    private Messager messagerUtils; // Log 日誌
    private Map<String, String> options; // 額外配置引數

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);

        filerUtils = processingEnvironment.getFiler();
        elementUtils = processingEnvironment.getElementUtils();
        messagerUtils = processingEnvironment.getMessager();
        options = processingEnvironment.getOptions();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(ModuleWrapper.class.getCanonicalName());
        return types;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        initModuleMap(roundEnvironment);
        return false;
    }

    private void initModuleMap(RoundEnvironment roundEnv) {
        //獲取對應的註解元素
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(ModuleWrapper.class);
        for (Element element : set) {
            //如果是個類
            if (element.getKind() == ElementKind.CLASS) {
                //獲取類名
                String clzName = element.getSimpleName().toString();
                //對元素進行處理,可用javapoet生成Java程式碼
                ......
            } else {
                messagerUtils.printMessage(Diagnostic.Kind.NOTE, "only support class");
            }
        }
    }
}
複製程式碼

3、AbstractProcessor實現方法介紹

  1. init():初始化過程,可通過初始化方法獲取各種工具類
  2. process():註解處理過程,可通過獲取註解元素後,對註解元素進行額外處理
  3. getSupportedAnnotationTypes():獲取需要解析的註解型別

APT知識點

1、初始化介紹

APT初始化階段為init方法的呼叫,我們可以使用ProcessingEnvironment獲取一些實用類以及獲取選項引數等

Android進階——Java註解實戰之APT構建模組化的第一步

2、Element介紹

Element是操作元素最主要的類,可通過getElementsAnnotatedWith獲取Element的元素,經過getKind()判斷元素的型別後,可強制轉換成對應的型別,在對應的型別中有著不同的方法可以呼叫

Android進階——Java註解實戰之APT構建模組化的第一步
ElementKind為元素的型別,元素的型別判斷不需要用instanceof去判斷,而應該通過**getKind()**去判斷對應的型別
Android進階——Java註解實戰之APT構建模組化的第一步

3、TypeMirror介紹

TypeMirror是一個介面,表示Java程式語言中的型別。這些型別包括基本型別、引用型別、陣列型別、型別變數和null型別等等

Android進階——Java註解實戰之APT構建模組化的第一步
TypeKind為型別的屬性,型別的屬性判斷不需要用instanceof去判斷,而應該通過**getKind()**去判斷對應的屬性
Android進階——Java註解實戰之APT構建模組化的第一步

這裡需要注意的是,如果我們通過註解去獲取Class型別的值,如果獲取的Class未被編譯,則會丟擲MirroredTypeException異常,此時我們需要通過try-catch語句在catch裡去獲取我們所需要的類元素

try {  
    annotation.value();//如果value為Class型別則會報異常
} catch (MirroredTypeException mte) {
    DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();
    TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();//通過異常去獲取類元素
}
複製程式碼

4、Filer介紹

Filer介面支援通過註解處理器建立新檔案。可以建立三種檔案型別:原始檔、類檔案和輔助資原始檔

Android進階——Java註解實戰之APT構建模組化的第一步

5、Messager介紹

Messager介面提供註解處理器用來報告錯誤訊息、警告和其他通知的方式

Android進階——Java註解實戰之APT構建模組化的第一步

6、Options介紹

通過getOptions()方法獲取選項引數,在gradle檔案中配置選項引數值

android {
    defaultConfig {
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [ version : '1.0.0' ]
            }
        }
    }
}
複製程式碼

通過ProcessingEnvironment去獲取對應的引數

processingEnvironment.getOptions().get("version");
複製程式碼

7、獲取註解元素

通過RoundEnvironment介面去獲取註解元素,通過JavaPoet生成Java程式碼

Android進階——Java註解實戰之APT構建模組化的第一步

APT實戰

下面通過APT的實戰,進行對專案的模組化劃分

1、專案結構

  1. 建立Module,名為annotation,放置我們的註解類
  2. 建立Module,名為compiler,放置我們的註解處理類
  3. 主工程則直接依賴annotation和compiler 注意事項:建立Module的時候,需要選擇java Lib,而不是Android Lib

2、Gradle配置

annotation的Module必須宣告Java編譯版本

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"
複製程式碼

compiler的Module必須宣告Java編譯版本,且依賴於annotation和匯入我們所需的庫

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation project(':annotation')
    implementation 'com.google.auto.service:auto-service:1.0-rc2'
    implementation 'com.squareup:javapoet:1.7.0'
}

sourceCompatibility = "1.7"
targetCompatibility = "1.7"
複製程式碼

主工程必須通過依賴annotaion和compiler,由於我們只是在編譯期生效,可用annotationProcessor

implementation project(':annotation')
annotationProcessor project(':compiler')
複製程式碼

注意事項:定義編譯的jdk版本為1.7

3、定義註解

ModuleWrapper註解,表示需要載入的模組

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ModuleWrapper {

}
複製程式碼

IModule介面,表示當前類是一個模組類

public interface IModule {
    String getModuleName();
}
複製程式碼

4、定義Processor

@AutoService(Processor.class)
public class ModuleProcessor extends AbstractProcessor {

    private Map<String, ModuleInfo> moduleMaps = new HashMap<>();

    private Filer filerUtils; // 檔案寫入
    private Elements elementUtils; // 操作Element 的工具類
    private Messager messagerUtils; // Log 日誌
    private Map<String, String> options;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);

        filerUtils = processingEnvironment.getFiler();
        elementUtils = processingEnvironment.getElementUtils();
        messagerUtils = processingEnvironment.getMessager();
        options = processingEnvironment.getOptions();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(ModuleWrapper.class.getCanonicalName());
        return types;
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        try {
            initModuleMap(roundEnvironment);
            createModuleMap();
            createModuleConstant();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 通過註解元素獲取元件實體
     *
     * @param roundEnv
     */
    private void initModuleMap(RoundEnvironment roundEnv) {
        Set<? extends Element> set = roundEnv.getElementsAnnotatedWith(ModuleWrapper.class);
        for (Element element : set) {
            if (element.getKind() == ElementKind.CLASS) {
                String clzName = element.getSimpleName().toString();
                if (moduleMaps.get(clzName) == null) {
                    ModuleInfo info = new ModuleInfo(elementUtils, (TypeElement) element);
                    moduleMaps.put(clzName, info);
                }
            } else {
                messagerUtils.printMessage(Diagnostic.Kind.NOTE, "only support class");
            }
        }
    }

    /**
     * 建立元件管理者
     *
     * @throws IOException
     */
    private void createModuleMap() throws IOException {

        FieldSpec fieldSpec = FieldSpec
                .builder(ParameterizedTypeName.get(HashMap.class, String.class, IModule.class)
                        , "moduleMap", Modifier.PRIVATE)
                .initializer("new HashMap<>()")
                .build();

        CodeBlock.Builder codeBlock = CodeBlock.builder();
        for (String key : moduleMaps.keySet()) {
            ModuleInfo info = moduleMaps.get(key);
            codeBlock.addStatement("moduleMap.put($S ,new $T())", info.getFullClassName(),
                    ClassName.get(info.packageName, info.className));
        }

        MethodSpec initMethod = MethodSpec.methodBuilder("init")
                .addModifiers(Modifier.PUBLIC)
                .addCode(codeBlock.build())
                .returns(TypeName.VOID)
                .build();

        MethodSpec getMethod = MethodSpec.methodBuilder("get")
                .addModifiers(Modifier.PUBLIC)
                .addParameter(String.class, "cls")
                .addStatement("return moduleMap.get(cls)")
                .returns(IModule.class)
                .build();

        ArrayList<MethodSpec> methods = new ArrayList<>();
        methods.add(initMethod);
        methods.add(getMethod);

        TypeSpec moduleFactory = TypeSpec.classBuilder("ModuleFactory")
                .addModifiers(Modifier.PUBLIC)
                .addMethods(methods)
                .addField(fieldSpec)
                .build();

        JavaFile javaFile = JavaFile.builder("com.hensen.compiler.processor", moduleFactory)
                .build();
        javaFile.writeTo(filerUtils);
    }

    /**
     * 建立元件常量
     *
     * @throws IOException
     */
    private void createModuleConstant() throws IOException {

        TypeSpec.Builder moduleConstant = TypeSpec.classBuilder("ModuleConstant")
                .addModifiers(Modifier.PUBLIC);

        for (String key : moduleMaps.keySet()) {
            ModuleInfo info = moduleMaps.get(key);
            FieldSpec fieldSpec = FieldSpec.builder(String.class, info.className)
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
                    .initializer("$S", info.getFullClassName())
                    .build();
            moduleConstant.addField(fieldSpec);
        }

        JavaFile javaFile = JavaFile.builder("com.hensen.compiler.processor", moduleConstant.build())
                .build();
        javaFile.writeTo(filerUtils);
    }
}
複製程式碼

5、元件實體

元件實體儲存著元件的資訊

public class ModuleInfo {

    public String packageName;
    public String className;

    public ModuleInfo(Elements elementUtils, TypeElement typeElement) {
        packageName = getPackageName(elementUtils, typeElement);
        className = getClassName(typeElement, packageName);
    }

    public String getClassName(TypeElement type, String packageName) {
        int packageLen = packageName.length() + 1;
        return type.getQualifiedName().toString().substring(packageLen)
                .replace('.', '$');
    }

    public String getPackageName(Elements elementUtils, TypeElement classElement) {
        PackageElement packageElement = elementUtils.getPackageOf(classElement);
        return packageElement.getQualifiedName().toString();
    }

    public String getFullClassName() {
        return packageName + "." + className;
    }

    public String getClassName(){
        return className;
    }
}
複製程式碼

6、註解使用

分別建立出禮物模組和聊天模組,聊天模組增加發訊息的方法

@ModuleWrapper
public class ChatModule implements IModule{

    @Override
    public String getModuleName() {
        return "ChatModule";
    }

    public void sendMessage() {
        Log.i("TAG", "Hi");
    }
}

@ModuleWrapper
public class GiftModule implements IModule{

    @Override
    public String getModuleName() {
        return "GiftModule";
    }
}
複製程式碼

7、生成程式碼

在生成程式碼之前需要gradle build,檢視生成程式碼。當然對於模組來說,不僅有get()、還有add()、remove()等其他擴充套件功能,具體的就留給大家去操作

public class ModuleFactory {
  private HashMap<String, IModule> moduleMap = new HashMap<>();

  public void init() {
    moduleMap.put("com.hensen.geneapt.GiftModule" ,new GiftModule());
    moduleMap.put("com.hensen.geneapt.ChatModule" ,new ChatModule());
  }

  public IModule get(String cls) {
    return moduleMap.get(cls);
  }
}

public class ModuleConstant {
  public static final String GiftModule = "com.hensen.geneapt.GiftModule";

  public static final String ChatModule = "com.hensen.geneapt.ChatModule";
}
複製程式碼

8、元件使用

初始化元件的載入,通過工廠獲取對應的模組進行操作

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ModuleFactory moduleFactory = new ModuleFactory();
        moduleFactory.init();

        ChatModule chatModule = (ChatModule) moduleFactory.get(ModuleConstant.ChatModule);
        chatModule.sendMessage();
    }
}
複製程式碼

相關文章