写在前面:
- 环境:Android Studio 3.0
- 本文目的:运行时注解一般与反射搭配使用,Android中强烈不建议使用反射,所以一般的注解框架都是采用编译时注解。本文通过一个小例子来认识编译时注解。
- 需要了解注解的基本概念,gradle基础,以及一些注入的概念。如对注解一无所知可参考这篇:(Android)注解系列-注解基本概念
正文
一、效果需求
假如我们在MainActivity的类上声明两个水果类,并且通过注解的方式进行初始化属性。点击按钮时输出这两个对象的信息
public class MainActivity extends AppCompatActivity {
/**
* 在这添加我们需要的注解属性,初始化实例的时候需要读取注解内容,
*/
@FruitProperties(name = "红富士",price = 5.5)
Apple apple;
@FruitProperties(name = "小米蕉",price = 20.0)
Bananer bananer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//通过依赖的注解模块提供的FruitInject对我们注解的对象进行创建实例并赋值,赋值根据注解的属性
FruitInject.inject(this);
initClickListener();
}
/**
* 效果演示
*/
private void initClickListener() {
Button button = findViewById(R.id.btn_show);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this,apple.toString()+bananer.toString(),Toast.LENGTH_SHORT).show();
}
});
}
}
/**
* 效果演示
*/
private void initClickListener() {
Button button = findViewById(R.id.btn_show);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this,apple.toString()+bananer.toString(),Toast.LENGTH_SHORT).show();
}
});
}
}
二、模块分析
项目分成四个模块:
1.Annotation:存放我们定义的所有注解(java lib)
2.Api:注解对外的统一接口(java/Android lib根据需求)
3.AnnotationCompiler:注解处理器模块,在编译时读取注解并生java源码文件。(与app无关,仅生成代码,打包apk时不打包进去。java lib)
4.app:项目模块,仅为了演示编译时注解
如图:
![](https://img.haomeiwen.com/i4835273/915009993561586e.png)
使用别人的编译时注解框架会发现一般都是两个依赖包,一个是Api+Annotation模块,一个是compiler模块。
三、实现步骤
-
3.1 Annnotation 模块
创建Annotation模块,存放所有的注释类,这里仅有一个注释类:
/**
* 说明:水果属性注解,有name和price两个属性
*
* @author LJY on 2017/11/14
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface FruitProperties {
String name();
double price();
}
-
3.2 Api模块
首先我们先写一个FruitProvider,我们的注解处理器会生成一个该接口的实现类,怎么生成后面会介绍:
public interface FruitProvider<T> {
void provide(T host);
}
然后还需要一个注入类,提供统一的注入入口:
public class FruitInject {
/**
* 注入方法,其实内部调用FruitProvider的方法
* @param host 我们传入的对象(MainActivity),访问其内部属性(Apple,Bananer)需要
*/
public static void inject(Object host){
try {
//获取frutiProvider接口的实现类,并调用接口方法。该实现类是通过注解处理器生成的
String hostName = host.getClass().getName();
Class<?> fruitProviderClass = Class.forName(hostName + "$$FruitProvider");
FruitProvider fruitProvider = (FruitProvider) fruitProviderClass.newInstance();
fruitProvider.provide(host);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
以上两步就是该模块必须的内容,为了演示我们还添加了一个Fruit接口:
/**
* 说明:水果接口
* @author LJY on 2017/11/14
*/
public interface Fruit {
Fruit init(String name, double price);
}
-
3.3注解处理器模块(本文的重点,核心)
3.3.1依赖文件:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
...
//注册依赖(可以通过autoService注册该注释处理器)
implementation 'com.google.auto.service:auto-service:1.0-rc3'
//java文件生成的工具类
implementation 'com.squareup:javapoet:1.9.0'
//我们需要处理的注解模块
implementation project(':Annotation')
}
3.3.2注解处理器
所有的注解处理器都需要实现javax.annotation.processing.Processor。这里我们定义MyAnnotationProcessor直接继承AbstractProcessor抽象类。我们会重写他的四个方法
//返回支持的注解类型,这个方法我们会用注释代替
Set<String> getSupportedAnnotationTypes();
//返回支持的源码版本,这个方法我们会用注释代替
SourceVersion getSupportedSourceVersion();
//在这里和可以获得一些工具类
void init(ProcessingEnvironment var1);
//真正的处理在这里,必须
boolean process(Set<? extends TypeElement> var1, RoundEnvironment var2);
此外我们还需要注册这个注解处理器,这样编译器才知道我们需要加载这个注解处理器。直接在MyAnnotationProcessor添加@AutoService(Processor.class),也有别的方式注册,这里不介绍。
上代码,注释写的比较详细:
/**
* 注:输出文件对象生成的java源码文件:MainActivity$$FruitProvider,本例子的类元素:MainActivity
*/
@AutoService(Processor.class)//注册注释处理器
@SupportedSourceVersion(SourceVersion.RELEASE_8)//支持源码版本
@SupportedAnnotationTypes("www.ljy.annotation.FruitProperties")//该处理器支持的注解类
public class MyAnnotationProcessor extends AbstractProcessor {
/**
* 文件输出工具类
*/
private Filer mFiler;
/**
* 元素辅助工具类
* 元素解释,一个java文件含有多个元素,包括类元素,方法元素,属性元素等...类似于xml文件
*/
private Elements mElementUtils;
/**
* 这里保存我们要输出的文件信息对象集合
*/
private Map<String ,JavaFileInfo> mJavaFileInfos =new TreeMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
//此外还有很多工具,如有需求,请自行查阅
mFiler = processingEnvironment.getFiler();
mElementUtils = processingEnvironment.getElementUtils();
}
/**
* 主要的处理在这里
*/
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
mJavaFileInfos.clear();
//获取所有标记了FruitProperties注解的元素
Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(FruitProperties.class);
//初始化所有要输出文件信息对象,保存到集合里
initFileInfos(elements);
//输出所有的java源码文件
outputJavaFile();
return true;
}
/**
* 将所有的文件信息对象输出java源码文件
*/
private void outputJavaFile() {
for (JavaFileInfo javaFileInfo: mJavaFileInfos.values()){
try {
javaFileInfo.generatedFile().writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 遍历所有元素(这里都是属性元素),提取他的类元素,查map是否已经有该类元素对应的输出文件信息对象,不存在则创建并且添加到集合中。
* 然后将该属性元素添加到输出文件信息对象中。
* @param elements
*/
private void initFileInfos(Set<? extends Element> elements) {
for (Element element:elements){
//获取元素的最外层(类元素,这里是MainActivity.class)
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
String fullName=enclosingElement.getQualifiedName().toString();
JavaFileInfo javaFileInfo = mJavaFileInfos.get(fullName);
if (javaFileInfo==null){
javaFileInfo= new JavaFileInfo(enclosingElement,mElementUtils);
mJavaFileInfos.put(fullName,javaFileInfo);
}
javaFileInfo.addFruitField(new FruitField(element));
}
}
}
以上就是核心步骤了
3.3.3其他类介绍:
- FruitField:封装属性元素信息,包括元素信息,元素注解信息
public class FruitField {
private VariableElement mVariableElement;
private String name;
private double price;
/**
* @param element
*/
FruitField(Element element) {
//如果这个元素的类型不是《属性元素》抛出异常
if (element.getKind() != ElementKind.FIELD) {
throw new IllegalArgumentException(String.format("Only fields can be annotated with @%$", FruitProperties
.class.getSimpleName()));
} else {
mVariableElement = (VariableElement) element;
//获取属性元素上的注解信息
FruitProperties fruitProperties=mVariableElement.getAnnotation(FruitProperties.class);
name=fruitProperties.name();
price=fruitProperties.price();
}
}
/**
* @return 元素名称
*/
Name getFieldName(){
return mVariableElement.getSimpleName();
}
/**
* @return 注解Name值
*/
String getFruitName(){
return name;
}
/**
* @return 注解price值
*/
double getPrice(){
return price;
}
/**
* @return 属性类型
*/
TypeMirror getFieldType(){
return mVariableElement.asType();
}
}
- JavaFileInfo:封装输出文件信息,可以生成JavaFile对象来输出源码文件,通过这个对象来配置要生成的JAVA文件的包,类,方法等属性
public class JavaFileInfo {
/**
* 该输出文件(MainActivity$$FruitProvider.class)对应的类元素(MainActivity.class)
*/
private TypeElement mTypeElement;
/**
* 输出文件对应类元素的属性变量集合
*/
private ArrayList<FruitField> mField;
/**
* 元素辅助工具类
*/
private Elements mElementUtils;
public JavaFileInfo(TypeElement element, Elements elementUtils) {
mTypeElement = element;
mElementUtils = elementUtils;
mField = new ArrayList<>();
}
public void addFruitField(FruitField fruitField) {
mField.add(fruitField);
}
/**
* @return 输出文件对象
*/
public JavaFile generatedFile() {
MethodSpec methodSpec = generatedMethod();
TypeSpec typeSpec = generatedClass(methodSpec);
String packageName = generatedPackage();
JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();
return javaFile;
}
/**
* @return 包名
*/
private String generatedPackage() {
//通过类元素获取包名
String packageName = mElementUtils.getPackageOf(mTypeElement).getQualifiedName().toString();
return packageName;
}
/**
* @param methodSpec 构造方法
* @return 构造类
*/
private TypeSpec generatedClass(MethodSpec methodSpec) {
TypeSpec typeSpec = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "$$FruitProvider")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(ParameterizedTypeName.get(getFruitProviderClassName(), TypeName.get(mTypeElement.asType())))
.addMethod(methodSpec).build();
return typeSpec;
}
private MethodSpec generatedMethod() {
MethodSpec.Builder provideMethodBuilder = MethodSpec.methodBuilder("provide");
provideMethodBuilder
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(TypeName.get(mTypeElement.asType()), "host");
for (FruitField fruitField : mField) {
provideMethodBuilder.addStatement("host.$L=new $L().init(\"$L\",$L)", fruitField.getFieldName(), ClassName.get(fruitField.getFieldType()),fruitField.getFieldName(), fruitField.getPrice());
}
return provideMethodBuilder.build();
}
private ClassName getFruitProviderClassName() {
return ClassName.get("www.ljy.api", "FruitProvider");
}
}
完成以上步骤并编译下就可以生成我们想要的java代码文件了,这里生成的是MainActivity$$FruitProvider
public class MainActivity$$FruitProvider implements FruitProvider<MainActivity> {
@Override
public void provide(MainActivity host) {
host.apple=new www.ljy.annotationprocessordemo.Apple().init("apple",5.5);
host.bananer=new www.ljy.annotationprocessordemo.Bananer().init("bananer",20.0);
}
}
4.app演示模块
依赖:
implementation project(':annotations')
implementation project(':api')
//annotationProcessor依赖的包不会打包进apk中
annotationProcessor project(':complier')
除了一开始看到的MainActivity外,还有演示用的apple类:
public class Apple implements Fruit {
String mName;
double mPrice;
public Apple() {
}
@Override
public Apple init(String name, double price){
mName=name;
mPrice=price;
return this;
}
@Override
public String toString() {
return "Apple{" + "mName='" + mName + '\'' + ", mPrice=" + mPrice + '}';
}
}
-
四、总结
其实最终打包进apk的只有我们的annotation+api+app模块,compiler只是我们自己写的一个工具而已,我们用这工具生成我们想要的代码(该代码在app-build-generated-source -apt-debug下)
结语
注1:如果编译时出现 错误: 编码GBK的不可映射字符 请在对应的模块gradle添加:
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
注2:本例仅为了讲一些基础型的知识,不考虑例子中内存泄露等问题。
注3:注解处理器在什么时候运行呢?他的优先级是非常高的,可以理解成编译的第0步。
注4:源码地址: AnnotationProcessorDemo
参考:
网友评论