Java注解全解析(三)——编译时注解示例
2016-12-20 17:55
507 查看
1 编译时注解示例
(项目参考:Android利用APT技术在编译期生成代码 )1.1 目标
通过编译时注解实现一个小的依赖注入库,帮助我们简化findViewbyId和setOnClickListener这两个操作的书写,例如:TextView tv1 = (TextView) findViewById(R.id.tv1); findViewById(R.id.tv2).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { } }); findViewById(R.id.tv3).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { } });
可以简化为:
@BindView(R.id.tv1) TextView tv1; @OnClick({R.id.tv2, R.id.tv3}) public void onClick() { }
在开始讨论具体的实现细节之前,首先来看看项目结构和执行流程,对项目整体有个大致的了解,这样在后面讲代码细节的时候才不至于迷失。
1.2 项目结构
项目中有4个module,右边的3个module合在一起就是一个注入库(相当于我们熟知的butterknife库)。
具体来说,此注入库中有3个module:injectiontool是一个android library,负责对外提供API;annotation库是一个java library,其中定义了注解;而compile也是一个java libraray,其中的代码会在编译时被执行,结果是根据注解生成一系列java类,供程序运行时使用。android library 和 java libraray的区别见附录。
app依赖injectiontool;compile和injectiontool均依赖annotation。
1.3 执行流程
大致的执行流程如下:在module app的MainActivity中使用注解
在MainActivity中调用module injectiontool中InjectionTool类的inject()方法
InjectionTool的inject()方法会通过反射拿到
MainActivity$$Injector对象,并调用
MainActivity$$Injector的inject()方法。所有功能都是在
MainActivity$$Injector的inject()方法中实现的
MainActivity$$Injector类是通过在编译时执行module compile中的代码,由module compile生成的。具体来说:module compile会去解析module app中的注解,拿到注解的属性值,再借助javapoet来生成
MainActivity$$Injector类。
本项目中生成的
MainActivity$$Injector代码如下:
public class MainActivity$$Injector implements Injector<MainActivity> { @Override public void inject(final MainActivity host, Object source, Provider provider) { host.mEditText = (EditText)(provider.findView(source, 2131427412)); View.OnClickListener listener; listener = new View.OnClickListener() { @Override public void onClick(View view) { host.onButtonClick(); } } ; provider.findView(source, 2131427413).setOnClickListener(listener); listener = new View.OnClickListener() { @Override public void onClick(View view) { host.onTextClick(); } } ; provider.findView(source, 2131427414).setOnClickListener(listener); } }
1.4 代码实现
1.4.1 定义注解
新建一个module annotation,类型为java library,其中定义了两个注解:@Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface BindView { int value(); } @Retention(RetentionPolicy.CLASS) @Target(ElementType.METHOD) public @interface OnClick { int[] value(); }
@BindView用来注解成员变量,接口一个int为参数;@OnClick用来注解方法,接收一个int数组为参数。
关于注解的相关知识可以参考之前的文章。
1.4.2 编写API
新建一个module injectiontool,类型为android library。此module的主要部分是一个InjectionTool类,该类对外提供了静态方法inject():
public class InjectionTool { private static final ActivityProvider activityProvider = new ActivityProvider(); private static final ViewProvider viewProvider = new ViewProvider(); private static final Map<String, Injector> injectorMap = new HashMap<>(); //在activity中使用 public static void inject(Activity activity) { inject(activity, activity, activityProvider); } //在view中使用 public static void inject(View view) { inject(view, view, viewProvider); } /** * 通过反射拿到XXX$$Injector对象并调用其inject方法 * XXX$$Injector类是在编译期间生成的 * <p> * host:注解所在的类,如MainActivity * source:查找view的地方(activity或view) * provider:主要就是封装了findViewById */ public static void inject(Object host, Object source, Provider provider) { String className = host.getClass().getName(); try { Injector injector = injectorMap.get(className); if (injector == null) { Class<?> clazz = Class.forName(className + "$$Injector"); injector = (Injector) clazz.newInstance(); injectorMap.put(className, injector); } injector.inject(host, source, provider); } catch (Exception e) { throw new RuntimeException("Unable to inject for " + className, e); } } }
上面的代码中,方法
inject(Activity activity)和
inject(View view)最终都会调用到
inject(Object host, Object source, Provider provider),而方法
inject(Object host, Object source, Provider provider)的功能是通过反射拿到
XXX$$Injector对象并调用其inject方法。对于
XXX$$Injector,在上面的“1.3 执行流程”中我们已经见过了,它是注入(inject)功能的真正实现者。
方法的三个参数:
host:注解所在的类,如MainActivity
source:查找view的地方(activity或view)
provider:主要就是封装了findViewById()操作
provider的代码如下:
public interface Provider { Context getContext(Object source); View findView(Object source, int id); } public class ViewProvider implements Provider { @Override public Context getContext(Object source) { return ((View) source).getContext(); } @Override public View findView(Object source, int id) { return ((View) source).findViewById(id); } } public class ActivityProvider implements Provider { @Override public Context getContext(Object source) { return ((Activity) source); } @Override public View findView(Object source, int id) { return ((Activity) source).findViewById(id); } }
依赖
module injectiontool依赖module annotation:
compile project(':annotation')
但是module injectiontool中分明没有用到注解,为什么要依赖module annotation呢?这是因为module app依赖module injectiontool,而module app中会使用注解。
这其实就是一个封装的概念:注入库(项目结构图中右边三个module的结合)的使用者(module app),应该尽量少的知道注入库的实现细节。因为module app依赖module injectiontool,我们只要让module injectiontool依赖module annotation,那么module app就隐式的、间接的依赖了module annotation,这样module app就不用在自己的build.gradle中写上
compile project(':annotation')了。
1.4.3 创建注解处理器
新建一个module compile,类型为java library。上面已经说过,此module中的代码会在编译时被执行,结果是生成
XXX$$Injector类,如
MainActivity$$Injector。而
XXX$$Injector中的代码会在程序运行时被用到(当我们调用InjectionTool的inject方法时,会间接调用到
XXX$$Injector的inject方法)。
那么如何让此module中的代码在编译时被执行呢?这就要借助java的Annotation Processing Tool(APT)和android-apt插件了(APT和android-apt参考附录),核心就是写一个类实现AbstractProcessor接口,这就是我们的注解处理器:
/** * 此类实现了AbstractProcessor接口,接口中的方法都会在编译时被执行 */ //使用Google的auto-service库可以自动生成META-INF/services/javax.annotation.processing.Processor文件 @AutoService(Processor.class) public class MyProcessor extends AbstractProcessor { private Filer mFiler; //文件相关的工具类 private Elements mElementUtils; //元素相关的工具类 private Messager mMessager; //日志相关的工具类 //注解所在的类 //key:全类名 value:AnnotatedClass对象 private Map<String, AnnotatedClass> mAnnotatedClassMap = new HashMap<>(); @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); //拿到几个工具类 mFiler = processingEnv.getFiler(); mElementUtils = processingEnv.getElementUtils(); mMessager = processingEnv.getMessager(); } //指定哪些注解需要被注解处理器处理 @Override public Set<String> getSupportedAnnotationTypes() { Set<String> types = new LinkedHashSet<>(); types.add(BindView.class.getCanonicalName()); types.add(OnClick.class.getCanonicalName()); return types; } //指定使用的 Java 版本,通常返回 SourceVersion.latestSupported() @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } //-------------------------------------------------------------------------------- /** * TypeElement:类元素 * VariableElement:字段元素 * ExcuteableElement:方法元素 */ @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { mAnnotatedClassMap.clear(); //获取被注解的字段元素和方法元素,将其封装为AnnotatedField和AnnotatedMethod对象 //再将AnnotatedField和AnnotatedMethod对象封装到AnnotatedClass对象中 try { processBindView(roundEnv); processOnClick(roundEnv); } catch (IllegalArgumentException e) { error(e.getMessage()); return true;//stop process } //为每个AnnotatedClass对象生成一个XXX$$Injector类,并将其写入到mFiler for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) { try { info("Generating file for %s", annotatedClass.getFullClassName()); annotatedClass.generateInjector().writeTo(mFiler); } catch (IOException e) { error("Generate file failed, reason: %s", e.getMessage()); return true; } } return true; } //1.获得被BindView注解的字段元素,将字段元素封装到AnnotatedField对象中 //2.将AnnotatedField对象封装到AnnotatedClass对象中 private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException { //获得被BindView注解的元素并遍历 for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) { //TODO: 检查字段的修饰符 AnnotatedClass annotatedClass = getAnnotatedClass(element); AnnotatedField field = new AnnotatedField(element); annotatedClass.addField(field); } } //1.获得被OnClick注解的方法元素,将方法元素封装到AnnotatedMethod对象中 //2.将AnnotatedMethod对象封装到AnnotatedClass对象中 private void processOnClick(RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) { AnnotatedClass annotatedClass = getAnnotatedClass(element); AnnotatedMethod method = new AnnotatedMethod(element); annotatedClass.addMethod(method); } } //获得字段元素或方法元素所在的类元素(TypeElement)并封装为AnnotatedClass对象 private AnnotatedClass getAnnotatedClass(Element element) { //获得类元素(TypeElement) TypeElement typeElement = (TypeElement) element.getEnclosingElement(); //封装为AnnotatedClass对象 String fullClassName = typeElement.getQualifiedName().toString(); AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassName); if (annotatedClass == null) { annotatedClass = new AnnotatedClass(typeElement, mElementUtils); mAnnotatedClassMap.put(fullClassName, annotatedClass); } return annotatedClass; } ... }
其中
init(),
getSupportedAnnotationTypes(),
getSupportedSourceVersion()这三个方法的写法是基本固定的:
方法 | 功能 |
---|---|
init(ProcessingEnvironment processingEnv) | 初始化,在此方法中可以拿到一些工具类 |
getSupportedAnnotationTypes() | 指定哪些注解需要被注解处理器处理 |
getSupportedSourceVersion() | 指定使用的 Java 版本 |
通过参数roundEnv的getElementsAnnotatedWith()方法获得被注解的字段和方法,通过字段和方法的getEnclosingElement()方法获得注解所在的类(即使用了注解的类)。在这里,字段、方法和类分别用字段元素(VariableElement)、方法元素(ExecutableElement)和类元素(TypeElement)来表示。为了方便使用,又将字段元素、方法元素、类元素及其相关操作封装到了AnnotatedField、AnnotatedMethod和AnnotatedClass中(实现细节参考项目代码)。
通过AnnotatedClass的generateInjector()方法为每一个使用了注解的类生成一个
XXX$$Injector类。
值得一提的是,在AnnotatedClass的generateInjector()方法中,是通过javapoet这个库来生成
XXX$$Injector类的。javapoet是square公司的一个开源库,专门用于自动生成.java文件,详细使用方法可以参考之后的介绍文章 以及 官方页面。
另外,要让我们的注解处理器正常工作,还需要为它写一些配置信息。这里我们借助了google的auto-service库,只要在注解处理器顶部加上
@AutoService(Processor.class),auto-service就会自动为我们生成需要的配置信息。
依赖
compile project(':annotation') compile 'com.squareup:javapoet:1.7.0' compile 'com.google.auto.service:auto-service:1.0-rc2'
要处理注解,就要用到注解的定义,当然就需要依赖module annotation;而javapoet和auto-service的作用上面已经说过。
1.4.4 使用此注入库
1.让module app依赖module injectiontool在module app的build.gradle中添加:
dependencies { compile project(':injectiontool') }
2.指定用android-apt插件来处理module compile
在Project的build.gradle中引入android-apt插件:
buildscript { dependencies { classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' } }
在module app的build.gradle中指定使用android-apt插件来处理module compile:
apply plugin: 'com.neenbedankt.android-apt' dependencies { apt project(':compile') }
3.接下来就可以在module app中正式使用此注入库了
就是两步:1.使用注解 2.调用InjectionTool.inject()方法
public class MainActivity extends AppCompatActivity { @BindView(R.id.et) EditText mEditText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //注入 InjectionTool.inject(this); } @OnClick(R.id.btn) public void onButtonClick() { Toast.makeText(this, "onButtonClick-" + mEditText.getText().toString(), Toast.LENGTH_SHORT).show(); } @OnClick(R.id.tv) public void onTextClick() { Toast.makeText(this, "onTextClick-" + mEditText.getText().toString(), Toast.LENGTH_SHORT).show(); } }
这篇文章到这里就结束了,以上内容只是自己学习过程中的一点笔记,很多细节都是浅尝辄止(因为感觉自己不大可能会去写一个编译时注解的库),同时也难免会有疏漏甚至理解错误之处,非常期待您的指正,感谢!
完整项目地址
附录
android library 和 java libraray二者的区别是编译时使用的环境和参数不同,结果就是:
在android library中可以调用android api,而java library中不能。
在android library中调用java api会有一些局限,例如javax包下的一些类可能无法import也无法使用,会提示找不到。但也有解决办法,就是在dependencies{}中显式的指定rt.jar的路径,如:
compile files ('C:/Program Files/Java/jdk1.8.0_101/jre/lib/rt.jar')
APT和android-apt插件:
APT(Annotation Processing Tool)是java官方提供的一套工具,用于在编译时处理注解。而android-apt是一个插件,帮助我们在android开发时使用APT。
APT使用注解处理器来处理注解,所有注解处理器都继承了AbstractProcessor,并运行于它自己的JVM中。是的,你没看错,javac启动了一个完整的java虚拟机来运行注解处理器。这意味你可以使用任何你在普通java程序中使用的东西。
APT参考:Annotation-Processing-Tool详解
相关文章推荐
- apue学习第三天——深度解析apue第三版示例程序编译问题
- Android 打造编译时注解解析框架 这只是一个开始
- Java注解教程:自定义注解示例,利用反射进行解析
- Java注解全解析(二)——运行时注解示例
- Java注解教程:自定义注解示例,利用反射进行解析
- html解析利器Html Tidy-附示例与交叉编译方式
- Java注解教程:自定义注解示例,利用反射进行解析
- Android 打造编译时注解解析框架 这只是一个开始
- Java注解教程:自定义注解示例,利用反射进行解析
- Java注解:自定义注解示例,利用反射进行解析
- Android 打造编译时注解解析框架 这只是一个开始
- Android 打造编译时注解解析框架
- Android 打造编译时注解解析框架 这只是一个开始
- Java注解教程:自定义注解示例,利用反射进行解析
- Android 编译时解析注解
- Android 打造编译时注解解析框架 这只是一个开始
- Android 打造编译时注解解析框架 这只是一个开始
- Java注解教程:自定义注解示例,利用反射进行解析
- Android 打造编译时注解解析框架 这只是一个开始
- 编译时注解解析