前面文章写了开始自定义APT之前需要了解的知识,刚开始接触自定义APT的小伙伴,可以看一下我前面的几篇文章:
自定义APT之:调试
自定义APT基础之:Element
自定义APT之:javapoet
现在终于可以开始创建自己的apt了。先说一下开始之前的业务背景。
在我的日常Android开发中,随着版本的不断迭代,需要对接N多个协议,需要适配N多个界面。在项目初期,使用if-else来进行区分,每个协议里面的又有很多子项。协议的子项通过Map来保存key-value键值对,通过反射来实例化并处理。(这里不推荐大家使用反射,可以使用抽象工厂来替代)。那么随着业务协议的增多以及更多界面的适配,出现了以下几个问题:
if-else方式:
1.代码臃肿,几十个if-else,单个java文件代码量过大;
2.依赖严重,每增加一个协议,都要去增加else,并且都是强引用。
3.代码不够优雅(最重要一点);
Map键值对反射方式:
1.每次增加子项都需要在Map里增加键值对,烦琐;
2.反射,性能消耗;
3.还是代码不够优雅;
针对上面的问题,起初我是想通过dagger来解决依赖的问题,但发现dagger还是避免不了在创建对象时要使用if-else判断。之后我想到了通过自定义apt的方式,老共传统技能:三步走战略。
第一步:使用apt,自动生成键值对,解决使用反射时,每次都需要手动去添加键值对;
第二步:使用apt,自动生成抽象工厂类,解决if-else的问题,连带解决了Map的反射;
第三步:在第二步基础上,支持抽象工厂传参构造。
按照我的想法,最终使用一个注解,自动生成抽象工程,创建对象的实例时,只需要按照协议解析出来的参数,自动实例化具体的对象。自动生成抽象工厂,最大的好处:有新的协议或者界面需要适配时,不用改动之前的代码,只需要在新增协议的处理类上加一个注解,自动构建,,封装性更强。
我的想法是生成这样一个类:
import java.lang.String;
import java.util.HashMap;
public class KimMap {
public static final HashMap MAP_CREATE = new HashMap<String, String>();
public static final HashMap MyMap = new HashMap<String, String>();
static {
MAP_CREATE.put("222","com.yanantec.ynarc.MainActivity");
MAP_CREATE.put("22","com.yanantec.ynarc.Test");
MyMap.put("666","com.yanantec.ynarc.BaseActivity");
}
}
首先由于apt只参与编译过程,并不会打包到apk中,所以定义注解和注解的实现需要分成两个模块,这里我们分成:annotation和complier模块,由于我们不需要Android的资源,只有java部分,因此annotation和complier模块使用java library。如下图:
首先在annotation模块新增注解:@MapCreate,该注解提供key和要加入到的Hashmap的变量名:
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface MapCreate
{
/**
* map添加的键值对的key
* @return
*/
String key();
/**
* map集合的 属性名
* @return
*/
String mapFiled() default "MAP_CREATE";
}
然后开始写complier模块:
1.这里需要依赖几个包:
// 自动构建执行processor
compileOnly 'com.google.auto.service:auto-service:1.0-rc6'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
implementation "com.yanantec:annotation:1.4.0"
// javapoet 用于生成java文件
implementation 'com.squareup:javapoet:1.11.1'
2.自定义processor,并加上@AutoService(Processor.class):
@AutoService(Processor.class)
public class MapProcessor extends AbstractProcessor
{
}
执行make project:
执行成功后,会自动创建下面文件:
auto-service其实也是一个apt,用于把@AutoService注解的类加入注册表(上图对应的文件),也可以不使用这个库,手动创建,并把对应自定义processor注册进去,如下图:
这里遇到我的第一个坑:@AutoService(Processor.class),里面固定用Processor.class,否则不会把当前MapProcessor 加入到注册表。
3.重写AbstractProcessor的以下方法:
@AutoService(Processor.class)
public class MapProcessor extends AbstractProcessor
{
private Filer mFiler;
private Messager mMessager;
// 用于存储map属性名和map存储的
private Map<String, List<TypeElement>> actionMaps = new HashMap<>();
private static String PACKAGE_NAME = "com.kim.map";
@Override
public synchronized void init(ProcessingEnvironment processingEnv)
{
super.init(processingEnv);
mFiler = processingEnv.getFiler();
mMessager = processingEnv.getMessager();
Map<String, String> options = processingEnv.getOptions();
if (options != null && options.size() > 0){
for (Map.Entry<String, String> entry : options.entrySet())
{
if (entry.getKey().contains("kim.applicationId")){
PACKAGE_NAME = entry.getValue();
}
}
}
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
{
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes()
{
// 设置我们要用的注解
return Collections.singleton(MapCreate.class.getCanonicalName());
}
@Override
public SourceVersion getSupportedSourceVersion()
{
return SourceVersion.latestSupported();
}
}
4.开始业务逻辑部分的代码:
业务思想:我们先把注解筛选成我们要使用的数据,这里我们使用Map<String, List>来存储Map的属性名和类元素,在生成java代码时,我们按照要生成几个Map,各自往Map里面添加<key, className>的键值对。
第一步,筛选:
actionMaps.clear();
// 获取所有MapCreate注解的元素
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(MapCreate.class);
// 遍历注解的类元素
Iterator<? extends Element> iterator = elements.iterator();
while (iterator.hasNext()){
Element element = iterator.next();
// MapCreate注解修饰的是类元素
if (element.getKind() == ElementKind.CLASS){
// 获取当前注解的类,要保存到的Map集合属性名
String filedName = element.getAnnotation(MapCreate.class).mapFiled();
List<TypeElement> elementList;
// 把当前类元素,加入到指定的Map中
if (actionMaps.containsKey(filedName)){
elementList = actionMaps.get(filedName);
}else {
elementList = new ArrayList<>();
}
elementList.add((TypeElement) element);
actionMaps.put(filedName, elementList);
}
}
第二步:生成.java文件:
// 生成MAP类
if (actionMaps.size() > 0){
// 创建一个类
TypeSpec.Builder typeBuilder = TypeSpec.classBuilder("KimMap").addModifiers(PUBLIC);
CodeBlock.Builder codeBuilder =CodeBlock.builder();
// 用于提示统一个集合是否存在重复的key
Map<String, String> allrRepeatKey = new HashMap<>();
for (Map.Entry<String, List<TypeElement>> entry : actionMaps.entrySet())
{
List<TypeElement> fileds = entry.getValue();
// 判断当前map属性名所包含的键值对是否为空
if (fileds == null || fileds.size() < 0) continue;
// map对应的属性名称
String mapName = entry.getKey();
// HashMap
ClassName hashMapClasssName = ClassName.get("java.util","HashMap");
// String
ClassName stringClassName = ClassName.get("java.lang", "String");
// HashMap<String, String>
TypeName hashMapStringClassName = ParameterizedTypeName.get(hashMapClasssName, stringClassName, stringClassName);
// 生成Map属性,并初始化,生成代码:P public static final HashMap MAP_CREATE = new HashMap<String, String>();
FieldSpec fieldSpec = FieldSpec.builder(HashMap.class, mapName)
.addModifiers(PUBLIC, FINAL, STATIC)
.initializer("new $T()", hashMapStringClassName)
.build();
typeBuilder.addField(fieldSpec);
// 添加类名和key的对应关系
Iterator<TypeElement> iterator1 = fileds.iterator();
// 找出类中重复的元素:<key值, 类名>,
Map<String, String> repeatKeys = new HashMap<>();
while (iterator1.hasNext()){
// 静态代码块中存入key-类名
TypeElement element = iterator1.next();
String value = element.getEnclosingElement().toString() + "." + element.getSimpleName().toString();
String key = element.getAnnotation(MapCreate.class).key();
codeBuilder.addStatement("$L.put($S,$S)", mapName, key, value);
if (repeatKeys.containsKey(key)){
// 保存重复的
String oldValue = repeatKeys.get(key);
if (!allrRepeatKey.containsKey(oldValue)){
allrRepeatKey.put(oldValue, key);
}
allrRepeatKey.put(value, key);
}else {
repeatKeys.put(key, value);
}
}
if (allrRepeatKey.size() > 0){
for (Map.Entry<String, String> stringEntry : allrRepeatKey.entrySet())
{
mMessager.printMessage(Diagnostic.Kind.ERROR, stringEntry.getKey() + "中的key:" + stringEntry.getValue() + ",已在其他类中存在");
}
return false;
}
}
// 构建类文件
TypeSpec typeSpec = typeBuilder.addStaticBlock(codeBuilder.build()).build();
// 生成.java文件
JavaFile javaFile = JavaFile.builder(PACKAGE_NAME, typeSpec).build();
try
{
javaFile.writeTo(mFiler);
} catch (Exception e)
{
e.printStackTrace();
mMessager.printMessage(Diagnostic.Kind.ERROR, "写文件错误:" + e);
}
最后,在项目中加入注解,并编译,编译成功后则可以找到生成的KimMap类。