您的位置:首页 > 编程语言 > Java开发

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 版本
然后就是最重要的process方法,在此方法中:

通过参数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详解
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: