組件化的架構設計(一):gradle組件化配置、APT+javapoet介紹和使用

博客主頁html

參考:java

gradle組件化配置

Android組件化開發的配置,離不開gradle構建工具,它的出現讓工程有無限的可能。gradle核心是基於groovy腳本語言,groovy腳本基於java且擴展了java,因此gradle須要依賴JDK和Groovy庫。android

gradle簡單介紹

gradle語法:從gradle的日誌輸出開始講解組件化設計之旅git

// 第一種打印字符串的方式
println("hello, gradle!")

// 第二種打印字符串的方式
println "hello2, gradle!"

從這兩種字符串輸出方式能夠看出,方法能夠不寫括號,一句話後能夠不寫分號,這是groovy的特性。github

能夠在Android工程中的build.gradle文件中使用println函數輸出日誌。而後經過 Build->Toggle view 查看build輸出的日誌編程

自定義屬性

gradle能夠添加額外的自定義屬性,經過ext屬性實現。先新建一個config.gradle文件,並自定義isRelease屬性,用於動態切換:組件化模式/集成化模式segmentfault

ext {
    // false: 組件化模式(子模塊能夠獨立運行)
    // true :集成化模式(打包整個項目apk,子模塊不可獨立運行)
    isRelease = true
}

那麼這個config文件怎麼使用呢?須要在項目的根build.gradle文件經過 apply from 方式引用config.gradle文件api

// build.gradle

// 能夠經過apply from方式引用
apply from: 'config.gradle'

而後在app應用項目的build.gradle文件中使用自定義屬性網絡

// build.gradle

// 使用自定義屬性,屬性須要寫在${}中
println "${rootProject.ext.isRelease}"

config.gradle文件基本配置

再新建一個購物shop庫模塊,項目結構以下圖:

查看app應用模塊和shop庫模塊中的build.gradle文件,發現上圖紅色框配置不少相似,那麼是否是可使用分模塊方式配置呢,使用gradle自定義屬性將共性的配置抽取出來,放在單獨的文件裏,供其餘build引用?答案是能夠的。閉包

接下來動手操做下,紅色框的配置都是跟Android版本有關的,能夠定義一個Map集合存相關屬性信息,配置以下:

// config.gradle

ext {
    // 定義Map存取版本相關的信息,key名稱能夠任意取
    versions = [
            compileSdkVersion: 29,
            buildToolsVersion: "29.0.2",
            minSdkVersion    : 21,
            targetSdkVersion : 29,
            versionCode      : 1,
            versionName      : "1.0"
    ]
}

而後在app應用模塊和shop庫模塊中的build.gradle文件,訪問Map集合中定義的屬性,例如app應用模塊build.gradle中訪問Map中自定義的屬性

// 獲取Map
def versions = rootProject.ext.versions

android {
    // 直接經過map.key訪問值
    compileSdkVersion versions.compileSdkVersion
    buildToolsVersion versions.buildToolsVersion
    defaultConfig {
        applicationId "com.example.modular.todo"
        minSdkVersion versions.minSdkVersion
        targetSdkVersion versions.targetSdkVersion
        versionCode versions.versionCode
        versionName versions.versionName
    }
}

1. applicationId配置
在組件化模塊與集成化模塊作切換,組件化模塊爲了可以獨立運行,將庫模塊切換成應用模塊時須要設置applicationId

因此還須要配置不一樣的applicationId屬性

// config.gradle

ext {
    // 組件化與集成化切換時,設置不一樣的applicationId
    appId = [
            app : "com.example.modular.todo",
            shop: "com.example.modular.shop"
    ]
}

在app應用模塊中的build.gradle文件中使用

def appId = rootProject.ext.appId
android {
    defaultConfig {
        applicationId appId.app
    }
}

2. 代碼中生產和正式環境配置切換
有的時候還須要在代碼中切換生產和正式環境配置,如:網絡請求的URL。Android爲咱們提供自定義BuildConfig功能。

// config.gradle

ext {
    baseUrl = [
            debug  : "https://127.0.0.1/debug", // 測試版本URL
            release: "https://127.0.0.1/relase" // 正式版本URL
    ]
}

在app應用模塊build.gradle文件中經過buildConfigField配置,在代碼中就能夠經過BuildConfig.baseUrl訪問到了。

// build.gradle

def baseUrl = rootProject.ext.baseUrl
android {
    buildTypes {
        debug {
            // void buildConfigField(
            //            @NonNull String type,
            //            @NonNull String name,
            //            @NonNull String value) { }
            buildConfigField("String", "baseUrl", "\"${baseUrl.debug}\"")
        }

        release {
            buildConfigField("String", "baseUrl", "\"${baseUrl.release}\"")
        }
    }
}

3. dependencies依賴配置

// config.gradle

ext {
    appcompatVersion = "1.0.2"
    constraintlayoutVersion = "1.1.3"
    dependencies = [
            appcompat       : "androidx.appcompat:appcompat:${appcompatVersion}",
            constraintlayout: "androidx.constraintlayout:constraintlayout:${constraintlayoutVersion}",
    ]

    tests = [
            "junit"        : "junit:junit:4.12",
            "espresso"     : "androidx.test.espresso:espresso-core:3.1.1",
            "androidJunit": "androidx.test.ext:junit:1.1.0"
    ]
}

app模塊中build.gradle文件引用

// build.gradle

def supports = rootProject.ext.dependencies
def tests = rootProject.ext.tests

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    // 標準寫法
    // implementation group: 'androidx.appcompat', name: 'appcompat', version: '1.0.2'
//    implementation supports.appcompat
//    implementation supports.constraintlayout

    // supports 依賴
    supports.each { key, value -> implementation value }

    testImplementation tests.junit
    androidTestImplementation tests.espresso
    androidTestImplementation tests.androidJunit
}

4. 簽名配置
簽名配置時須要注意:signingConfigs 必須寫在buildTypes以前

android {
    // 簽名配置(隱形坑:必須寫在buildTypes以前)
    signingConfigs {
        debug {
            // 天坑:填錯了,編譯不經過還找不到問題
            storeFile file('/Users/xujinbing839/.android/debug.keystore')
            storePassword "android"
            keyAlias "androiddebugkey"
            keyPassword "android"
        }
        release {
            // 簽名證書文件
            storeFile file('/Users/xujinbing839/work/mycode/todo/todo_modular/keystore/modular')            
            storeType "modular" // 簽名證書的類型            
            storePassword "123456" // 簽名證書文件的密碼        
            keyAlias "modular" // 簽名證書中密鑰別名  
            keyPassword "123456" // 簽名證書中該密鑰的密碼
            v2SigningEnabled true // 是否開啓V2打包
        }
    }

    buildTypes {
        debug {
            // 對構建類型設置簽名信息
            signingConfig signingConfigs.debug
        }

        release {
            // 對構建類型設置簽名信息
            signingConfig signingConfigs.release
        }
    }
}

其它配置

android {
    defaultConfig {
        // 開啓分包
        multiDexEnabled true
        // 設置分包配置
        // multiDexKeepFile file('multidex-config.txt')

        // 將svg圖片生成 指定維度的png圖片
        // vectorDrawables.generatedDensities('xhdpi','xxhdpi')
        // 使用support-v7兼容(5.0版本以上)
        vectorDrawables.useSupportLibrary = true
        // 只保留指定和默認資源
        resConfigs('zh-rCN')

        // 配置so庫CPU架構(真機:arm,模擬器:x86)
        // x86  x86_64  mips  mips64
        ndk {
            //abiFilters('armeabi', 'armeabi-v7a')
            // 爲了模擬器啓動
            abiFilters('x86', 'x86_64')
        }
    }

    // AdbOptions 能夠對 adb 操做選項添加配置
    adbOptions {
        // 配置操做超時時間,單位毫秒
        timeOutInMs = 5 * 1000_0

        // adb install 命令的選項配置
        installOptions '-r', '-s'
    }
    // 對 dx 操做的配置,接受一個 DexOptions 類型的閉包,配置由 DexOptions 提供
    dexOptions {
        // 配置執行 dx 命令是爲其分配的最大堆內存
        javaMaxHeapSize "4g"
        // 配置是否預執行 dex Libraries 工程,開啓後會提升增量構建速度,不過會影響 clean 構建的速度,默認 true
        preDexLibraries = false
        // 配置是否開啓 jumbo 模式,代碼方法是超過 65535 須要強制開啓才能構建成功
        jumboMode true
        // 配置 Gradle 運行 dx 命令時使用的線程數量
        threadCount 8
        // 配置multidex參數
        additionalParameters = [
                '--multi-dex', // 多dex分包
                '--set-max-idx-number=50000', // 每一個包內方法數上限
                // '--main-dex-list=' + '/multidex-config.txt', // 打包到主classes.dex的文件列表
                '--minimal-main-dex'
        ]
    }
    // 執行 gradle lint 命令便可運行 lint 檢查,默認生成的報告在 outputs/lint-results.html 中
    lintOptions {
        // 遇到 lint 檢查錯誤會終止構建,通常設置爲 false
        abortOnError false
        // 將警告看成錯誤來處理(老版本:warningAsErros)
        warningsAsErrors false
        // 檢查新 API
        check 'NewApi'
    }
}

組件化詳細部署

組件化開發的意義

什麼是組件化開發?
組件化開發就是將一個app分紅多個模塊,每一個模塊都是一個組件(Module),開發的過程當中咱們可讓這些組件相互依賴或者單獨調試部分組件,可是最終發佈的時候是將這些組件合併統一成一個apk,這就是組件化開發。

組件化和插件化開發略有不一樣:
插件化開發時將整個app拆分紅不少模塊,這些模塊包括一個宿主和多個插件,每一個模塊都是一個apk(組件化的每一個模塊是個lib),最終打包的時候將宿主apk和插件apk分開打包。

爲何要組件化呢?
1. 開發需求
不相互依賴、能夠相互交互、任意組合、高度解耦
2. 團隊效率

library與application區別、切換

Phone Module 和 Android Library區別:

Phone Module是一個能夠獨立運行,編譯成一個apk,且build.gradle文件中須要配置applicationId;而Android Library 不能獨立運行,不能單獨編譯成一個apk,且build.gradle文件中不須要配置applicationId。

Phone Module 和 Android Library切換:

下面以子模塊購物shop爲例:

若是購物shop模塊可以獨立編譯apk,就須要切換爲Phone Module(也就是組件化模式),經過isRelease動態切換:

  1. 當isRelease爲true:集成化模式(也就是可打包整個項目apk),子模塊購物shop不可獨立運行
  2. 當isRelease爲false:組件化模式,子模塊購物shop能夠獨立運行

其中isRelease變量是config.gradle中定義的屬性

// build.gradle

if (isRelease) { // 若是是發佈版本時,各個模塊都不能獨立運行
    apply plugin: 'com.android.library'
} else {
    apply plugin: 'com.android.application'
}

android {
    defaultConfig {
        // 若是是集成化模式,不能有applicationId
        if (!isRelease) applicationId appId.shop
    }
}

當子模塊購物shop從library模塊切換爲application模塊時,可能須要編寫測試代碼,如:啓動的入口。

使用sourceSets配置,將測試的代碼放入debug文件夾中,當切換到集成化模式時,打包成apk時移除全部的debug代碼(也就是debug代碼不會打包到apk中)。

android {
    // 配置資源路徑,方便測試環境,打包不集成到正式環境
    sourceSets {
        main {
            if (!isRelease) {
                // 若是是組件化模式,須要單獨運行時
                manifest.srcFile 'src/main/debug/AndroidManifest.xml'
            } else {
                // 集成化模式,整個項目打包apk
                manifest.srcFile 'src/main/AndroidManifest.xml'
                java {
                    // release 時 debug 目錄下文件不須要合併到主工程
                    exclude '**/debug/**'
                }
            }
        }
    }
}

組件化開發規範:

  1. 子模塊購物shop:在src類和res資源命令中添加前綴,shop_,如:shop_activity_home
  2. app模塊:能夠不修改,默認

Module與Module之間交互

Module間怎麼交互(包括:跳轉、傳參等)?方式有不少:

  1. EventBus 很是混亂,難以維護
  2. 反射 反射技術能夠成功,可是維護成本較高,且出現高版本的@hide限制
  3. 隱式意圖 維護成本還好,就是比較麻煩,須要維護Manifest的action
  4. BroadCastReceiver 須要動態註冊(7.0後),需求方發送廣播
  5. 類加載 須要準確的全類名路徑,維護成本較高且容易出現人爲失誤

第一種實現方案:類加載技術交互

經過Class的forName加載目標類,須要準確知道目標類的全類名路徑。

private void jump() {
    try {
        Class<?> targetClass = Class.forName("com.example.modular.shop.ShopActivity");

        Intent intent = new Intent(this, targetClass);
        intent.putExtra("moduleName","app");
        startActivity(intent);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }
}

第二種實現方案:全局Map記錄路徑信息

要跳轉,若是知道了目標類的Class對象,不就能夠跳轉了,接下來只須要解決目標類的Class對象查找就能夠了。

能夠定義一個PathBean用於封裝目標類的相關信息,如:目標類的全路徑名,目標類的Class對象

public class PathBean {

    public String path; // 跳轉目標全類名
    public Class<?> targetClass; // 跳轉目標類的Class對象

    public PathBean(String path, Class<?> targetClass) {
        this.path = path;
        this.targetClass = targetClass;
    }
}

一個模塊中會有不少PathBean,能夠List存取PathBean,而又有不少模塊,可使用Map區分不一樣的模塊。

// key:模塊名,如shop模塊  value:該模塊下全部的Activity路徑信息
 private static final Map<String, List<PathBean>> mGroupMap = new HashMap<>();

mGroupMap是一個Map,key是模塊名,如:shop模塊;value是該模塊下全部的Activity路徑信息.

將全部的路徑信息加入到mGroupMap中,使用時經過根據組名(模塊名)和路徑名獲取目標類對象。

/**
 * 將路徑信息加入到全局的Map中
 */
public static void addGroup(String groupName, String path, Class<?> targetClass) {
    List<PathBean> pathBeans = mGroupMap.get(groupName);
    if (pathBeans == null) {
        pathBeans = new ArrayList<>();
        pathBeans.add(new PathBean(path, targetClass));
        mGroupMap.put(groupName, pathBeans);
    } else {
        pathBeans.add(new PathBean(path, targetClass));
    }
}

/**
 * 根據組名和路徑名獲取目標類
 */
public static Class<?> findTargetClass(String groupName,String path) {
    List<PathBean> pathBeans = mGroupMap.get(groupName);
    if (pathBeans != null) {
        for (PathBean pathBean : pathBeans) {
            if (!TextUtils.isEmpty(path) && path.equalsIgnoreCase(pathBean.path)) {
                return pathBean.targetClass;
            }
        }
    }
    return null;
}

APT介紹和使用

APT介紹

APT(Annotation Processing Tools)是一種處理註釋的工具,它對源代碼文件進行檢測找出其中的Annotaion,使用Annotation進行額外的處理。Annotation處理器在處理Annotation時能夠根據源文件中的Annotaion生成額外的源文件和其它的文件(文件具體內容由Annotaion處理器的編寫者決定),APT還會編譯生成的源文件和原來的源文件,將它們一塊兒生成class文件。

通俗的理解:根據規則,幫助咱們生成代碼,生成類文件。

1. APT核心實現原理

編譯時Annotation解析的基本原理是,在某些代碼元素上(如類型、函數、字段等)添加註解,在編譯時javac編譯器會檢查AbstractProcessor的子類,而且調用該類型的process函數,而後將添加了註解的全部元素都傳遞到process函數中,使得開發人員能夠在編譯器進行相應的處理,例如:根據註解生成新的java類,這也就是ARouter、Butterknife、Dragger等開源庫的基本原理。

2. java源文件編程層Class文件

工具是經過javac工具,註解處理器是一個在javac中的,用來編譯時掃描和處理的註解工具。能夠認爲是特定的註解,註冊你本身的註解處理器。

3. 怎麼註冊註解處理器
MyProcessor到javac中。你必須提供一個.jar文件。就像其它.jar文件同樣,你打包你的註解處理器到此文件中。而且,在你的jar中,你須要打包一個特定的文件javax.annotation.processing.Processor到META-INF/services路徑下。

知識點詳解

1. jar

  • com.google.auto.service:auto-service 谷歌提供的java生成源代碼庫
  • com.squareup:javapoet 提供了各類API讓你用各類姿式去生成java代碼文件

2. @AutoService

這是一個其它註解處理器中引入的註解。AutoService註解處理器是Google開發的,用來生成META-INF/services/javax.annotation.processing.Processor文件的。咱們能夠在註解處理器中使用註解。很是方便

3. 結構體語言

對於java源文件來講,也是一種結構體語言。JDK中提供了用來描述結構體語言元素的接口。

在註解處理過程當中,咱們掃描全部的java源文件。源代碼的每一部分都是一個特定類型的Element。換句話說:Element表明程序的元素,例如包、類或者方法。每一個Element表明一個靜態的、語言級別的構件。

package com.example; // PackageElement

public class Foo { // TypeElement
    
   private int a; // VariableElement
   private Foo other; // VariableElement

   public Foo {} // ExecutableElement

   public void setA( // ExecutableElement
      int newA // VariableElement ) {}
}

Element程序元素

PackageElement 表示包程序元素。

TypeElement 表示一個類或接口程序元素。

ExecutableElement 表示類或接口的方法,構造函數或初始化器(靜態或實例),包括註釋類型元素。

VariableElement 表示一個字段, 枚舉常量,方法或構造函數參數,局部變量,資源變量或異常參數。

TypeParameterElement 表示通用類,接口,方法或構造函數元素的正式類型參數。

Types
一個用來處理TypeMirror的工具類

Filer
使用Filer你能夠建立java文件

AbstractProcessor核心API

1. process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)

至關於每一個處理器的主函數main()。能夠在這裏寫你的掃描、評估和處理註解的代碼,以及生成java文件。輸入參數RoundEnvironment,可讓你查詢出包含特定註解的被註解元素。

2. getSupportedAnnotationTypes()

這個註解處理器是註冊給哪一個註解的。注意,它的返回值是一個字符串的集合,包含本處理器想要處理的註解類型的合法全稱。換句話說,你在這裏定義你的註解處理器註冊到哪些註解上。

3. getSupportedSourceVersion()

用來指定你使用的java版本。一般這裏返回SourceVersion.latestSupported()。若是你有足夠的理由只支持java6的話,你也能夠返回SourceVersion.RELEASE_6

4. getSupportedOptions()

用來指定註解處理器處理的選項參數。須要在gradle文件中配置選項參數值

// 在gradle文件中配置選項參數值(用於APT傳參接收)
// 切記:必須寫在defaultConfig節點下
javaCompileOptions {
    annotationProcessorOptions {
        arguments = [moduleName: project.getName()]
    }
}

這些API也可使用註解的方式指定:

// AutoService則是固定的寫法,加個註解便可
// 經過auto-service中的@AutoService能夠自動生成AutoService註解處理器,用來註冊
// 用來生成 META-INF/services/javax.annotation.processing.Processor 文件
@AutoService(Processor.class)
// 容許/支持的註解類型,讓註解處理器處理(新增annotation module)
@SupportedAnnotationTypes({"com.example.modular.annotations.ARouter"})
// 指定JDK編譯版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
// 註解處理器接收的參數
@SupportedOptions("moduleName")
public class ARouterProcessor extends AbstractProcessor {
  // ignore
}

ProcessingEnvironment核心API

// ignore
public class ARouterProcessor extends AbstractProcessor {
   // 操做Element工具類 (類、函數、屬性都是Element)
    private Elements elementUtils;

    // type(類信息)工具類,包含用於操做TypeMirror的工具方法
    private Types typeUtils;

    // Messager用來報告錯誤,警告和其餘提示信息
    private Messager messager;

    // 文件生成器 類/資源,Filter用來建立新的源文件,class文件以及輔助文件
    private Filer filer;

    // 模塊名,經過getOptions獲取build.gradle傳過來
    private String moduleName;

    // 該方法主要用於一些初始化的操做,經過該方法的參數ProcessingEnvironment能夠獲取一些列有用的工具類
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        // processingEnv是父類受保護屬性,能夠直接拿來使用。
        // 其實就是init方法的參數ProcessingEnvironment
        // processingEnv.getMessager(); //參考源碼64行
        elementUtils = processingEnvironment.getElementUtils();
        messager = processingEnvironment.getMessager();
        filer = processingEnvironment.getFiler();
        typeUtils = processingEnvironment.getTypeUtils();

       // 經過ProcessingEnvironment去獲取build.gradle傳過來的參數
        Map<String, String> options = processingEnvironment.getOptions();
        if (options != null && !options.isEmpty()) {
            moduleName = options.get("moduleName");
            // 有坑:Diagnostic.Kind.ERROR,異常會自動結束,不像安卓中Log.e那麼好使
            messager.printMessage(Diagnostic.Kind.NOTE, "moduleName=" + moduleName);
        }
    }
}

RoundEnvironment 核心API

// ignore
public class ARouterProcessor extends AbstractProcessor {
      /**
     * 至關於main函數,開始處理註解
     * 註解處理器的核心方法,處理具體的註解,生成Java文件
     *
     * @param set              使用了支持處理註解的節點集合(類 上面寫了註解)
     * @param roundEnvironment 當前或是以前的運行環境,能夠經過該對象查找找到的註解。
     * @return true 表示後續處理器不會再處理(已經處理完成)
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (set.isEmpty()) return false;

        // 獲取全部帶ARouter註解的 類節點
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ARouter.class);
        // 遍歷全部類節點
        for (Element element : elements) {
            // ignore
        }
        // ignore
        return true;
    }
}

Element核心API

APT使用

開發環境的兼容

https://github.com/google/auto

1. Android Studio 3.2.1 + Gradle 4.10.1 臨界版本

dependencies {
     // 註冊註解,並對其生成META-INF的配置信息,rc2在gradle5.0後有坑
     // As-3.2.1 + gradle4.10.1-all + auto-service:1.0-rc2
     implementation 'com.google.auto.service:auto-service:1.0-rc2'
}

2. Android Studio 3.4.1 + Gradle 5.1.1 向下兼容

dependencies {
    // As-3.4.1 + gradle5.1.1-all + auto-service:1.0-rc4
    compileOnly'com.google.auto.service:auto-service:1.0-rc4'
    annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'
}

使用APT技術幫助咱們生成代碼

1. ARouter註解

新建java library工程,工程名爲:annotations。而後建立ARouter註解

/**
 * <ul>
 * <li>@Target(ElementType.TYPE)   // 接口、類、枚舉、註解</li>
 * <li>@Target(ElementType.FIELD) // 屬性、枚舉的常量</li>
 * <li>@Target(ElementType.METHOD) // 方法</li>
 * <li>@Target(ElementType.PARAMETER) // 方法參數</li>
 * <li>@Target(ElementType.CONSTRUCTOR)  // 構造函數</li>
 * <li>@Target(ElementType.LOCAL_VARIABLE)// 局部變量</li>
 * <li>@Target(ElementType.ANNOTATION_TYPE)// 該註解使用在另外一個註解上</li>
 * <li>@Target(ElementType.PACKAGE) // 包</li>
 * <li>@Retention(RetentionPolicy.RUNTIME) <br>註解會在class字節碼文件中存在,jvm加載時能夠經過反射獲取到該註解的內容</li>
 * </ul>
 *
 * 生命週期:SOURCE < CLASS < RUNTIME
 * 一、通常若是須要在運行時去動態獲取註解信息,用RUNTIME註解
 * 二、要在編譯時進行一些預處理操做,如ButterKnife,用CLASS註解。註解會在class文件中存在,可是在運行時會被丟棄
 * 三、作一些檢查性的操做,如@Override,用SOURCE源碼註解。註解僅存在源碼級別,在編譯的時候丟棄該註解
 */
@Target(ElementType.TYPE) // 該註解做用在類之上
@Retention(RetentionPolicy.CLASS) // 要在編譯時進行一些預處理操做。註解會在class文件中存在
public @interface ARouter {

    // 詳細路由路徑(必填),如:"app/MainActivity"
    String path();

    // 路由組名(選填,若是不填寫,能夠從path中截取)
    String group() default "";
}

2. 自定義ARouterProcessor註解處理器
新建java library工程,工程名爲:compiler。建立ARouterProcessor類繼承AbstractProcessor。

爲了使用APT技術生成代碼,首先要設計咱們想生成的代碼模版,下面的代碼是由APT生成的。

package com.example.modular.shop;

public class ARouter$$ShopActivity {
    public static Class<?> findTargetClass(String path) {
        if (path.equals("/shop/ShopActivity")) {
            return ShopActivity.class;
        }
        return null;
    }
}

在ARouterProcessor類中實現process方法,處理支持的註解,生成咱們想要的代碼。

/**
 *
 * 至關於main函數,開始處理註解
 *  註解處理器的核心方法,處理具體的註解,生成Java文件
 *
 * @param set              支持註釋類型的集合,如:@ARouter註解
 * @param roundEnvironment 當前或是以前的運行環境,能夠經過該對象查找找到的註解
 * @return true 表示後續處理器不會再處理(已經處理完成)
 */
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    // set集合,就是支持的註解集合,如:ARouter註解
    if (set.isEmpty()) return false;

    // 獲取全部被@ARouter註解註釋的元素
    Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(ARouter.class);

    if (elementsAnnotatedWith != null && !elementsAnnotatedWith.isEmpty()) {

        for (Element element : elementsAnnotatedWith) {

            // 經過類節點獲取包節點,(全路徑名,如:com.example.modular.shop)
            String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();

            // 獲取被@ARouter註解的簡單類名
            String simpleName = element.getSimpleName().toString();

            // 注: 包名:com.example.modular.shop 被註解的類名:ShopActivity
            messager.printMessage(Diagnostic.Kind.NOTE, "包名:" + packageName + " 被註解的類名:" + simpleName);

            // 最終生成的類文件名
            String finalClassName = "ARouter$$" + simpleName;
            try {
                // 建立一個新的源文件並返回一個JavaFileObject對象以容許寫入它
                JavaFileObject sourceFile = filer.createSourceFile(packageName + "." + finalClassName);

                // 獲取此文件對象的Writer,開啓寫入功能
                Writer writer = sourceFile.openWriter();

                writer.write("package " + packageName + ";\n");
                writer.write("public class " + finalClassName + " {\n");
                writer.write("public static Class<?> findTargetClass(String path) {\n");

                // 獲取ARouter註解
                ARouter aRouter = element.getAnnotation(ARouter.class);

                writer.write("if (path.equals(\"" + aRouter.path() + "\")) {\n");

                writer.write("return " + simpleName + ".class;\n");

                writer.write("}\n");

                writer.write("return null;\n");

                writer.write("}\n}");

                // 關閉寫入流
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    return true;
}

使用時要在build.gradle文件添加依賴,如購物shop模塊中

dependencies {
    implementation project(path: ':ARouter:annotations')
    annotationProcessor project(path: ':ARouter:compiler')
}

而後在ShopActivity類上添加ARouter註解

@ARouter(path = "/shop/ShopActivity")
public class ShopActivity extends AppCompatActivity {}

最後build -> Make Project,就會生成咱們想要的代碼。

注意:若是出現中文亂碼,在build.gradle中添加以下配置:

// java控制檯輸出中文亂碼
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}

APT + javapoet

javapoet是square公司推出的開源java代碼生成框架,提供java api生成.java源文件。這個框架很是實用,也是咱們習慣的java面向對象OOP語法。能夠很方便的使用它根據註解生成對於的代碼。經過這種自動化生成代碼的方式,可讓咱們用更加簡潔優雅的方式替代繁瑣冗雜的重複工做。

項目主頁及源碼
https://github.com/square/jav...

依賴javapoet庫

dependencies {
    // 幫助咱們經過類調用的方式來生成java代碼
    implementation 'com.squareup:javapoet:1.11.1'
}

javapoet 8個經常使用的類

先來看下javapoet官網提供的一個簡單的例子,生成HelloWorld類

package com.example.helloworld;

public final class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello, JavaPoet!");
  }
}

使用javapoet生成上面這段代碼:

MethodSpec main = MethodSpec.methodBuilder("main")
    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
    .returns(void.class)
    .addParameter(String[].class, "args")
    .addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
    .build();

TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
    .addMethod(main)
    .build();

JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
    .build();

javaFile.writeTo(System.out);

javapoet jar中提供了8個經常使用的類

javapoet字符串格式化規則

$L  字面量,如:"int value=$L", 10
$S  字符串,如:$S, "hello"
$T  類、接口,如:$T, MainActivity
$N  變量,如:user.$N, name

接下來仍是生成ARouter$$ShopActivity

package com.example.modular.shop;

public class ARouter$$ShopActivity {
    public static Class<?> findTargetClass(String path) {
        return path.equals("/shop/ShopActivity") ? ShopActivity.class : null;
    }
}

使用javapoet生成ARouter$$ShopActivity

public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    // 獲取全部被@ARouter註解註釋的元素
    Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(ARouter.class);

    if (elementsAnnotatedWith != null && !elementsAnnotatedWith.isEmpty()) {

        for (Element element : elementsAnnotatedWith) {

            // 經過類節點獲取包節點,(全路徑名,如:com.example.modular.shop)
            String packageName = elementUtils.getPackageOf(element).getQualifiedName().toString();

            // 獲取被@ARouter註解的簡單類名
            String simpleName = element.getSimpleName().toString();

            // 注: 包名:com.example.modular.shop 被註解的類名:ShopActivity
            messager.printMessage(Diagnostic.Kind.NOTE, "包名:" + packageName + " 被註解的類名:" + simpleName);

            // 最終生成的類文件名
            String finalClassName = "ARouter$$" + simpleName;

            ARouter aRouter = element.getAnnotation(ARouter.class);

            ClassName targetClassName = ClassName.get((TypeElement) element);
            // 構建方法體
            MethodSpec findTargetClass = MethodSpec.methodBuilder("findTargetClass")
                    .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
                    .returns(Class.class) // 返回值Class<?>
                    .addParameter(String.class, "path")
                    .addStatement("return path.equals($S) ? $T.class : null", aRouter.path(), targetClassName)
                    .build();

            // 構建類
            TypeSpec finalClass = TypeSpec.classBuilder(finalClassName)
                    .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                    .addMethod(findTargetClass)
                    .build();

            // 生成文件
            JavaFile javaFile = JavaFile.builder(packageName, finalClass)
                    .build();

            try {
                javaFile.writeTo(filer);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    return true;
}

若是個人文章對您有幫助,不妨點個贊鼓勵一下(^_^)

相關文章
相關標籤/搜索