最近换了一份新的工作,到了新公司以后一直在思索接下来该写一些什么东西,直到有一天我在写代码过程中遇到一个让我非常无语的问题,我先来说说这个问题是什么吧, 公司里面的网络接口都是使用注解来做的,但是使用起来非常恶心的就是每次写完注解的接口类后,必须编译一下才能生成真正的请求类,,这样就导致在实际开发过程中大家非常排斥这些注解,感觉还没有手动写代码方便,其实这种方式可以使用动态代理的方式来解决,类似Retrofit ,我们这边实际的需求其实和Retrofit 还是有一点偏差的,Retrofit 在开发过程中比较推荐整合所有的请求到一个service中,这样可以减少反射的次数,尽可能的提升运行效率,但是我们项目中的组件化已经被拆分的非常细了,不同模块之间的网络请求必须留在自己模块之中,这就导致所有的接口比较分散,在实际处理过程中需要注意接口和动态编译生成的类之间的映射关系,对于EventBus3.0比较了解的同学可能会联想到他在3.0时增加的对应关系的文件,其实我们也可以借鉴他这种思想,使用apt在动态编译过程中帮助我们建立这样的辅助文件,先来看一下EventBus 自己生成的文件
非常的简单,也易懂,我就在想我是不是可以自己写一些apt的代码,来实现这个,就开启了我自学apt之路,
首先想要使用apt就必须知道Annotation 注解的作用,以及作用域,这个就不在本篇细说了,大家可以百度自行了解一下,关于这类的文章比较多,
使用apt的第一步就是创建 annotation Java modulel ,使用AndroidStrudio 创建 Java Or Katlin modulel ,同时编写anno文件,
我先贴一下我自己注解代码
/// 类注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface TsmAnnotation {
}
///方法注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface TsmCall {
}
大家可以看到我声明了一个类型为Type 的类注解, 一个类型为Method 的方法注解, 最开始的想法是遍历所有的类注解,生成Impl 实现该注解类, 重写未实现的方法,但是试验过很多api,无法找到这些未实现的方法,就放弃了这个想法,突然想到反正是编译过程中,又不是发生在运行过程中,那我再创建一个方法注解,在遍历类注解的过程中,不停的去遍历方法注解,如果方法注解的类名与类注解的类名相同,那么就认为这个方法是这个类的未实现的方法, 这就是我本次学习中最主要的逻辑,至于那么辅助类,写起来真是非常简单
写完anno之后,就进入到Processor 这个步骤了,同样的还是创建 一个java modulel ,此时不同的是,需要依赖刚才创建的 anno module,毕竟要引用刚才创建的 同时还需要autoService 依赖,还有antuService Processor 插件,我在最开始的时候从网上粘贴了一些代码,但是在build之后都没有什么反应,最后再一篇文章才知道了绝大部分人只是使用了 antuService 包,并没有使用 Processor ,简直让人抓狂,
先贴一下我的依赖
//自动生成代码框架
implementation 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
//开源java代码生成框架
implementation 'com.squareup:javapoet:1.10.0'
// anno 的依赖
implementation project(path: ':tsm_annotation')
此时所有的准备的工作都已经完成了,我们可以创建Processor 开始撸代码了,
@AutoService(Processor.class)
//@SupportedAnnotationTypes({"com.tsm.tsm_annotation.TsmAnnotation"})
public class TsmProcessor extends AbstractProcessor {
//辅助查找包名
private Elements mElementUtils;
///辅助打印日志
Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
mElementUtils = processingEnv.getElementUtils();
}
@Override
public Set getSupportedAnnotationTypes() {
HashSet supportTypes = new LinkedHashSet<>();
supportTypes.add(TsmAnnotation.class.getCanonicalName());
supportTypes.add(TsmCall.class.getCanonicalName());
return supportTypes;
}
}
在写Processor过程中有几个需要注意的地方
必须继承AbstractProcessor ,同时需要 @AutoService 注解 ,为了方便autoService帮我注入代码
想要使用的注解必须被添加到 getSupportedAnnotationTypes 的set里面,同时这个地方也可以使用另一个注解
@SupportedAnnotationTypes( {"包名+类名","包名+类名","包名+类名"} )
3.必须要重写process 这个方法,来帮助我们生成文件
/////其实在写编译文件过程中最折磨人的就是如何通过Element 来获取包名 类名 方法名, 由于使用的Element 可能是类注解,也可能是方法注解,所以还是有一定区别的,为了方便自己使用,我写了一个Utils,通过这个Util,可以方面的获取到类名和方法名,
mElementUtils = processingEnv.getElementUtils();
/**
* 类注解获取 类名
* @param element
* @return
*/
public static String getClassAnnotationClassName(TypeElement element){
return element.getSimpleName().toString();
}
/**
* 类注解获取包名
* @param mElementUtils
* @param element
* @return
*/
public static String getClassAnnotationPackageName(Elements mElementUtils,TypeElement element){
PackageElement packageElement = mElementUtils.getPackageOf(element.getEnclosingElement());
return packageElement.getQualifiedName().toString();
}
/**
* 方法注解获取类名
* @param element
* @return
*/
public static String getMethodAnnotationClassName(Element element){
return (element.getEnclosingElement()).getSimpleName().toString();
}
/**
* 方法注解获取包名
* @param mElementUtils
* @param element
* @return
*/
public static String getMethodAnnotationPackageName(Elements mElementUtils,TypeElement element){
PackageElement packageElement = mElementUtils.getPackageOf(element.getEnclosingElement());
return packageElement.getQualifiedName().toString();
}
/**
* 方法注解获取方法名
* @param element
* @return
*/
public static String getMethodAnnotationMethodName(Element element){
return element.getSimpleName().toString();
}
/**
* Parameter 注解获取方法名
* @param varEle
* @return
*/
public static String getParameterAnnotationMethodName(VariableElement varEle){
return varEle.getEnclosingElement().getSimpleName().toString();
}
/**
* Parameter 注解获取包名
* @param mElementUtils
* @param varEle
* @return
*/
public static String getParameterAnnotationPackageName(Elements mElementUtils,VariableElement varEle){
PackageElement packageElement = mElementUtils.getPackageOf(varEle.getEnclosingElement());
return packageElement.getQualifiedName().toString();
}
/**
* Parameter 注解获取类名
*
* @param varEle
* @return
*/
public static String getParameterAnnotationClassName(VariableElement varEle){
return varEle.asType().toString();
}
/**
* Parameter 注解获取属性名
* @param varEle
* @return
*/
public static String getParameterAnnotationAttrName(VariableElement varEle){
return varEle.getSimpleName().toString();
}
最后贴一下生成Impl文件的代码,写起来非常简单
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
//得到所有类的注解
Set extends TypeElement> elements = (Set extends TypeElement>) roundEnvironment.getElementsAnnotatedWith(TsmAnnotation.class);
//得到所有方法的注解
Set extends TypeElement> methods= (Set) roundEnvironment.getElementsAnnotatedWith(TsmCall.class);
for (TypeElement element : elements) {
String packageName =Utils.getClassAnnotationPackageName(mElementUtils,element);
String className =Utils.getClassAnnotationClassName(element);//类名
try {
ClassName clsName= ClassName.bestGuess(packageName+"."+className);
clsSet.put(className,element);
messager.printMessage(Diagnostic.Kind.NOTE, "-----------------------" + clsName.packageName()+ " "+clsName.simpleName());
//写Impl文件 文件名增加Impl后缀,并实现 类注解接口,
TypeSpec.Builder classType= TypeSpec.classBuilder(className + "Impl")
.addSuperinterface(clsName)
.addModifiers(Modifier.PUBLIC);
//遍历所有方法
for (Element method : methods) {
String methodClassName=Utils.getMethodAnnotationClassName(method);
///如果注解方法类名和注解类的类名相同,则证明是未实现的方法
if(methodClassName.equals(className)){
messager.printMessage(Diagnostic.Kind.NOTE, "-----------------------找到方法啦");
///开始写方法 无返回参数
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Utils.getMethodAnnotationMethodName(method))
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(void.class)
.addStatement(new StringBuilder()
.append(" $T.i( \"------------")
.append(className)
.append("----------")
.append(Utils.getMethodAnnotationMethodName(method))
.append("---------------\")").toString(),ClassName.bestGuess("com.tsm.tsm_commpont_model.utils.LogUtils"));
classType.addMethod(methodBuilder.build());
}
}
///最后写文件
JavaFile javaFile = JavaFile.builder("com.tsm.service.impl", classType.build()).build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
try {
///收集到了所有类注解,开始写辅助类,
createInfoIndexFile();
}catch (Exception e){}
return true;
}
在添加方法体的过程中出现了一些比较特殊的情况,那就是 我直接添加的代码无法找到他们类引用,此时需要用到 $T 这个替换符,所有需要引入的类就需要使用 ClassName 来替换该符号, 而ClassName的创建也非常简单 ClassName.bestGuess( 包名 + 类名 ),此时实现类已经完成,只需要加入想要代码逻辑了,
至于最终的辅助类,我的把java文件写一遍,,然后在通过手打把他写进去,注意import 就好了,
private void createInfoIndexFile() {
BufferedWriter writer = null;
String index="com.tsm.service.impl.TsmServiceIndex";
try {
JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(index);
int period = index.lastIndexOf('.');
String myPackage = period > 0 ? index.substring(0, period) : null;
String clazz = index.substring(period + 1);
writer = new BufferedWriter(sourceFile.openWriter());
if (myPackage != null) {
writer.write("package " + myPackage + ";\n\n");
}
writer.write("import java.util.HashMap;\n");
writer.write("import java.lang.reflect.InvocationTargetException;\n");
writer.write("import java.util.Map;\n");
Collection values = clsSet.values();
for (TypeElement element : values) {
PackageElement packageElement = mElementUtils.getPackageOf(element.getEnclosingElement());
String packageName = packageElement.getQualifiedName().toString();//包名,这个是获取到的有 @TsmAnnotation 标识类的包名
String className = element.getSimpleName().toString();//类名
writer.write("import "+"com.tsm.service.impl"+"."+className+"Impl;\n");//创建文件的包名是写死的,所以这里的包名必须写死
}
writer.write("public class " + clazz + "{\n");
writer.write(" private static Map providers = new HashMap<>();\n\n");
writer.write(" private static Map impl =new HashMap<>();\n\n");
writer.write(" public static T buildService(Class cls) {\n");
writer.write(" Object instance = impl.get(cls.getSimpleName());\n");
writer.write(" if(instance==null){\n");
writer.write(" findServiceImpl();\n");
writer.write(" Class cla = providers.get(cls.getSimpleName());\n");
writer.write(" try {\n");
writer.write(" instance=(T) cla.getConstructor().newInstance();\n");
writer.write(" impl.put(cls.getSimpleName(),instance);\n");
writer.write(" return (T)instance;\n");
writer.write(" } catch (InstantiationException e) {\n");
writer.write(" e.printStackTrace();\n");
writer.write(" } catch (InvocationTargetException e) {\n");
writer.write(" e.printStackTrace();\n");
writer.write(" } catch (NoSuchMethodException e) {\n");
writer.write(" e.printStackTrace();\n");
writer.write(" } catch (IllegalAccessException e) {\n");
writer.write(" e.printStackTrace();\n");
writer.write(" }\n");
writer.write(" }\n");
writer.write(" return (T)instance;\n");
writer.write(" }\n");
writer.write(" \n\n\n");
writer.write(" public static void findServiceImpl(){\n");
Set keySet = clsSet.keySet();
for (String key : keySet) {
TypeElement element=clsSet.get(key);
String className = element.getSimpleName().toString();//类名
writer.write(" providers.put(\""+key+"\", "+className+"Impl.class);\n");
}
writer.write(" }\n");
writer.write("}\n");
} catch (IOException e) {
throw new RuntimeException("Could not write source for " + index, e);
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
//Silent
}
}
}
}
到这里正题就结束了,解决的问题就是在写完注解接口后,不需要在等待编译生成新的文件,然后才能使用它,现在的使用方式变成了
TsmServiceIndex.buildService(TsmTestService.class).say();
其实在真正的开发过程中,使用场景肯定要比现在复杂,大家还需要继续努力
是不是方便很多