前言
说起注解框架,不得不说的是 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 。。。
你看,非常清晰,链式编程一目了然,代码本身即是注释:
用了一些操作符:
- 先获取所有带 @GET 的元素
- filter: 过滤,留下符合的(@GET 标注在函数上,并且该函数定义在 interface 里)
- groupBy:分组,method 按照所在 interface 分组,得到 HashMap< interface, List< method >>。这个操作符 nb 吧,本来我用 for 循环写了一大坨,现在一行搞定。。。
- map: 转换,把分组后的 HashMap 数据赋给 ClassBuilder,委托它来写“类代码”。。。
- 还是 map: 把 ClassBuilder 委托给 FileBuilder,看名字也知道它是用来写“文件”的。。。
- 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);
}
});
}