記錄一次AbstractProcessor的使用

需求

之前封裝了BaseActivity方法, 里面有兩個抽象方法

    protected abstract @LayoutRes int getContentLayoutId();
    /**
     * 創(chuàng)建MVP中的Presenter
     *
     * @return Presenter
     */
    protected abstract T createPresenter();

然后在子Activity中實現(xiàn)這兩個方法

    @Override
    protected int getContentLayoutId() {
        return R.layout.activity_answering_detail;
    }

    @Override
    protected AnsweringDetailContract.Presenter createPresenter() {
        return new AnsweringDetailPresenter();
    }

即可完成布局文件的設置和Presenter的創(chuàng)建和關聯(lián)
現(xiàn)在的目標就是通過注解, 干掉getContentLayoutId和createPresenter這兩個方法.只在子類Activity上添加注解即可.

方案1 注解+反射

1. 創(chuàng)建注解類

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityBinding {
    int layout();
    Class presenter() default Void.class;
}

注意,這里的Retention必須是RUNTIME, 否則,反射無法獲取到

2.反射

可以分成兩步:
①調(diào)用Activity#setContentView方法設置布局
②通過反射創(chuàng)建Presenter對象,然后賦值給mPresenter

public class MvpReflectHelper {
    public static void bind(Activity activity) {
        if (activity == null) {
            return;
        }
        ActivityBindingReflect annotation = activity.getClass().getAnnotation(ActivityBindingReflect.class);
        activity.setContentView(annotation.layout());
        Class presenter = annotation.presenter();
        try {
            Class cls = Class.forName("com.houtrry.annotationprocessorsamples.BaseActivity");
            Field presenterField = cls.getDeclaredField("mPresenter");
            Object instance = presenter.newInstance();
            presenterField.setAccessible(true);
            presenterField.set(activity, instance);
        } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException | InstantiationException e) {
            e.printStackTrace();
        }
    }
}

3.使用

open class BaseActivity: AppCompatActivity() {

    var mPresenter:Any? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        MvpReflectHelper.bind(this)
        initData()
        initEvent()
    }

    open fun initData() {
    }

    open fun initEvent() {
    }
}

@ActivityBindingReflect(layout = R.layout.activity_main, presenter = MainPresenter::class)
class MainActivity : BaseActivity() {

    override fun initData() {
        super.initData()
        println("===>>>$mPresenter")
    }
}

結(jié)果UI正常展示, mPresenter不為空, 說明運行正常.

2020-05-07 19:22:53.776 16064-16064/com.houtrry.annotationprocessorsamples I/System.out: ===>>>com.houtrry.annotationprocessorsamples.MainPresenter@3ce781f

眾所周知, 反射影響性能( why?), 所以, 我們有了下面的方案.

方案2 注解+AbstractProcessor

準備

一般而言, 我們需要創(chuàng)建3個lib,

  • lib-annotations: 專門用來存放各個注解. 可以是java/Android lib,一般是java lib.
  • lib-complier: 用來聲明自定義AbstractProcessor, 獲取注解生成.java文件的lib. 只能是java lib, Android lib中沒有AbstractProcessor. 該lib需要依賴lib-annotations.
  • lib-mvphelper: 提供類和方法給用戶使用的lib. 比如butterKnife中提供ButterKnife.java和ButterKnife#bind方法給用戶調(diào). 該lib一般是通過反射關聯(lián)lib-complier生成的類. 可以是java/Android lib,一般是Android lib.

簡單說明一下, 為什么得是分成3個lib:
①AbstractProcessor在Android中沒有, 況且, 編譯生成.java的這部分邏輯是在編譯期, 不需要將AbstractProcessor相關的代碼邏輯打包進apk文件中, 所以, 我們首先可以想到, 把AbstractProcessor這部分邏輯給抽取出來, 生成lib-complier
②注解應該放到哪里? 注解在lib-complier和主項目中需要使用, 那是否考慮放到lib-complier中? 在①中我們說到AbstractProcessor的邏輯我們我們不期望打包進apk, 所以, 把注解放進lib-complier是不合適的. 那放到lib-mvphelper合適嗎? 放到lib-mvphelper的問題是, 我們lib-complier需要用到注解, 但是又不需要反射bind這些, 所以, 綜上考慮, 拆成3個lib是合適的.


現(xiàn)在, 創(chuàng)建兩個Java Module(lib-annotations 和 lib-complier), 一個Android Module(lib-mvphelper)
lib-complier依賴lib-annotations,
lib-mvphelper依賴lib-annotations(建議用api, 而不是implementation, 這樣的話, app依賴lib-mvphelper的時候可以不再依賴lib-annotations)
app依賴lib-mvphelper和lib-complier

    implementation project(path: ':lib-mvphelper')
    annotationProcessor project(path: ':lib-compiler')

注意
①lib-compiler用的是annotationProcessor
②lib-mvphelper中依賴lib-annotations用的是api. 如果用的是implementation,那app 下的build.gradle這里還需要添加implementation project(path: ':lib-annotations')

1. lib-annotations

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface ActivityBinding {
    int layout();
    Class presenter();
}

注意,這里的Retention用CLASS即可.

2. lib-compiler

①創(chuàng)建MvpHelperProcessor, 繼承AbstractProcessor

②配置AbstractProcessor

有兩種方式
方式一: main下創(chuàng)建\resources\META-INF\services\javax.annotation.processing.Processor



注意,這里的文件夾和文件名都是固定的, 不可更改.
javax.annotation.processing.Processor文件的內(nèi)容是

com.houtrry.lib_compiler.MvpHelperProcessor

也就是①里創(chuàng)建的文件的路徑
方式二: 使用auto-service
lib-compiler的build.gradle中引入auto-service

    implementation 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'

然后在添加注解@AutoService(Processor.class)在MvpHelperProcessor上(也就是我們繼承了AbstractProcessor的類)

@AutoService(Processor.class)
public class MvpHelperProcessor extends AbstractProcessor {
//...省略
}

編譯成功后, 可以在 lib-compiler的build文件下下看到自動創(chuàng)建的javax.annotation.processing.Processor文件.如下圖:


③ 實現(xiàn)MvpHelperProcessor

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

    private Filer mFiler;
    private Messager mMessager;
    private Elements mElementUtils;

    /**
     * 初始化
     * 在該初始化方法中, 我們可以獲取mFiler/mElementUtils
     * mFiler/mElementUtils是后面生成java文件要用的
     *
     * @param processingEnvironment
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();
        mMessager = processingEnvironment.getMessager();
        mElementUtils = processingEnvironment.getElementUtils();
    }

    /**
     * 此Processor支持的java版本
     *
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * @return 支持的注解類型
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Collections.singleton(ActivityBinding.class.getCanonicalName());
    }

    /**
     * 核心方法
     * 獲取注解信息, 生成.java的具體邏輯
     *
     * @param set
     * @param roundEnvironment
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        System.out.println("================process start==========================="); 
        return false;
    }  
}

④根據(jù)注解生成.java文件

這里, 我們可以先寫下我們期望生成的文件
比如, 簡單點, 生成如下文件

public final class JavaActivity$$$Binding {
  public JavaActivity$$$Binding(JavaActivity target) {
    target.setContentView(R.layout.activity_annotation_processor);
    target.mPresenter=new MainPresenter();
  }
}

這里, 生成java文件我們使用javapoet

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        System.out.println("================process start===========================");
        try {
            Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(ActivityBinding.class);
            for (Element element : elements) {
                if (element.getKind() != ElementKind.CLASS) {
                    continue;
                }
                mMessager.printMessage(Diagnostic.Kind.NOTE, "===element: " + element);
                createMvpHelperClass((TypeElement)element);
            }
        } catch (Exception e) {
            e.printStackTrace();
            mMessager.printMessage(Diagnostic.Kind.ERROR, "===e: " + e);
        }
        System.out.println("================process end===========================");
        return false;
    }

    private void createMvpHelperClass(TypeElement typeElement) {
        ActivityBinding annotation = typeElement.getAnnotation(ActivityBinding.class);
        if (annotation == null) {
            return;
        }
        int layoutId = annotation.layout();

        // 獲取包名
        String packageName = mElementUtils.getPackageOf(typeElement).getQualifiedName().toString();
        // 根據(jù)舊Java類名創(chuàng)建新的Java文件
        String className = typeElement.getQualifiedName().toString().substring(packageName.length() + 1);
        String newClassName = className + "$$$Binding";
        
        //生成構(gòu)造方法
        ClassName presenterClassName = getClsName(typeElement);
        if (presenterClassName == null) {
            return;
        }
        MethodSpec.Builder methodBuilder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(ClassName.bestGuess(className), "target")
                .addStatement("target.setContentView($L)", layoutId)
                .addStatement("target.mPresenter=new $T()", presenterClassName);

        //生成類信息
        TypeSpec typeBuilder = TypeSpec.classBuilder(newClassName)
                .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                .addMethod(methodBuilder.build())
                .build();

        //輸出.java文件
        JavaFile javaFile = JavaFile.builder(packageName, typeBuilder)
                .addFileComment("Generated code from Mvp Helper. Do not modify!")
                .build();
        try {
            javaFile.writeTo(mFiler);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private ClassName getClsName(TypeElement typeElement) {
        ActivityBinding annotation = typeElement.getAnnotation(ActivityBinding.class);
        if (annotation == null) {
            return null;
        }
        ClassName className = null;
        try {
            Class presenter = annotation.presenter();
            className = ClassName.get(presenter);
        } catch (MirroredTypeException e) {
            e.printStackTrace();
            //捕捉MirroredTypeException異常
            //在該異常中, 通過異常獲取TypeMirror
            //通過TypeMirror獲取TypeName
            TypeMirror typeMirror = e.getTypeMirror();
            if (typeMirror != null) {
                TypeName typeName = ClassName.get(typeMirror);
                if (typeName instanceof ClassName) {
                    className = (ClassName) typeName;
                }
            }
        }
        return className;
    }

javapoet可以參考JavaPoet的基本使用以及javapoet的javapoet github
process方法中的Element可以參考
make project成功后, 我們就可以在項目中看到生成的.java文件了

3. lib-mvphelper 反射關聯(lián)

public class MvpHelper {
    public static void bind(Activity activity) {
        try {
            Class cls = Class.forName(activity.getComponentName() + "$$$Binding");
            Constructor declaredConstructor = cls.getDeclaredConstructor(activity.getClass());
            System.out.println("bind=>declaredConstructor: "+declaredConstructor);
            Object o = declaredConstructor.newInstance(activity);
            System.out.println("bind=>o: "+o);
        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

簡單來說, 就是通過反射執(zhí)行new JavaActivity$$$Binding(activity)方法.

public class MvpHelper {
    public static void bind(@NonNull Activity activity) {
        try {
            Class cls = Class.forName(activity.getClass().getCanonicalName() + "$$$Binding");
            Constructor declaredConstructor = cls.getDeclaredConstructor(activity.getClass());
            System.out.println("bind=>declaredConstructor: "+declaredConstructor);
            Object o = declaredConstructor.newInstance(activity);
            System.out.println("bind=>o: "+o);
        } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

至于為啥還是反射, 不是說反射影響性能的嗎? 因為這里只需要反射一次, 而上面的方案1至少一次, 一般都是很多次.

遇到的問題

1.

警告: 來自注釋處理程序 'org.gradle.api.internal.tasks.compile.processing.TimeTrackingProcessor' 的受支持 source 版本 'RELEASE_6' 低于 -source '1.8'

分析: 如果不重寫getSupportedSourceVersion, 則其父類方法AbstractProcessor#getSupportedSourceVersion方法中默認返回SourceVersion.RELEASE_6.

    public SourceVersion getSupportedSourceVersion() {
        SupportedSourceVersion var1 = (SupportedSourceVersion)this.getClass().getAnnotation(SupportedSourceVersion.class);
        SourceVersion var2 = null;
        if (var1 == null) {
            var2 = SourceVersion.RELEASE_6;
            if (this.isInitialized()) {
                this.processingEnv.getMessager().printMessage(Kind.WARNING, "No SupportedSourceVersion annotation found on " + this.getClass().getName() + ", returning " + var2 + ".");
            }
        } else {
            var2 = var1.value();
        }

        return var2;
    }

解決辦法: 重寫getSupportedSourceVersion,并返回SourceVersion.latestSupported

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

2.

javax.lang.model.type.MirroredTypeException: Attempt to access Class object for TypeMirror com.het.soildetector.mvp.presenter.AnsweringListPresenter
    at com.sun.tools.javac.model.AnnotationProxyMaker$MirroredTypeExceptionProxy.generateException(AnnotationProxyMaker.java:308)
    at sun.reflect.annotation.AnnotationInvocationHandler.invoke(AnnotationInvocationHandler.java:84)
    at com.sun.proxy.$Proxy152.presenter(Unknown Source)
    at com.het.lib_mvphelper_compiler.MvpHelperProcessor.getClsName(MvpHelperProcessor.java:143)
    at com.het.lib_mvphelper_compiler.MvpHelperProcessor.createMvpHelperClass(MvpHelperProcessor.java:113)
    at com.het.lib_mvphelper_compiler.MvpHelperProcessor.process(MvpHelperProcessor.java:74)
    at org.gradle.api.internal.tasks.compile.processing.DelegatingProcessor.process(DelegatingProcessor.java:62)
    at org.gradle.api.internal.tasks.compile.processing.NonIncrementalProcessor.process(NonIncrementalProcessor.java:45)
    at org.gradle.api.internal.tasks.compile.processing.DelegatingProcessor.process(DelegatingProcessor.java:62)
    at org.gradle.api.internal.tasks.compile.processing.TimeTrackingProcessor.access$401(TimeTrackingProcessor.java:37)
    //...省略
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.executeNextNode(DefaultPlanExecutor.java:193)
    at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:129)
    at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
    at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
    at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
    at java.lang.Thread.run(Thread.java:748)

分析: 通過注解獲取注解中的Class值時拋出MirroredTypeException異常.
參考 # Java 6 annotation processing — getting a class from an annotation
Getting Class values from Annotations in an AnnotationProcessor
解決方法如下

    private ClassName getClsName(TypeElement typeElement) {
        ActivityBinding annotation = typeElement.getAnnotation(ActivityBinding.class);
        if (annotation == null) {
            return null;
        }
        ClassName className = null;
        try {
            Class presenter = annotation.presenter();
            className = ClassName.get(presenter);
        } catch (MirroredTypeException  e) {
            e.printStackTrace();
            //捕捉MirroredTypeException異常
            //在該異常中, 通過異常獲取TypeMirror
            //通過TypeMirror獲取TypeName
            TypeMirror typeMirror = e.getTypeMirror();
            if (typeMirror != null) {
                TypeName typeName = ClassName.get(typeMirror);
                if (typeName instanceof ClassName) {
                    className = (ClassName) typeName;
                }
            }
        }
        return className;
    }

3. 使用auto-service沒有生成META-INF文件

發(fā)現(xiàn)跟gradle版本有關
5.0之后,需要加上

annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末灭袁,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子摘昌,更是在濱河造成了極大的恐慌况芒,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,816評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡戒洼,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評論 3 385
  • 文/潘曉璐 我一進店門允华,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人寥掐,你說我怎么就攤上這事靴寂。” “怎么了召耘?”我有些...
    開封第一講書人閱讀 158,300評論 0 348
  • 文/不壞的土叔 我叫張陵百炬,是天一觀的道長。 經(jīng)常有香客問我污它,道長剖踊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,780評論 1 285
  • 正文 為了忘掉前任衫贬,我火速辦了婚禮德澈,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘固惯。我一直安慰自己梆造,他們只是感情好,可當我...
    茶點故事閱讀 65,890評論 6 385
  • 文/花漫 我一把揭開白布葬毫。 她就那樣靜靜地躺著镇辉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪贴捡。 梳的紋絲不亂的頭發(fā)上忽肛,一...
    開封第一講書人閱讀 50,084評論 1 291
  • 那天,我揣著相機與錄音烂斋,去河邊找鬼屹逛。 笑死础废,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的煎源。 我是一名探鬼主播色迂,決...
    沈念sama閱讀 39,151評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼手销!你這毒婦竟也來了歇僧?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,912評論 0 268
  • 序言:老撾萬榮一對情侶失蹤诈悍,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后侥钳,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,355評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡舷夺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,666評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了给猾。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,809評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡颂跨,死狀恐怖敢伸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情恒削,我是刑警寧澤池颈,帶...
    沈念sama閱讀 34,504評論 4 334
  • 正文 年R本政府宣布,位于F島的核電站钓丰,受9級特大地震影響躯砰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜携丁,卻給世界環(huán)境...
    茶點故事閱讀 40,150評論 3 317
  • 文/蒙蒙 一弃揽、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧则北,春花似錦矿微、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至快骗,卻和暖如春娜庇,著一層夾襖步出監(jiān)牢的瞬間塔次,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評論 1 267
  • 我被黑心中介騙來泰國打工名秀, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留励负,地道東北人。 一個月前我還...
    沈念sama閱讀 46,628評論 2 362
  • 正文 我出身青樓匕得,卻偏偏與公主長得像继榆,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子汁掠,可洞房花燭夜當晚...
    茶點故事閱讀 43,724評論 2 351