The previous articles have written about the knowledge you need to know before starting to customize APT. for those who have just started to contact with custom APT, you can take a look at my previous articles:
Custom APT: debugging
Element of user defined APT Foundation
Custom APT: javapoet
Now you can finally start creating your own apt. Let's talk about the business background before we start.
In my daily Android development, with the continuous iteration of the version, I need to connect N multiple protocols and adapt N multiple interfaces. At the beginning of the project, if else is used to distinguish. There are many sub items in each protocol. The sub items of the protocol save key value pairs through Map, and instantiate and process them through reflection. (reflection is not recommended here. You can use abstract factory instead). Then, with the increase of business protocols and the adaptation of more interfaces, the following problems arise:
If else mode:
1. The code is bloated, dozens of if else, and the amount of code in a single java file is too large;
2. The dependence is serious. For each protocol added, else must be added, and they are all strong references.
3. The code is not elegant enough (the most important point);
Reflection method of Map key value pair:
1. Every time you add a sub item, you need to add key value pairs in the Map, which is cumbersome;
2. Reflection, performance consumption;
3. The code is not elegant enough;
In view of the above problems, at first I wanted to solve the dependency problem through dagger, but I found that dagger still can not avoid using if else judgment when creating objects. Then I thought of the traditional skills of the Communist Party of China through customized apt: three-step strategy.
Step 1: use apt to automatically generate key value pairs. When using reflection, you need to manually add key value pairs every time;
Step 2: use apt to automatically generate abstract factory classes, solve the problem of if else, and solve the reflection of Map;
Step 3: on the basis of step 2, support the transfer parameter construction of abstract factory.
According to my idea, finally, an annotation is used to automatically generate an abstract project. When creating an instance of an object, you only need to automatically instantiate a specific object according to the parameters parsed by the protocol. The biggest advantage of automatic generation of abstract factory: when there is a new protocol or interface that needs to be adapted, there is no need to change the previous code, but only need to add an annotation on the processing class of the new protocol, which can be built automatically and has stronger encapsulation.
Let's start the first step today. My idea is to generate such a class:
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"); } }
First of all, apt only participates in the compilation process and will not be packaged into apk. Therefore, the implementation of defining annotations and annotations needs to be divided into two modules. Here we are divided into annotation and supplier modules. Since we do not need Android resources and only have java part, the annotation and supplier modules use java library. The figure is as follows:
First, add the annotation @ MapCreate in the annotation module, which provides the key and the variable name of the Hashmap to be added:
@Documented @Retention(RetentionPolicy.CLASS) @Target(ElementType.TYPE) public @interface MapCreate { /** * map Key of added key value pair * @return */ String key(); /** * map Property name of the collection * @return */ String mapFiled() default "MAP_CREATE"; }
Then start writing the complier module:
1. There are several packages to rely on:
// Auto build execution 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 is used to generate java files implementation 'com.squareup:javapoet:1.11.1'
2. Customize the processor and add @ AutoService(Processor.class):
@AutoService(Processor.class) public class MapProcessor extends AbstractProcessor { }
Execute make project:
After successful execution, the following files will be automatically created:
In fact, auto service is also an apt, which is used to add the class annotated by @ AutoService to the registry (the corresponding file in the figure above). You can also create it manually without using this library and register the corresponding custom processor, as shown in the following figure:
Here I come across my first pit: @ AutoService(Processor.class), which is fixed with processor Class, otherwise the current MapProcessor will not be added to the registry.
3. Rewrite the following methods of AbstractProcessor:
@AutoService(Processor.class) public class MapProcessor extends AbstractProcessor { private Filer mFiler; private Messager mMessager; // Used to store the map attribute name and map storage 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() { // Set the annotation we want to use return Collections.singleton(MapCreate.class.getCanonicalName()); } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } }
4. Code for starting business logic:
Business idea: we first filter the annotations into the data we want to use. Here, we use Map < string, list > to store the attribute names and class elements of the Map. When generating java code, we add the key value pairs of < key, classname > to the Map according to the number of maps to be generated.
Step 1: filter:
actionMaps.clear(); // Get all MapCreate annotated elements Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(MapCreate.class); // Traversing annotated class elements Iterator<? extends Element> iterator = elements.iterator(); while (iterator.hasNext()){ Element element = iterator.next(); // The MapCreate annotation modifies class elements if (element.getKind() == ElementKind.CLASS){ // Get the attribute name of the Map collection to save to the class of the current annotation String filedName = element.getAnnotation(MapCreate.class).mapFiled(); List<TypeElement> elementList; // Add the current class element to the specified Map if (actionMaps.containsKey(filedName)){ elementList = actionMaps.get(filedName); }else { elementList = new ArrayList<>(); } elementList.add((TypeElement) element); actionMaps.put(filedName, elementList); } }
Step 2: generate java file:
// Generate MAP class if (actionMaps.size() > 0){ // Create a class TypeSpec.Builder typeBuilder = TypeSpec.classBuilder("KimMap").addModifiers(PUBLIC); CodeBlock.Builder codeBuilder =CodeBlock.builder(); // Used to prompt whether there are duplicate key s in a uniform set Map<String, String> allrRepeatKey = new HashMap<>(); for (Map.Entry<String, List<TypeElement>> entry : actionMaps.entrySet()) { List<TypeElement> fileds = entry.getValue(); // Judge whether the key value pair contained in the current map attribute name is empty if (fileds == null || fileds.size() < 0) continue; // Attribute name corresponding to 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); // Generate the Map attribute and initialize it. The generation code is 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); // Add the correspondence between class name and key Iterator<TypeElement> iterator1 = fileds.iterator(); // Find out the duplicate elements in the class: < key value, class name >, Map<String, String> repeatKeys = new HashMap<>(); while (iterator1.hasNext()){ // Store key class name in static code block 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)){ // Save duplicate 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() + "Medium key:" + stringEntry.getValue() + ",Already exists in another class"); } return false; } } // Build class file TypeSpec typeSpec = typeBuilder.addStaticBlock(codeBuilder.build()).build(); // Generate java file JavaFile javaFile = JavaFile.builder(PACKAGE_NAME, typeSpec).build(); try { javaFile.writeTo(mFiler); } catch (Exception e) { e.printStackTrace(); mMessager.printMessage(Diagnostic.Kind.ERROR, "Write file error:" + e); }
Finally, add annotations to the project and compile it. After successful compilation, you can find the generated KimMap class.