APT(Annotation Processing Tool) is an Annotation Processing Tool used to scan and process annotations during compilation and generate Java files through annotations. Using annotations as a bridge, Java files are automatically generated using predefined code generation rules. Examples of such annotation frameworks are ButterKnife, Dragger2, EventBus, and more

The Java API already provides a framework for scanning source code and parsing annotations, and developers can implement their own annotation parsing logic by inheriting AbstractProcessor classes. The principle of APT is that after annotating some code elements (such as fields, functions, classes, etc.), the compiler will check the subclass of AbstractProcessor at compile time and automatically call its process() method, and then pass all code elements with specified annotations to the method as parameters. The developer then outputs the corresponding Java code at compile time based on the annotation elements

Implement a lightweight ‘ButterKnife’

With ButterKnife as the goal, we walk through Android APT with a step-by-step implementation of a lightweight control binding framework that automatically generates findViewById() code with annotations as shown below

package hello.leavesc.apt;

public class MainActivityViewBinding {
    public static void bind(MainActivity _mainActivity) {
        _mainActivity.btn_serializeSingle = (android.widget.Button) (_mainActivity.findViewById(2131165221));
        _mainActivity.tv_hint = (android.widget.TextView) (_mainActivity.findViewById(2131165333));
        _mainActivity.btn_serializeAll = (android.widget.Button) (_mainActivity.findViewById(2131165220));
        _mainActivity.btn_remove = (android.widget.Button) (_mainActivity.findViewById(2131165219));
        _mainActivity.btn_print = (android.widget.Button) (_mainActivity.findViewById(2131165218));
        _mainActivity.et_userName = (android.widget.EditText) (_mainActivity.findViewById(2131165246));
        _mainActivity.et_userAge = (android.widget.EditText) (_mainActivity.findViewById(2131165245));
        _mainActivity.et_singleUserName = (android.widget.EditText) (_mainActivity.findViewById(2131165244));
        _mainActivity.et_bookName = (android.widget.EditText) (_mainActivity.findViewById(2131165243)); }}Copy the code

The control is bound as follows

    @BindView(R.id.et_userName)
    EditText et_userName;

    @BindView(R.id.et_userAge)
    EditText et_userAge;

    @BindView(R.id.et_bookName)
    EditText et_bookName;
Copy the code

1.1. Establish Module

Start by creating a Java Library in your project named apt_processor that houses AbstractProcessor implementation classes. Create a new Java Library called APt_Annotation to store annotations

The following dependencies need to be imported for apt_processor

dependencies { implementation fileTree(dir: 'libs', include: [' *. Jar ']) implementation 'com. Google. Auto. Services: auto - service: 1.0 rc2' implementation 'com. Squareup: javapoet: 1.10.0' implementation project(':apt_annotation') }Copy the code

JavaPoet is Square’s open source Java code generation framework that makes it easy to generate code in specified formats (modifiers, return values, parameters, function bodies, and so on) through its API. Auto-service is an open-source annotation registration processor from Google

In practice, the above two dependency libraries are not required and can be hardcoded as code generation rules, but they are recommended because the code is more readable and can improve development efficiency

The App Module relies on both Java libraries

    implementation project(':apt_annotation')
    annotationProcessor project(':apt_processor')
Copy the code

This way, all the base dependencies we need are set up

1.2. Write code generation rules

Looking at the auto-generated code first, you can generalize a few things that need to be done:

1. The file and the source Activity are under the same package name

2. The class name consists of the Activity name + ViewBinding

3. The bind() method instantiates an Activity object by passing in the control object it declares, which is why ButterKnife requires that control variables to be bound not be declared private

package hello.leavesc.apt; public class MainActivityViewBinding { public static void bind(MainActivity _mainActivity) { _mainActivity.btn_serializeSingle = (android.widget.Button) (_mainActivity.findViewById(2131165221)); _mainActivity.tv_hint = (android.widget.TextView) (_mainActivity.findViewById(2131165333)); . }}Copy the code

Create the BindViewProcessor class in the apt_Processor Module and inherit the AbstractProcessor class. The abstract class contains an abstract method process (), and an abstract method getSupportedAnnotationTypes () should be implemented by us

/ * * * the author: leavesC * time: 2019/1/3 then * description: * GitHub:https://github.com/leavesC * Blog: https://www.jianshu.com/u/9df45b87cfdf */ @AutoService(Processor.class) public class BindViewProcessor extends AbstractProcessor { @Override public Set<String> getSupportedAnnotationTypes() { Set<String> hashSet = new HashSet<>(); hashSet.add(BindView.class.getCanonicalName()); return hashSet; } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { return false; }}Copy the code

GetSupportedAnnotationTypes () method is used to specify the goal of the AbstractProcessor annotation objects, the process () method is used to handle containing the specified annotation object code elements

The BindView annotation is declared as follows, in apt_annotation, and the annotation value value is used to declare viewId

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}
Copy the code

To automatically generate the findViewById() method, you need to get the reference to the control variable and the corresponding Viewid, so you need to iterate through all the annotation objects contained in each Activity

@Override public boolean process(Set<? Extends TypeElement> set, RoundEnvironment RoundEnvironment) {extends TypeElement> set, RoundEnvironment RoundEnvironment) { extends Element> elementSet = roundEnvironment.getElementsAnnotatedWith(BindView.class); Map<TypeElement, Map<Integer, VariableElement>> typeElementMapHashMap = new HashMap<>(); for (Element element : ElementSet) {// Since BindView is FIELD, VariableElement = (VariableElement) Element; The getEnclosingElement method returns the innermost Element enclosing this Element. It returns the enclosing element / / here said that Activity class objects TypeElement TypeElement = (TypeElement) variableElement. GetEnclosingElement (); Map<Integer, VariableElement> variableElementMap = typeElementMapHashMap.get(typeElement); if (variableElementMap == null) { variableElementMap = new HashMap<>(); typeElementMapHashMap.put(typeElement, variableElementMap); } / / access annotation values, namely the ViewId BindView bindAnnotation = variableElement. GetAnnotation (BindView. Class); int viewId = bindAnnotation.value(); // Save each field object containing the BindView annotation and its annotation value variableElementMap.put(viewId, variableElement); }... return true; }Copy the code

Element is used to represent an Element of a program. This Element can be a package, class, interface, variable, method, etc. The Activity object is used as the Key and the map is used to store all the annotation objects under different activities

Once you have all the annotation objects, you can construct the bind() method

MethodSpec is a JavaPoet concept that abstracts the basic elements needed to be born into a function, and it should be easy to see what it means by looking directly at the following methods

Fill in the required argument elements with the addCode() method and loop through each line of the findView method

/** * @param typeElement annotation object superelement object, That is, the Activity object * @param variableElementMap Activity contains the annotation object and the target object of the annotation * @return */ private MethodSpec generateMethodByPoet(TypeElement typeElement, Map<Integer, VariableElement> variableElementMap) { ClassName className = ClassName.bestGuess(typeElement.getQualifiedName().toString()); / / method parameter name String parameter = "_" + StringUtils. ToLowerCaseFirstChar (className. SimpleName ()); MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(className, parameter); for (int viewId : variableElementMap.keySet()) { VariableElement element = variableElementMap.get(viewId); String name = element.getSimplename ().toString(); // The full name of the object type of the annotated field String type = element.astype ().toString(); String text = "{0}.{1}=({2})({3}.findViewById({4}));" ; methodBuilder.addCode(MessageFormat.format(text, parameter, name, type, parameter, String.valueOf(viewId))); } return methodBuilder.build(); }Copy the code

The complete code declaration is shown below

/ * * * the author: leavesC * time: 2019/1/3 then * description: * GitHub:https://github.com/leavesC * Blog: https://www.jianshu.com/u/9df45b87cfdf */ @AutoService(Processor.class) public class BindViewProcessor extends AbstractProcessor { private Elements elementUtils; @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); elementUtils = processingEnv.getElementUtils(); } @Override public Set<String> getSupportedAnnotationTypes() { Set<String> hashSet = new HashSet<>(); hashSet.add(BindView.class.getCanonicalName()); return hashSet; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public boolean process(Set<? Extends TypeElement> set, RoundEnvironment RoundEnvironment) {extends TypeElement> set, RoundEnvironment RoundEnvironment) { extends Element> elementSet = roundEnvironment.getElementsAnnotatedWith(BindView.class); Map<TypeElement, Map<Integer, VariableElement>> typeElementMapHashMap = new HashMap<>(); for (Element element : ElementSet) {// Since BindView is FIELD, VariableElement = (VariableElement) Element; The getEnclosingElement method returns the innermost Element enclosing this Element. It returns the enclosing element / / here said that Activity class objects TypeElement TypeElement = (TypeElement) variableElement. GetEnclosingElement (); Map<Integer, VariableElement> variableElementMap = typeElementMapHashMap.get(typeElement); if (variableElementMap == null) { variableElementMap = new HashMap<>(); typeElementMapHashMap.put(typeElement, variableElementMap); } / / access annotation values, namely the ViewId BindView bindAnnotation = variableElement. GetAnnotation (BindView. Class); int viewId = bindAnnotation.value(); // Save each field object containing the BindView annotation and its annotation value variableElementMap.put(viewId, variableElement); } for (TypeElement key : typeElementMapHashMap.keySet()) { Map<Integer, VariableElement> elementMap = typeElementMapHashMap.get(key); String packageName = ElementUtils.getPackageName(elementUtils, key); JavaFile javaFile = JavaFile.builder(packageName, generateCodeByPoet(key, elementMap)).build(); try { javaFile.writeTo(processingEnv.getFiler()); } catch (IOException e) { e.printStackTrace(); } } return true; } /** * Generate Java class ** @param typeElement annotation object superelement object, That is, the Activity object * @param variableElementMap Activity contains the annotation object and the annotation target object * @return */ private TypeSpec generateCodeByPoet(TypeElement typeElement, Map<Integer, VariableElement> variableElementMap) {// Automatically generated files are named return with the Activity name + ViewBinding TypeSpec.classBuilder(ElementUtils.getEnclosingClassName(typeElement) + "ViewBinding") .addModifiers(Modifier.PUBLIC) .addMethod(generateMethodByPoet(typeElement, variableElementMap)) .build(); } /** * @param typeElement annotation object superelement object, That is, the Activity object * @param variableElementMap Activity contains the annotation object and the target object of the annotation * @return */ private MethodSpec generateMethodByPoet(TypeElement typeElement, Map<Integer, VariableElement> variableElementMap) { ClassName className = ClassName.bestGuess(typeElement.getQualifiedName().toString()); / / method parameter name String parameter = "_" + StringUtils. ToLowerCaseFirstChar (className. SimpleName ()); MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind") .addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(void.class) .addParameter(className, parameter); for (int viewId : variableElementMap.keySet()) { VariableElement element = variableElementMap.get(viewId); String name = element.getSimplename ().toString(); // The full name of the object type of the annotated field String type = element.astype ().toString(); String text = "{0}.{1}=({2})({3}.findViewById({4}));" ; methodBuilder.addCode(MessageFormat.format(text, parameter, name, type, parameter, String.valueOf(viewId))); } return methodBuilder.build(); }}Copy the code

1.3. Binding effect of annotation

First declare two BindView annotations in the MainActivity, and then Rebuild Project, which causes the compiler to generate the code we need from the BindViewProcessor

public class MainActivity extends AppCompatActivity { @BindView(R.id.tv_hint) TextView tv_hint; @BindView(R.id.btn_hint) Button btn_hint; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); }}Copy the code

Once rebuild is complete, you can see the MainActivityViewBinding class automatically generated in the generatedJava folder

There are two ways to trigger the bind() method at this point

  1. Call the bind() method of the MainActivityViewBinding directly in the MainActivity method
  2. Since the package name path of MainActivityViewBinding is the same as the Activity, reflection can also trigger the bind() method of MainActivityViewBinding
/ * * * the author: leavesC * time: 2019/1/3 number * description: * GitHub:https://github.com/leavesC * Blog: https://www.jianshu.com/u/9df45b87cfdf */ public class ButterKnife { public static void bind(Activity activity) { Class clazz = activity.getClass(); try { Class bindViewClass = Class.forName(clazz.getName() + "ViewBinding"); Method method = bindViewClass.getMethod("bind", activity.getClass()); method.invoke(bindViewClass.newInstance(), activity); } catch (Exception e) { e.printStackTrace(); }}}Copy the code

Both approaches have advantages and disadvantages. The first method generates code after each build project and cannot reference the corresponding ViewBinding class until then. The second option can be fixed method calls, but reflection costs slightly more performance than the first option

But the results are exactly the same

@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); MainActivityViewBinding.bind(this); tv_hint.setText("leavesC Success"); btn_hint.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(MainActivity.this, "Hello", Toast.LENGTH_SHORT).show(); }}); }Copy the code

Object persistence + serialization + deserialization framework

From the first section, the reader should know the powerful functions of APT. This section goes on to implement a framework that makes it easy to persist, serialize, and de-sequence objects

2.1. Identify goals

Typically, our applications have many configuration items that need to be cached, such as user information, Settings switch, server IP address, and so on. Using native SharedPreferences, it’s easy to write ugly code that not only maintains key values for multiple data items, but also has a large amount of code that repeats every time data is saved and retrieved

SharedPreferences sharedPreferences = getSharedPreferences("SharedPreferencesName", Context.MODE_PRIVATE); SharedPreferences.Editor editor = sharedPreferences.edit(); Editor. PutString (" IP ", "192.168.0.1"); editor.commit(); String userName = sharedPreferences.getString("userName", ""); String ip = sharedPreferences.getString("IP", "");Copy the code

Therefore, here is to use APT to achieve a framework that can easily persist data + serialization + deserialization, specific goals are as follows:

1. You can serialize Object and provide methods to deserialize Object

2. The serialized result of Object can be persisted and stored locally

3. The unique key value required for persistent data is automatically maintained within the framework

4. The specific process of serialization, deserialization and persistence is realized outside the framework, which is only responsible for building operation logic

Goal 1 can be achieved by Gson, and Goal 2 can be achieved by using Tencent’s open source MMKV framework. The following two dependencies need to be imported

Implementation 'com. Google. Code. Gson: gson: 2.8.5' implementation 'com. Tencent: MMKV: 1.0.16'Copy the code

2.2 Effect preview

Here’s a preview of how frameworks are used. The new annotation is named Preferences and assumes that three field values in the User class need to be cached locally, so they are annotated with Preferences

public class User { @Preferences private String name; @Preferences private int age; @Preferences private Book book; . }Copy the code

What we need to do is to automatically generate a UserPreferences subclass for the User class through APT, and then the data caching operations are carried out by UserPreferences

Caching the entire object

    User user = new User();
    UserPreferences.get().setUser(user);
Copy the code

Caching individual attribute values

    String userName = et_singleUserName.getText().toString();
    UserPreferences.get().setName(userName);
Copy the code

Gets the cached object

    User user = UserPreferences.get().getUser();
Copy the code

Removes cached objects

    UserPreferences.get().remove();
Copy the code

As you can see, this whole thing is pretty neat, so let’s get started

2.3. Realize the operation interface

To achieve goal 4, the operational interface needs to be defined and the concrete implementation passed in from the outside

Public interface IPreferencesHolder {serialize String serialize(String key, Object SRC); // deserialize <T> T deserialize(String key, Class<T> classOfT); Void remove(String key); }Copy the code

The above three operations should be unique internally to the framework and can therefore be maintained globally through the singleton pattern. APT generates code that calls persistence + serialization + deserialization methods through this entry

public class PreferencesManager {

    private IPreferencesHolder preferencesHolder;

    private PreferencesManager() {
    }

    public static PreferencesManager getInstance() {
        return PreferencesManagerHolder.INSTANCE;
    }

    private static class PreferencesManagerHolder {
        private static PreferencesManager INSTANCE = new PreferencesManager();
    }

    public void setPreferencesHolder(IPreferencesHolder preferencesHolder) {
        this.preferencesHolder = preferencesHolder;
    }

    public IPreferencesHolder getPreferencesHolder() {
        return preferencesHolder;
    }

}
Copy the code

Pass the concrete implementation in the Application’s onCreate() method

 PreferencesManager.getInstance().setPreferencesHolder(new PreferencesMMKVHolder());
Copy the code
public class PreferencesMMKVHolder implements IPreferencesHolder {

    @Override
    public String serialize(String key, Object src) {
        String json = new Gson().toJson(src);
        MMKV kv = MMKV.defaultMMKV();
        kv.putString(key, json);
        return json;
    }

    @Override
    public <T> T deserialize(String key, Class<T> classOfT) {
        MMKV kv = MMKV.defaultMMKV();
        String json = kv.decodeString(key, "");
        if (!TextUtils.isEmpty(json)) {
            return new Gson().fromJson(json, classOfT);
        }
        return null;
    }

    @Override
    public void remove(String key) {
        MMKV kv = MMKV.defaultMMKV();
        kv.remove(key);
    }

}
Copy the code

2.4. Write code generation rules

Again, you need to inherit AbstractProcessor class, subclass named PreferencesProcessor

First, the PreferencesProcessor class needs to generate a method that serializes the entire object. For example, you need to generate a subclass UserPreferences for the User class, which contains a setUser(User instance) method

    public String setUser(User instance) {
        if (instance == null) {
            PreferencesManager.getInstance().getPreferencesHolder().remove(KEY);
            return "";
        }
        return PreferencesManager.getInstance().getPreferencesHolder().serialize(KEY, instance);
    }
Copy the code

The corresponding method generation rules are shown below. As you can see, the general rule is the same as in the first section: you need to concatenate the entire code with strings. Where, L, L, L, and T are substitution characters similar to MessageFormat

/** * construct a method for serializing the entire object ** @param typeElement annotation object superelement object, The Java object * @ return * / private MethodSpec generateSetInstanceMethod (TypeElement TypeElement) {/ / top class name String enclosingClassName = ElementUtils.getEnclosingClassName(typeElement); / / the method name String methodName = "set" + StringUtils. ToUpperCaseFirstChar (enclosingClassName); // Method parameter String fieldName = "instance"; MethodSpec.Builder builder = MethodSpec.methodBuilder(methodName) .addModifiers(Modifier.PUBLIC) .returns(String.class) .addParameter(ClassName.get(typeElement.asType()), fieldName); builder.addStatement("if ($L == null) { $T.getInstance().getPreferencesHolder().remove(KEY); return \"\"; }", fieldName, serializeManagerClass); builder.addStatement("return $T.getInstance().getPreferencesHolder().serialize(KEY, $L)", serializeManagerClass, fieldName); return builder.build(); }Copy the code

In addition, you need a method for deserializing locally cached data

    public User getUser() {
        return PreferencesManager.getInstance().getPreferencesHolder().deserialize(KEY, User.class);
    }
Copy the code

The corresponding method generation rules are shown below

/** * construct the method used to get the entire serialized object ** @param typeElement annotation object superelement object, The Java object * @ return * / private MethodSpec generateGetInstanceMethod (TypeElement TypeElement) {/ / top class name String enclosingClassName = ElementUtils.getEnclosingClassName(typeElement); / / the method name String methodName = "get" + StringUtils. ToUpperCaseFirstChar (enclosingClassName); MethodSpec.Builder builder = MethodSpec.methodBuilder(methodName) .addModifiers(Modifier.PUBLIC) .returns(ClassName.get(typeElement.asType())); builder.addStatement("return $T.getInstance().getPreferencesHolder().deserialize(KEY, $L.class)", serializeManagerClass, enclosingClassName); return builder.build(); }Copy the code

In order to achieve goal 3 (the unique key needed to persist data is automatically maintained within the framework), the key used in persistence is determined by the current package name path + class name, thus ensuring the uniqueness of the key value

For example, the Key used by the UserPreferences class to cache data is

private static final String KEY = "leavesc.hello.apt.model.UserPreferences";
Copy the code

The corresponding method generation rules are shown below

/** * define the Key that the annotation class uses in serialization ** @param typeElement annotation object superelement object, Java object * @return */ private FieldSpec generateKeyField(TypeElement TypeElement) {return FieldSpec.builder(String.class, "KEY") .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) .initializer("\"" + typeElement.getQualifiedName().toString() + SUFFIX + "\"") .build(); }Copy the code

Other corresponding get and set method generation rules are not described, interested students can download the source code to read

2.5. Practical experience

Modify the layout of MainActivity

public class MainActivity extends AppCompatActivity { @BindView(R.id.et_userName) EditText et_userName; @BindView(R.id.et_userAge) EditText et_userAge; ··· @override protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // ButterKnife.bind(this); MainActivityViewBinding.bind(this); btn_serializeAll.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String userName  = et_userName.getText().toString(); String ageStr = et_userAge.getText().toString(); int age = 0; if (! TextUtils.isEmpty(ageStr)) { age = Integer.parseInt(ageStr); } String bookName = et_bookName.getText().toString(); User user = new User(); user.setAge(age); user.setName(userName); Book book = new Book(); book.setName(bookName); user.setBook(book); UserPreferences.get().setUser(user); }}); btn_serializeSingle.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String userName = et_singleUserName.getText().toString(); UserPreferences.get().setName(userName); }}); btn_remove.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { UserPreferences.get().remove(); }}); btn_print.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { User user = UserPreferences.get().getUser(); if (user == null) { tv_hint.setText("null"); } else { tv_hint.setText(user.toString()); }}}); }}Copy the code

The whole process of data access is very simple, do not have to maintain the bloated key table, and can achieve the uniqueness of access path, or can improve some development efficiency

Those interested in seeing the implementation can click on the portal:Android_APT