手撸注解框架 —— 仿 Retrofit

2 篇文章 0 订阅
2 篇文章 0 订阅

前言


说起注解框架,不得不说的是 ButterKnife,Retrofit,还有 Dagger。想来全是 Square 公司的作品,不由得心生膜拜。。。最近一直在用 ButterKnife,看了下源码也有一些感悟。人家大神写代码就是高大上,就为了省一行代码 findViewById(),硬生生撸了一个库出来。。。

以前没感觉,现在感觉注解真 TM 好用。俺也要用注解省代码!!!


项目地址

项目完整代码, 有福利哦,绅士们可要把持住啊~
https://github.com/fashare2015/injector

准备工作


其实解析注解有两种方式,一种是编译时、一种是运行时。在手机性能优先的前提下,运行时注解和反射都是需要避免的。所以我们当然采取编译时注解啦。

至于编程语言,这几天看了下 kotlin,尝尝鲜嘛,就决定是它了。
kotlin 依赖配置:

// 在你的 app 或 lib_module 下的 gradle 里加入:
// kotlin module
dependencies {
    // ...
    // kotlin
    compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}

apply plugin: 'kotlin'
buildscript {
    ext.kotlin_version = "1.0.0-rc-1036"
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
repositories {
    mavenCentral()
}
sourceSets {
    main.java.srcDirs += 'src/main/kotlin'
}

之后可能还得在 Settings -> Plugins 里下个插件: kotlin
详细介绍请看:为什么说Kotlin值得一试

吐槽:java8 的 Stream 虽说还行,然而 AS 对 lambda 的支持是在太烂了。没有 lambda 写个 J8(java8) 啊!!!真不是广告,Kotlin 一生推啊!!!

需求


好吧,扯远了。回到正题,话说我们要弄个注解框架,先给个需求呗。
用过 Retrofit 没?
以下例子来自:快速Android开发系列网络篇之Retrofit

// 请求接口:
public interface GitHubService {
  @GET("/users/{user}/repos") // 1
  List<Repo> listRepos(@Path("user") String user);
}

// 调用:
RestAdapter restAdapter = new RestAdapter.Builder()
    .setEndpoint("https://api.github.com")
    .build();

GitHubService service = restAdapter.create(GitHubService.class); // 2
List<Repo> repos = service.listRepos("octocat");

看1处有一个正常的接口,关注到 @GET 用于标识 url 地址。2处直接就 new 了一个 GitHubService 出来,我擦,我怎么找不到实现体呢。
嘿嘿,神奇吧,没错,需求就是它了。

总结一下:
1.给定一个注解 @GET,和一个带有 @GET的接口:

@Retention(CLASS) @Target(METHOD)
public @interface Get{
    String url();      // url 地址
    Class<?> clazz();  // 返回的数据类型
}

// 通过 Apis.URL 来加载图片
interface ObservableProvider {
    // 顺带复习一下 rxjava ...
    @Get(url = Apis.URL, clazz = Bitmap.class)
    Observable<Bitmap> getImageObservable();
}  

2.自动生成该接口的实现体:
直接看最终效果吧,DUANG~~~

// 自动生成的 $$Impl 后缀的实现类
class ObservableProvider$$Impl implements ObservableProvider {
    ObservableProvider$$Impl() {
    }

    public Observable<Bitmap> getImageObservable() {
        return ObservableUtil.newInstance(new OnLoadData() {
            public Bitmap loadData() {
                return HttpUtils.getNetWorkBitmap("http://www.jdlingyu.moe/wp-content/uploads/2016/02/2016-09-09_20-51-52.jpg");
            }
        });
    }
}

3.像 retrofit 示例 2 处那样调用 ObservableProvider

// 用某种方式(反射)new 出我们的实现类 ObservableProvider$$Impl,
// 然后便可以调用 getImageObservable()
InjectFactory.newObservablesImpl(ObservableProvider.class)
        .getImageObservable()
        .xxx()
        .subcribe(...);

简而言之,我们的需求就是偷懒,不想写 ObservableProvider 的实现体。

实现


new 出实现类

先看需求 3 吧,假装我们已经有了一个实现体 Impl 了。我们怎么来 new 对象呢,本质上我们完全可以这么做:

new Impl()
        .getImageObservable()
        .xxx()
        .subcribe(...);

不行啊, Impl 还没生成,这样编译都过不了吧。。。
还好,我们还可以用反射来 new 对象。

class InjectFactory {
    companion object {  // 1 static
        var INJECT_POSTFIX = "$\$Impl"  // 实现类的后缀

        fun <T> newObservablesImpl(type: Class<T>): T? {
            val name = type.canonicalName + INJECT_POSTFIX
            var obj: T? = null
            try {
                obj = Class.forName(name).newInstance() as T // 2
            } catch (e: ClassNotFoundException) {
            } catch (e: IllegalAccessException) {
            } catch (e: InstantiationException) {
            }
            return obj
        }
    }
}

我们搞了一个工厂方法 newObservablesImpl(),在 2 处通过类名+”$$Impl”来 new 实现类。这意味着我们后面生成类的时候,也得按照这个起名。

1 处 companion object {…}代码块标示内部的成员全是 static 的。坑啊,是因为 kotlin 压根就没有 static 关键字。。。 在 java 这边调用的时候稍有不同:InjectFactory.Companion.newObservablesImpl(ObservableProvider.class)

生成实现类

编译时注解不同于运行时,此时的解析还在 .class 生成之前,这意味着我们将失去反射这个重要工具。没有反射玩个J8啊,我们咋获取注解哩?此时我们需要借助这个核心类 com.example.processor.AbstractProcessor

解析入口 GenerateJavaFileProcessor.java

相当于 jvm 编译时给我们留的口,我们继承它来访问相关注解:GenerateJavaFileProcessor.java

@SupportedAnnotationTypes("com.example.Get") // 1 必须, 否则找不到注解, 等价于重写 getSupportedAnnotationTypes()
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class GenerateJavaFileProcessor extends AbstractProcessor {
    private Filer mOutputFiler;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mOutputFiler = processingEnv.getFiler(); // 2 输出路径
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        ProcessorUtil.Companion.writeTo(roundEnv, mOutputFiler); // 3 解析,写文件
        return true;
    }
}

1 处定义了需要解析的注解com.example.Get,当时忘记了调了半天。。。
2 处指定一个输出文件,也不用我们配置,最后会生成在app/build/intermediates/classes/debug+/原接口路径 下边
这里写图片描述
3 处 解析,写文件

解析流程 ProcessorUtil.kt

我们接着看 ProcessorUtil.kt:
它主要负责解析注解。写文件的任务代理给一系列的 Builder, 可以先不用管。

class ProcessorUtil {
    companion object {  // static
        protected val TAG = "ProcessorUtil: "
        val CLASS_GET: Class<Get> = Get::class.java

        /**
         * 向 outputFiler 写入代码
         * @param roundEnv
        * @param outputFiler
         */
        fun writeTo(roundEnv: RoundEnvironment, outputFiler: Filer) {
            println(TAG + "------- begin write -------")
            println("\n\n")

            // 1 获取所有标有 @Get 的项: Set< Element >
            roundEnv.getElementsAnnotatedWith(CLASS_GET)
                    // 2 过滤条件: element 必须是 method, 且 element 定义在 interface 中
                    .filter (fun (it: Element): Boolean{
                        var valid = it is ExecutableElement
                                && ReflectUtil.isInterface(it.enclosingElement.asType())
                        if(!valid){
                            println("   Ignore ${it.enclosingElement.simpleName}.${it.simpleName}() !!! :\n" +
                                    "       Annotated element [ ${it.simpleName}() ] must be a method, \n" +
                                    "       and it's holder [ ${it.enclosingElement.simpleName} ] must be an interface !!!")
                        }
                        return valid
                    })
                    // 3 收集: 按照 method 所在 class 分组
                    .groupBy { it.enclosingElement }        // HashMap<class, List< method>>
                    // 4 转换: HashMap<class, List< method>> => List< ClassBuilder>
                    .map {
                        ClassBuilder(//
                                it.key as TypeElement, // Element => TypeElement
                                it.value.map { MethodBuilder(it as ExecutableElement) } // List< Element> => List< MethodBuilder>
                        )
                    }
                    // 5 转换: List< ClassBuilder> => List< FileBuilder>
                    .map { FileBuilder(it) }
                    // 6 写入: outputFiler
                    .forEach { it.build().writeTo(outputFiler) }

            println("\n\n")
            println(TAG + "------- end write -------")
        }
    }
}

非常激动,本来想用 java8 的 Stream 写的,这种数据变换、集合操作,用函数式来实现是在太优雅了,光是想想就高潮了。。。然并卵,垃圾 AS 还我 java8,最后还是投奔了 kotlin 。。。

你看,非常清晰,链式编程一目了然,代码本身即是注释:
用了一些操作符:

  1. 先获取所有带 @GET 的元素
  2. filter: 过滤,留下符合的(@GET 标注在函数上,并且该函数定义在 interface 里)
  3. groupBy:分组,method 按照所在 interface 分组,得到 HashMap< interface, List< method >>。这个操作符 nb 吧,本来我用 for 循环写了一大坨,现在一行搞定。。。
  4. map: 转换,把分组后的 HashMap 数据赋给 ClassBuilder,委托它来写“类代码”。。。
  5. 还是 map: 把 ClassBuilder 委托给 FileBuilder,看名字也知道它是用来写“文件”的。。。
  6. forEach: 真正的开始写文件,for 循环,每个 interface 对应一个实现类

文件生成:接口 Builder.kt && 代码生成器 javapoet

/**
 * Created by apple on 16-10-11.
 * 用于 file, class, method ...各级别相关配置的分层
 */
interface Builder<R> {
    fun build(): R
}

你大概注意到有好几个Builder,他们用来配置代码生成模式。为了分层搞了一个接口,细化各自的职责: FileBuilder -> ClassBuilder -> MethodBuilder,从左到右有着委托关系。

// file: 完成 javapoet 工具中 JavaFile.build()
class FileBuilder(private val mClazzBuilder: ClassBuilder): Builder<JavaFile> {

    override fun build(): JavaFile {
        return JavaFile.builder(
                mClazzBuilder.typeElement
                        .let { it.enclosingElement as PackageElement }  // 获取整个 PackageElement
                        .let { it.qualifiedName.toString() },           // 获取 packageName
                mClazzBuilder.build()    // 递归配置 mClazzBuilder
        ).build()
    }
}

// class: 完成 javapoet 工具中 TypeSpec.build()
class ClassBuilder @JvmOverloads constructor(
        val typeElement: TypeElement,
        private val mMethodBuilderList: List<MethodBuilder> = ArrayList<MethodBuilder>()
) : Builder<TypeSpec> {
    protected val TAG = this.javaClass.simpleName + ": "

    override fun build(): TypeSpec {
        println(TAG + TypeName.get(typeElement.asType()))

        return TypeSpec.classBuilder(typeElement.simpleName.toString() + InjectFactory.INJECT_POSTFIX) // 指定实现类的名字: InterfaceA$$Impl
                .addModifiers(*typeElement.modifiers
                        .filter { it != Modifier.ABSTRACT }             // 实现类不加 abstract 关键字
                        .toTypedArray())
                .addSuperinterface(TypeName.get(typeElement.asType()))  // 使得生成的 InterfaceA$$Impl 类 implements InterfaceA
                .addMethods(
                        // map: List<MethodBuilder> => List<MethodSpec>
                        mMethodBuilderList.map { it.build() }   // 递归配置 MethodBuilder
                ).build()
    }
}

// method: 完成 javapoet 工具中 MethodSpec.build()
class MethodBuilder(executableElement: ExecutableElement) : Builder<MethodSpec> {
    protected val TAG = this.javaClass.simpleName + ": "

    private val mReturnType: TypeMirror
    private val mArguments: List<VariableElement>
    private val mMethodName: String
    private val mModifiers: Set<Modifier>

    private val mUrl: String
    private val mClazz: TypeMirror?

    init {
        this.mReturnType = executableElement.returnType
        this.mArguments = executableElement.parameters
        this.mMethodName = executableElement.simpleName.toString()
        this.mModifiers = executableElement.modifiers

        val annotationGet = executableElement.getAnnotation(Get::class.java)
        this.mUrl = annotationGet.url
        this.mClazz = ReflectUtil.getTypeMirror(annotationGet)
    }

    override fun build(): MethodSpec {
        println(TAG + mMethodName)

        return MethodSpec.methodBuilder(mMethodName)
                .addAnnotation(Override::class.java)
                .addModifiers(mModifiers.filter { it != Modifier.ABSTRACT })    // 实现方法不加 abstract 关键字
                .returns(TypeName.get(mReturnType))
                .addParameters(convertToParameterSpec(mArguments))
                .addStatement(
                        "return \$T.newInstance(new \$T<\$T>() {\n" +
                        "   @\$T\n" +
                        "   public \$T loadData() {\n" +
                        "       return HttpUtils.getNetWorkBitmap(\$S);\n" +
                        "   }\n" +
                        "})",
                        ObservableUtil::class.java, ObservableUtil.OnLoadData::class.java, mClazz,
                        Override::class.java,
                        mClazz,
                        mUrl)
                .build()
    }

    private fun convertToParameterSpec(arguments: List<VariableElement>): Iterable<ParameterSpec> {
        return arguments.map{
                ParameterSpec.builder(
                        TypeName.get(it.asType()),  // TypeMirror => TypeName
                        it.simpleName.toString(),
                        *it.modifiers.toTypedArray()
                ).build()
        }
    }
}

这里用的是 javapoet 这个代码生成工具,gradle 依赖为
compile 'com.squareup:javapoet:1.7.0'
其中,三个 Builder 分别完成了 file, class, method 三个级别的配置,然后串在一起。至于怎么配置,自己查 javapoet 的 api 把~
然后,我们看 MethodBuilder.build() 里边的 .addStatement(),是不是看到最终代码的雏形了。没图说个 java8, 看,生成的文件!!!, 虽然格式有点丑陋。
这里写图片描述

细节补充 Element 和 TypeMirror

关于前面有些细节没有解释,Element 和 TypeMirror。
此前说到,这里还没有完全编译出 .class 文件,我们这样 annotationGet.clazz访问 @Get里边的 clazz 域是有问题的。此时根本没有编译出 class 文件啊,它肯定会任性的抛出异常。。。

因此,这里有相应的类来代替反射那一套东西:

Element: 代表结构化的 java 文本,此时它未编译,我们不能再以Class, Mothod等反射的角度看待它。它充其量只是一个结构化的 xml 或者 json 之类的语法树。

  • PackageElement: 对应 import xx.xx.package 代码块
  • TypeElement: 对应 class{ … } 代码块
  • ExecutableElement:对应 void foo(…){ … } 代码块

TypeMirror 对应 Class 类型

这里写图片描述
图来自:Java注解处理器使用详解

最后的最后,贴上 demo 中调用的代码,很简单,点击按钮,拉取图片,设置到 mImageView 上。github 地址在开头,绅士们自取哦~

    @Override
    public void onClick(View v) {
        loadImage();
    }

    private void loadImage() {
//        new ObservableProvider()
        InjectFactory.Companion.newObservablesImpl(ObservableProvider.class)
                .getImageObservable()
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<Bitmap>() {
                    @Override
                    public void call(Bitmap bitmap) {
                        mImageView.setImageBitmap(bitmap);
                    }
                });
    }
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值