记录一次AbstractProcessor的使用

需求

之前封装了BaseActivity方法, 里面有两个抽象方法

    protected abstract @LayoutRes int getContentLayoutId();
    /**
     * 创建MVP中的Presenter
     *
     * @return Presenter
     */
    protected abstract T createPresenter();

然后在子Activity中实现这两个方法

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

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

即可完成布局文件的设置和Presenter的创建和关联
现在的目标就是通过注解, 干掉getContentLayoutId和createPresenter这两个方法.只在子类Activity上添加注解即可.

方案1 注解+反射

1. 创建注解类

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

注意,这里的Retention必须是RUNTIME, 否则,反射无法获取到

2.反射

可以分成两步:
①调用Activity#setContentView方法设置布局
②通过反射创建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")
    }
}

结果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

准备

一般而言, 我们需要创建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方法给用户调. 该lib一般是通过反射关联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是合适的.


现在, 创建两个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

①创建MvpHelperProcessor, 继承AbstractProcessor

②配置AbstractProcessor

有两种方式
方式一: main下创建\resources\META-INF\services\javax.annotation.processing.Processor



注意,这里的文件夹和文件名都是固定的, 不可更改.
javax.annotation.processing.Processor文件的内容是

com.houtrry.lib_compiler.MvpHelperProcessor

也就是①里创建的文件的路径
方式二: 使用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文件下下看到自动创建的javax.annotation.processing.Processor文件.如下图:


③ 实现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;
    }  
}

④根据注解生成.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();
        // 根据旧Java类名创建新的Java文件
        String className = typeElement.getQualifiedName().toString().substring(packageName.length() + 1);
        String newClassName = className + "$$$Binding";
        
        //生成构造方法
        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 反射关联

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();
        }
    }
}

简单来说, 就是通过反射执行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文件

发现跟gradle版本有关
5.0之后,需要加上

annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335