一、写在前面
大家在开发过程中,肯定都接触过注解,使用它可以很优雅地实现我们的功能,同时可以结合javaPoet(KotlinPoet)来解决一些重复代码的问题。
二、目标
这里我们拟定一个目标,比如我们在网络请求的时候,拿Retrofit举例,我们调用接口请求,直接以interface的形式定义好我们请求的格式,剩下的内部使用动态代理去实现了,当然动态代理是运行时的,会耗费更多的运行时资源,我们可不可以使用自定义注解的形式将它改成编译时呢?当然这里是一个目标,我们这里只介绍使用“自定义注解+javapoet”的使用,不追求达到跟Retrofit完全一样的功能。
三、实现逻辑
1、注解结构定义
我们知道一个网络请求涉及到很多东西,我们最常变动的就是业务参数,以及HEAD等,所以我们先定义两个个接口。
在此之前先说下两个注解,Retention和Target, 一般自定义注解会加上这连个注解:
Retention
1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
生命周期长度 SOURCE < CLASS < RUNTIME ,所以前者能作用的地方后者一定也能作用。一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。
Target
ElementType 这种枚举类型的常量提供了Java程序中可能出现注释的语法位置的简单分类。这些常量在java.lang.annotation.Target元注释中用于指定在何处写入给定类型的注释是合法的。注释可能出现的语法位置分为声明上下文(注释适用于声明)和类型上下文(注释适用于声明和表达式中使用的类型)。常量注释类型、构造函数、字段、局部变量、方法、包、参数、类型和类型参数与JLS 9.6.4.1中的声明上下文相对应。
public enum ElementType {
/** 类、接口(包括注释类型)或枚举声明 */
TYPE,
/** 字段声明(包括枚举常量) */
FIELD,
/** 方法声明 */
METHOD,
/** 形式参数声明 */
PARAMETER,
/** 构造函数声明 */
CONSTRUCTOR,
/** 局部变量声明 */
LOCAL_VARIABLE,
/** 注释类型声明 */
ANNOTATION_TYPE,
/** 包 声明 */
PACKAGE,
/**
* 类型参数声明
*
* @since 1.8
*/
TYPE_PARAMETER,
/**
* 字体的使用
*
* @since 1.8
*/
TYPE_USE
}
接下来我们自定义两个注解:
1、RequestFit
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RequestFit {
String api();
String version();
}
这个RequestFit是用来标记哪些方法是是需要处理的,注解处理器可以根据这个注解来识别需要处理的方法,内部的方法可自定义,这里可以放一些固定参数。
2、RequestHead
@Target(METHOD)
@Retention(RUNTIME)
public @interface RequestHead {
String[] value();
}
这个就是HEAD的自定义注解,当然,此注解可以不使用,一般说来接口还是业务参数居多。
2、请求结构定义
上述将注解的结构定义好了,接下来要定义interface,同时使用新定义的注解来描述我们的请求。
public interface ApiRequest {
@RequestFit(api = "xxxxxaaa",version = "1.0")
RequestParams getDetail(String name, HashMap dataParams);
@RequestFit(api = "xxxxx",version = "1.0")
RequestParams getListMessage(String age, HashMap dataParams);
}
这里我们先定一个ApiRequest,内部有两方法,表示有两个网络请求,我们这里只演示RequestFit,RequestHead就不演示了,使用跟RequestFit是一样的。
如上所示,以getDetail为例,我们使用了参数分别有String类型和HashMap类型,注解RequestFit表示当前是哪个api,以及对应版本号。
3、注解处理器实现
强调下,注解处理器需要单独写个Moudle,然后这个Moudle是一个java library。
导入需要的依赖
//处理器注册
implementation 'com.google.auto.service:auto-service:1.0.1'
annotationProcessor 'com.google.auto.service:auto-service:1.0.1'
implementation 'com.google.auto:auto-common:1.2.1'
annotationProcessor 'com.google.auto:auto-common:1.2.1'
//========注解必备(3)=========
//开源javapoet:https://github.com/square/javapoet/
//使用在线库 或者 拷贝源码库
//用于注解之后,进行的代码处理框架(比手动写效率高)
implementation 'com.squareup:javapoet:1.13.0'
这里说一下,APT其实就是基于SPI一个工具,是JDK留给开发者的一个在编译前处理注解的接口,AutoService框架的作用是自动生成SPI清单文件(META-INF/services下的文件)。不用它也行,如果不使用它就需要手动去创建这个文件、手动往这个文件里添加服务(接口实现),但是既然有了,为啥不用呢
MyProcessor
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
/**
* Types是一个用来处理TypeMirror的工具
*/
private Types typesUtils;
/**
* Elements是一个用来处理Element的工具
*/
private Elements elements;
/**
* 生成java源码
*/
private Filer filer;
/**
* Messager提供给注解处理器一个报告错误、警告以及提示信息的途径。
* 它不是注解处理器开发者的日志工具,
* 而是用来写一些信息给使用此注解器的第三方开发者的
*/
private Messager messager;
private Locale locale;
private SourceVersion sourceVersion;
private Map<String, String> optMap;
//===============核心设置==================
private Map<String, MyAnnotationClass> mAnnotatedClassMap;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
//===============核心设置==================
typesUtils = processingEnv.getTypeUtils();
filer = processingEnv.getFiler();
//ProcessingEnvironment可以获取的对象
elements = processingEnv.getElementUtils();
messager = processingEnv.getMessager();
locale = processingEnv.getLocale();
sourceVersion = processingEnv.getSourceVersion();
optMap = processingEnv.getOptions();
//
mAnnotatedClassMap = new TreeMap<>();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnv) {
mAnnotatedClassMap.clear();
//创建自定义注解处理类
try {
processMyView(roundEnv);
} catch (Exception e) {
System.out.println("异常:" + e.toString());
}
//将自定义注解处理类,写入文件
for (MyAnnotationClass annotationClass : mAnnotatedClassMap.values()) {
try {
annotationClass.generateFiler().writeTo(filer);
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
/**
* 创建处理类,处理自定义注解
*
* @param roundEnv
*/
private void processMyView(RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(RequestFit.class)) {
MyAnnotationClass annotationClass = createMyAnnotationClass(element);
MySendField bindViewField = new MySendField(element);
annotationClass.addField(bindViewField);
System.out.println("processMyView annotatedClass: " + annotationClass);
System.out.println("processMyView MySendField: " + bindViewField);
}
}
/**
* 获取每一个Element的处理类,并将生成的处理类保存到map中
*
* @param element
* @return
*/
private MyAnnotationClass createMyAnnotationClass(Element element) {
TypeElement typeElement = (TypeElement) element.getEnclosingElement();
String fullName = typeElement.getQualifiedName().toString();
System.out.println("getAnnotatedClass typeElement: " + typeElement);
MyAnnotationClass annotationClass = mAnnotatedClassMap.get(fullName);
//如果集合中不存在,则添加到集合中
if (annotationClass == null) {
//创建注解处理类
annotationClass = new MyAnnotationClass(typeElement, elements);
mAnnotatedClassMap.put(fullName, annotationClass);
}
return annotationClass;
}
/**
* (3)必须重写的方法,
* <p>
* 处理器想要处理的自定义注解
*
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
types.add(annotation.getCanonicalName());
}
return types;
}
/**
* <p>
* 将自定义的注解添加到set列表中
* <p>
* 给(3)getSupportedAnnotationTypes使用
*
* @return
*/
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
annotations.add(RequestFit.class);
return annotations;
}
/**
* (4)必须重写的方法:
* <p>
* 指定使用的Java版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_6
*
* @return 使用的Java版本
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
MyAnnotationClass
public class MyAnnotationClass {
private ArrayList<MySendField> mFields;//自定义注解的处理集合
private TypeElement mTypeElement;
private Elements mElements;
private ArrayList<MethodSpec> bindViewBuidlers;//自定义注解的处理集合
private ArrayList<MethodSpec> netRequestBuidlers;//自定义注解的处理集合
public MyAnnotationClass(TypeElement typeElement, Elements elements) {
mFields = new ArrayList<>();
bindViewBuidlers = new ArrayList<>();
netRequestBuidlers = new ArrayList<>();
this.mTypeElement = typeElement;
this.mElements = elements;
}
/**
* 保存有自定义注解的处理
*
* @param field
*/
public void addField(MySendField field) {
mFields.add(field);
}
/**
* 核心
* 利用开源javaPoet生成对应的.java代码
*
* @return
*/
public JavaFile generateFiler() {
try {
//添加网络请求方法的处理解析
for (MySendField field : mFields) {
String dataType = field.getDataType();
System.out.println("创建方法: " + field.getMethodName().toString()+" "+ dataType);
List<ParameterSpec> specList = field.getMethodParams();
//(1)生成java网络请求方法:
MethodSpec.Builder bindViewBuidler = MethodSpec.methodBuilder(field.getMethodName().toString())
.addModifiers(Modifier.PUBLIC)//public
.addAnnotation(Override.class)//接口的复写方法
.addParameters(specList)
.returns(TypeUtil.PROVIDER);
bindViewBuidler.addStatement("$T params =new $T()"
, ClassName.get(HashMap.class)
, ClassName.get(HashMap.class));
bindViewBuidler.addStatement("params.put("API",$S)"
, field.getApi());
bindViewBuidler.addStatement("params.put("VERSION",$S)"
, field.getVersion());
for (ParameterSpec spec : specList) {
bindViewBuidler.addStatement("params.put("$N"," + spec.name + ")"
, spec.name);
}
bindViewBuidler.addStatement("$T requestParams =new $T()"
, TypeUtil.PROVIDER
, TypeUtil.PROVIDER);
bindViewBuidler.addStatement("requestParams.setParams(params)"
, field.getApi());
bindViewBuidler.addStatement("return requestParams");
bindViewBuidlers.add(bindViewBuidler.build());
}
System.out.println("创建方法完事: " + "");
//(3)生成java的类文件(.java的文件)
TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "Impl")
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(TypeName.get(mTypeElement.asType()))//类实现的接口名
.addMethods(bindViewBuidlers)
.build();
//添加包名
String packageName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString();
//JavaFile
JavaFile result = JavaFile.builder(packageName, injectClass).build();
//将打印也写入
result.writeTo(System.out);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 反射调用对应的接口,需要的接口位置,包名:com.test.lib
*/
private static class TypeUtil {
static final ClassName PROVIDER = ClassName.get("com.test.testrequestfit", "RequestParams");
}
}
MYSendField
public class MYSendField {
private ExecutableElement mVariableElement;
private String api;
private String version;
private String dataType;
public MYSendField(Element element) {
//排错
if (element.getKind() != ElementKind.METHOD) {
throw new IllegalArgumentException(String.format("Only fields can be annotated with @%s",
RequestFit.class.getSimpleName()));
}
System.out.println("MySendField before mVariableElement: " + element);
mVariableElement = (ExecutableElement) element;
System.out.println("MySendField after mVariableElement: " + mVariableElement);
//获取自定义注解
RequestFit requestFit = mVariableElement.getAnnotation(RequestFit.class);
RequestDataType requestDataType = mVariableElement.getAnnotation(RequestDataType.class);
if(null !=requestDataType) {
dataType = requestDataType.type();
}
//拿到控件的id
api = requestFit.api();
version = requestFit.version();
System.out.println("BindViewField mResId: " + api);
if (null == api) {
throw new IllegalArgumentException(
String.format("value() in %s for field %s is not valid !", RequestFit.class.getSimpleName(),
mVariableElement.getSimpleName()));
}
}
public String getDataType() {
return dataType;
}
public Name getMethodName() {
return mVariableElement.getSimpleName();
}
public ExecutableElement getmVariableElement() {
return mVariableElement;
}
public List<ParameterSpec> getMethodParams() {
List<ParameterSpec> specList = new ArrayList<>();
for (VariableElement element: mVariableElement.getParameters()) {
specList.add(ParameterSpec.builder(TypeName.get(element.asType()), element.getSimpleName().toString())
.addModifiers(element.getModifiers())
.build());
}
}
public String getVersion() {
return version;
}
public String getApi() {
return api;
}
/**
* 获取变量类型
*
* @return
*/
TypeMirror getFieldType() {
return mVariableElement.asType();
}
}
上述类中最主要逻辑在MyProcessor的processMyView方法中,就是通过getElementsAnnotatedWith来获取我们自定义注解RequestFit标记的方法,然后根据typeElement.getQualifiedName() 来归属生成的类,因为我们也可以定义多个interface。Map对应的value是一个MyAnnotationClass对象,这个对象内部维护一个MySendField列表,每个MySendField对应一个注解标记的方法,即每个注解的Element都被收集好了,最后在MyProcessor的process方法中逐个annotationClass.generateFiler().writeTo(filer) 使用javaPoet生成java文件,写入本地。
细节分析
这里注重说下javapoet的使用流程,上述说了MyAnnotationClass中存放了MySendField的列表,接下来都是根据这个来生成对应代码。
1、定义生成的方法
MethodSpec.Builder bindViewBuidler
= MethodSpec.methodBuilder(field.getMethodName().toString())
.addModifiers(Modifier.PUBLIC)//public
.addAnnotation(Override.class)//接口的复写方法
.addParameters(specList)
.returns(TypeUtil.PROVIDER);
static final ClassName PROVIDER = ClassName.get("com.test.testrequestfit", "RequestParams");
field为循环的一个MySendField对象, 我们定义了一个RequestParams, 用于存放所有参数,返回,然后用公共的请求方法来调用。
2、填充参数
//创建Hash对象
bindViewBuidler.addStatement("$T params =new $T()", ClassName.get(HashMap.class), ClassName.get(HashMap.class));
//填充RequestFit注解的api
bindViewBuidler.addStatement("params.put("API",$S)", field.getApi());
//填充RequestFit注解的version
bindViewBuidler.addStatement("params.put("VERSION",$S)", field.getVersion());
//循环填充 注解标记方法的参数
for (ParameterSpec spec : specList) {
bindViewBuidler.addStatement("params.put("$N"," + spec.name + ")", spec.name);
}
// 创建RequestParams对象
bindViewBuidler.addStatement("$T requestParams =new $T()", TypeUtil.PROVIDER, TypeUtil.PROVIDER);
//将上述的参数填充进requestParams对象中
bindViewBuidler.addStatement("requestParams.setParams(params)", field.getApi());
//返回这个对象
bindViewBuidler.addStatement("return requestParams");
这里就是根据每个MySendField, 生成填充参数的过程,简单说下各种Spec
JavaPoet的常用类
类名 | 备注 |
---|---|
TypeSpec | 用于生成类、接口、枚举对象的类 |
MethodSpec | 用于生成方法对象的类 |
ParameterSpec | 用于生成参数对象的类 |
AnnotationSpec | 用于生成注解对象的类 |
FieldSpec | 用于配置生成成员变量的类 |
在JavaPoet中,format中存在三种特定的占位符:
备注 | |
---|---|
$T | 在JavaPoet代指的是TypeName,该模板主要将Class抽象出来,用传入的TypeName指向的Class来代替 |
$N | 代指的是一个名称,例如调用的方法名称,变量名称,这一类存在意思的名称 |
$S | 和String.format中%s一样,字符串的模板,将指定的字符串替换到S的地方,需要注意的是替换后的内容,默认自带了双引号,如果不需要双引号包裹,需要使用S的地方,需要注意的是替换后的内容,默认自带了双引号,如果不需要双引号包裹,需要使用S的地方,需要注意的是替换后的内容,默认自带了双引号,如果不需要双引号包裹,需要使用L |
上述specList就是根据给个注解标记得方法对应的ExecutableElement,通过getParameters()方法,取到VariableElement
for (VariableElement element: mVariableElement.getParameters()) {
specList.add(ParameterSpec.builder(TypeName.get(element.asType()), element.getSimpleName().toString())
.addModifiers(element.getModifiers())
.build());
}
就可以得到该标记方法所有的参数对应的ParameterSpec列表。
关于java的Element,有以下类型:
名称 | 描述 |
---|---|
VariableElement | 变量元素,表示字段、 enum常量、方法或构造函数参数、局部变量、资源变量或异常参数 |
ExecutableElement | 可执行元素,表示类或接口的方法、构造函数或初始值设定项(静态或实例),包括注解类型元素 |
TypeElement | 类型元素,表示一个类或接口程序元素。 提供对有关类型及其成员的信息的访问。 请注意,枚举类型是一种类,注解类型是一种接口 |
PackageElement | 包元素,表示包程序元素。 提供对有关包及其成员的信息的访问 |
Parameterizable | 可参数化的,具有类型参数的元素的混合接口 |
TypeParameterElement | 类型参数化元素,表示泛型类、接口、方法或构造函数元素的形式类型参数 |
QualifiedNameable | 限定可命名的 |
我们上面用到了ExecutableElement,就是对应被标记的方法,对应每个参数就是一个VariableElement。
3、生成对应的Java类
//(3)生成java的类文件(.java的文件)
TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "Impl")
.addModifiers(Modifier.PUBLIC)
// .addSuperinterface(ParameterizedTypeName.get(TypeUtil.BINDER, TypeName.get(mTypeElement.asType())))//类实现的接口名
.addSuperinterface(TypeName.get(mTypeElement.asType()))//类实现的接口名
.addMethods(bindViewBuidlers)
.build();
//添加包名
String packageName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString();
//JavaFile
JavaFile result = JavaFile.builder(packageName, injectClass).build();
//将打印也写入
result.writeTo(System.out);
就是生成一个mTypeElement名+"Impl"的类,该类实现了这个mTypeElement对应的接口,添加包名,并写入。
四、结语
对比一下编写的文件以及生成的文件
ApiRequest.java
ApiRequestImpl.java
可以看到这个已经成功了,接下来就是在请求网络接口调用时,填入上述生成的对应方法,当然还需要写一套请求的逻辑,将参数与okHttp关联起来。本文中例子是用来生成重复代码的。后续网络请求库的封装就不叙述了。总之完成后,每次请求代码调用类似于 :
NetWorkUtils.request(ApiRequestImpl.getDetail("xxx",{"age":"11"}),new RequestCallBack(){
@Override
void success(MyResponse response){
}
@Override
void error(Response response){
}
},MyResponse.class);