1 前言
我们知道,在日常开发中我们常用的两种注解是运行时注解和编译时注解,运行时注解是通过反射来实现注解处理器的,对性能稍微有一点损耗,而编译时注解是在程序编译期间生成相应的代理类,替我们完成某些功能。今天我们来讲解一下编译时注解以及写一个小例子,以便加深对编译时注解的理解。
2 编译时注解
编译时注解(RetentionPolicy.CLASS),指@Retention(RetentionPolicy.CLASS)作用域class字节码上,生命周期只有在编译器间有效。编译时注解注解处理器的实现主要依赖于AbstractProcessor来实现,这个类是在javax.annotation.processing包中,同时为了我们自己生成java源文件方便,我们还需要引入一些第三方库,主要包括
javapoet 用于生成java源文件,可参考https://github.com/square/javapoet
auto-service 主要用于生成一些辅助信息,例如META-INF/services 一些信息等
编译时注解的核心就是实现AbstractProcessor的process()方法,一般来说主要有以下两个步骤
1 搜集信息,包括被注解的类的类信息,方法,字段等信息,还有注解的值
2 生成对应的java源代码,主要根据上一步的信息,生成响应的代码
下面我们来实际的写一个编译时注解的例子
3 编译时注解例子
假设我们写这样一个类注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Bind {
int value();
}
这个Bind注解只能作用与类上面,一般我们作用于某个类上面了,我们做什么工作呢,我们新建一个类,这个类的内容大体如下:
public class XXX$$value {
public int sayHello(int n) {
return value;
}
}
其中,XXX是被注解的类名,value是注解的值。
看起来很简单,对吧?下面我们就开始吧。
1 新建两个java model,注意不是Android library model
model的gradle配置如下
apply plugin: 'java'
为什么要新建两个module呢,
(1) 是因为后面再引用的时候,两者有些不一样,可以看一下
api project(':ioc-annotation')
annotationProcessor project(':ioc-compiler')
(2) 因为两者的引用方式不同,导致最好把注解处理器和注解分开,这样我们也能更好的解耦
注意,ioc-compiler需要引用以下两个开源库
apply plugin: 'java'
//解决编译中文乱码问题
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.google.auto.service:auto-service:1.0-rc4'
compile 'com.squareup:javapoet:1.7.0'
compile project(':ioc-annotation')
}
sourceCompatibility = "1.8"
targetCompatibility = "1.8"
2 新建Bind注解,指定注解类型为class
/**
* @author Created by qiyei2015 on 2018/4/14.
* @version: 1.0
* @email: 1273482124@qq.com
* @description: Bind注解 指明注解作用域为类
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Bind {
int value();
}
这一步挺好理解的。
3 新建BindProcessor继承与AbstractProcessor并实现其process方法
/**
* @author Created by qiyei2015 on 2018/4/15.
* @version: 1.0
* @email: 1273482124@qq.com
* @description: Bind注解处理器
*/
@AutoService(Processor.class)
public class BindProcessor extends AbstractProcessor{
/**
* java源文件操作相关类,主要用于生成java源文件
*/
private Filer mFiler;
/**
* 注解类型工具类,主要用于后续生成java源文件使用
* 类为TypeElement,变量为VariableElement,方法为ExecuteableElement
*/
private Elements mElementsUtils;
/**
* 日志打印,类似于log,可用于输出错误信息
*/
private Messager mMessager;
private static final ClassName sClassName = ClassName.get("com.qiyei.ioc.api", "Test");
/**
* 初始化,主要用于初始化各个变量
* @param processingEnv
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mElementsUtils = processingEnv.getElementUtils();
mMessager = processingEnv.getMessager();
}
/**
* 支持的注解类型
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> typeSet = new LinkedHashSet<>();
typeSet.add(Bind.class.getCanonicalName());
return typeSet;
}
/**
* 支持的版本
* @return
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
/**
*
* 1.搜集信息
* 2.生成java源文件
* @param annotations
* @param roundEnv
* @return
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (!annotations.isEmpty()){
//获取Bind注解类型的元素,这里是类类型TypeElement
Set<? extends Element> bindElement = roundEnv.getElementsAnnotatedWith(Bind.class);
try {
generateCode(bindElement);
} catch (IOException e) {
e.printStackTrace();
}
return true;
}
return false;
}
/**
*
* @param elements
*/
private void generateCode(Set<? extends Element> elements) throws IOException{
for (Element element : elements){
//由于是在类上注解,那么获取TypeElement
TypeElement typeElement = (TypeElement) element;
//获取全限定类名
String className = typeElement.getQualifiedName().toString();
mMessager.printMessage(Diagnostic.Kind.WARNING,"className:" + className);
//获取包路径
PackageElement packageElement = mElementsUtils.getPackageOf(typeElement);
String packageName = packageElement.getQualifiedName().toString();
mMessager.printMessage(Diagnostic.Kind.WARNING,"packageName:" + packageName);
//获取用于生成的类名
className = getClassName(typeElement,packageName);
//获取注解值
Bind bindAnnotation = typeElement.getAnnotation(Bind.class);
int value = bindAnnotation.value();
System.out.println("value:" + value);
//生成方法
MethodSpec.Builder methodBuilder = MethodSpec
.methodBuilder("sayHello")
.addModifiers(Modifier.PUBLIC)
.addParameter(TypeName.INT,"n")
.returns(TypeName.INT);
//$L表示字面量 $T表示类型
methodBuilder.addStatement("return $L",value);
//生成的类
TypeSpec type = TypeSpec
.classBuilder(className + "$$" + value)
.addModifiers(Modifier.PUBLIC)
.addMethod(methodBuilder.build())
.build();
//创建javaFile文件对象
JavaFile javaFile = JavaFile.builder(packageName,type).build();
//写入源文件
javaFile.writeTo(mFiler);
}
}
/**
* 根据type和package获取类名
* @param type
* @param packageName
* @return
*/
private static String getClassName(TypeElement type, String packageName) {
int packageLen = packageName.length() + 1;
return type.getQualifiedName().toString().substring(packageLen)
.replace('.', '$');
}
}
其他方法暂时不做讲解,逻辑都很简单,主要讲解generateCode(Set
//获取全限定类名
String className = typeElement.getQualifiedName().toString();
//获取包路径
PackageElement packageElement = mElementsUtils.getPackageOf(typeElement);
String packageName = packageElement.getQualifiedName().toString();
(2) 构造如上所示的java源文件
//生成方法
MethodSpec.Builder methodBuilder = MethodSpec
.methodBuilder("sayHello")
.addModifiers(Modifier.PUBLIC)
.addParameter(TypeName.INT,"n")
.returns(TypeName.INT);
//$L表示字面量 $T表示类型
methodBuilder.addStatement("return $L",value);
//生成的类
TypeSpec type = TypeSpec
.classBuilder(className + "$$" + value)
.addModifiers(Modifier.PUBLIC)
.addMethod(methodBuilder.build())
.build();
这里主要是用到javapoet,关于为什么用这个开源库,首先是用法比较简单,其次很多开源框架都在用,包括ButterKnife等。关于生成java代码请一定参考https://github.com/square/javapoet 里面有很详细的说明。
(3) 将java源码写入文件
//创建javaFile文件对象
JavaFile javaFile = JavaFile.builder(packageName,type).build();
//写入源文件
javaFile.writeTo(mFiler);
(4) 使用Bind注解,然后编译生成代码
我们在我们的一个Activity上使用Bind注解,如下:
/**
* @author Created by qiyei2015 on 2017/8/28.
* @version: 1.0
* @email: 1273482124@qq.com
* @description:
*/
@Bind(10)
public class MainActivity extends BaseSkinActivity {
private RecyclerView mRecyclerView;
private static final int MY_PERMISSIONS_REQUEST_WRITE_STORE = 1;
/**
* ViewModel
*/
private MainMenuViewModel mMenuViewModel;
private MainMenuAdapter mMenuAdapter;
/**
* 标题栏
*/
private CommonTitleBar mTitleBar = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// PermissionManager.requestAllDangerousPermission(this);
initData();
initView();
LogManager.i(TAG,"onCreate");
}
.......
}
然后编译,生成的代码如下:
代码路径:\build\generated\source\apt\baidu\debug\com\qiyei\appdemo\ui\activity
package com.qiyei.appdemo.ui.activity;
public class MainActivity$$10 {
public int sayHello(int n) {
return 10;
}
}
可以看到,符合我们的预期,这样我们一个简单的编译时注解就已经完成了
注:虽然我们的代码如约生成了,但是我们并没有用生成的代码,所以我们还应该写一个api来使用生成的java源代码文件,不过这都是后话了,下次再介绍。