Custom apt actual combat one > Mapcreate

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.

Tags: Android apt

Posted by phpdolan on Fri, 15 Apr 2022 07:40:26 +0930