An overview of the

APT(Annotation Processing Tool), also known as compile-time Annotation, compile-time code generation), is a Tool for Processing annotations. Specifically, it is a javAC Tool that scans and processes annotations at compile time. The annotation processor takes Java code (or compiled bytecode) as input and generates a. Java file as output. This is simply a.java file generated by annotations at compile time.

Using compile-time annotations properly can greatly improve development efficiency and avoid writing repetitive, error-prone code. Annotations can replace Java reflection most of the time at compile time, using code that can be called directly instead of reflection, which is a huge increase in runtime efficiency.

So here’s the question:

  • What are annotations?
  • What are runtime annotations?
  • What are compile-time annotations? APT?

Annotations follow this article, which takes you through compile-time annotations in the following table of contents:

  • What are annotations

  • Simple use of runtime annotations – Implement automatic injection of ContentView

  • Explore the core principles behind Retrofit, the iconic framework for runtime annotations

  • What are compile-time annotations

  • Compile – time annotation project practice – implement automatic subclass generation

  • Explore the core principles of common third-party frameworks

    • Discover the core of ButterKnife
    • Explore the core principle of Dagger
    • Explore the core principles of EventBus
    • Discover ARouter’s core principles
    • Explore the core principles of Room
  • conclusion

What are annotations

Generally, we judge someone by saying, this is a good person, a bad person, a god, a goddess, a god, a single person, etc. These are artificial labels that we attach to help us or others to obtain basic information about the person being evaluated.

In software development, we can also give some classes, some fields a similar label, the name of this label is called annotations, but this label is for the code to see.

For example, Xiao Zhang is labeled as a stingy person, so Xiao Hong thinks Xiao Zhang is a stingy person, but Xiao Zhang will not change himself because of this label. Maybe Xiao Zhang is a generous person in nature.

Therefore, annotations themselves do not affect the operation of the code itself. They are only useful for specific code. The code used to process annotations is called THE Annotation Processing Tool (APT).

1.1 Example Notes

You are no doubt familiar with annotations. Here are our most common ones:

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
}
Copy the code
@IntDef({ AppBootType.COLD_BOOT, AppBootType.HOT_BOOT, })
@Retention(RetentionPolicy.SOURCE)
public @interface AppBootType {
    int COLD_BOOT = 0;
    int HOT_BOOT = 1;
}
Copy the code

1.2 Annotation Classification

First, annotations fall into three categories:

  • Standard annotations

    Override, Deprecated and SuppressWarnings are Java annotations. They are identified by the compiler and will not be compiled and will not affect code operation. Their meanings are not the focus of this sharing and will not be described here.

  • Yuan the Annotation

    @Retention, @target, @Inherited, @documented, these are the annotations that define annotations. That is, we need to use them when we want to customize annotations.

  • The custom Annotation

    Custom annotations as needed. And the custom way, we’ll talk about that.

Similarly, custom annotations fall into three categories, defined by meta-annotation-@retention:

  • @Retention(RetentionPolicy.SOURCE)

    Source code annotations, commonly used as compiler tags. Such as Override, Deprecated, SuppressWarnings.

  • @Retention(RetentionPolicy.RUNTIME)

    Runtime annotations, which are identified by reflection at runtime.

  • @Retention(RetentionPolicy.CLASS)

    Compile-time annotations, which are identified and processed at compile time, are the focus of this chapter.

Simple use of runtime annotations – implement automatic injection of ContentView

The essence of runtime annotations is that you mark them up in your code with annotations, and the runtime does some processing by looking for them through reflection. Runtime annotations have long been plagued by reflex inefficiency.

2.1 Runtime annotations show Demo overview

The following is just a Demo. Let’s not talk about which one is better or worse, let’s just talk about the technology and not the requirements. The Demo is designed to annotate layout files.

Previously we set up the layout file like this:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_home);
}
Copy the code

With annotations, we can set up the layout like this

@ContentView(R.layout.activity_home)
public class HomeActivity extends BaseActivity {... }Copy the code

So how does this annotation work? That’s easy. Look down.

2.2 Creating an annotation

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface ContentView {
    int value(a);
}
Copy the code

The first line: @ Retention (RetentionPolicy. RUNTIME)

@retention specifies what type of annotation this is (i.e. how long the annotation is retained). This indicates that the annotation is a runtime annotation. This way APT knows when to process the annotation.

Line 2: @target ({elementtype.type})

The @target is used to indicate where the annotation can be used. For example: classes, methods, properties, interfaces, and so on. Elementtype. TYPE indicates that this annotation can be used to modify: Class, interface, or enum declaration. When you modify a method with a ContentView, the compiler prompts an error.

Line 3: Public @interface ContentView

Interface doesn’t mean ContentView is an interface. Just like declaring a class with the keyword class. Declare enumeration with enum. So the annotation is at sign interface. (It is worth noting that in the classification of ElementType, class, interface, Annotation and enum all belong to the same category as Type, and from the official Annotation, it seems that interface contains @interface.)

/** Class, interface (including annotation type), or enum declaration */
    TYPE,
Copy the code

Line 4: int value();

The return value indicates what type of value can be stored in the annotation. For example, this is how we use it

@ContentView(R.layout.activity_home)
Copy the code

R.layout.activity_home is essentially an int id. If used this way, an error will be reported:

@ ContentView (" string ")
Copy the code

2.3 Annotation Analysis

Annotation statement is done, but how do you recognize and use this annotation?

@ContentView(R.layout.activity_home)
public class HomeActivity extends BaseActivity {... }Copy the code

Annotation parsing is in BaseActivity. Let’s take a look at the BaseActivity code

public class BaseActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // Annotation parsing
    for (Class c = this.getClass(); c ! = Context.class; c = c.getSuperclass()) { ContentView annotation = (ContentView) c.getAnnotation(ContentView.class);if(annotation ! =null) {
            try {
                this.setContentView(annotation.value());
            } catch (RuntimeException e) {
                e.printStackTrace();
            }
            return; }}}Copy the code

Step 1: Iterate through all the subclasses

Step 2: Find the class that decorates the annotation ContentView

Step 3: Get the property value of the ContentView.

Step 4: Set the layout for your Activity.

2.4 summarize

You now have some understanding of the use of runtime annotations. We also know where the runtime annotations are sick.

You might think that *setContentView(r.layout.activity_HOME) is no different from @ContentView(R.layout.activity_home)*, and that using annotations adds to the performance problem.

But remember, this is just the simplest application of annotations.

Explore the core principles behind Retrofit, the iconic framework for runtime annotations

3.1 Brief review of Retrofit usage

Go here, directly refer to Retrofit the official document: (square. The dead simple. IO/Retrofit /)

The core steps are:

  1. Creating a Network Interface
  2. Create a Retrofit facade class object and create an instance of the network interface through a dynamic proxy
  3. Call the method of the instance of the network interface to get the network data back

3.2 Analysis of core principles of Retrofit

With Retrofit, it’s easy to access the web without having to worry about the underlying implementation. Retrofit decouages you from the underlying framework. Callers don’t use the underlying implementation of the relational framework (OkHttp), and it’s easy to switch to the underlying web engine in the future.

Let’s start with Retrofit’s Create approach:

  @SuppressWarnings("unchecked") // Single-interface proxy creation guarded by parameter safety.
  public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {
      eagerlyValidateMethods(service);
    }
    return (T) Proxy.newProxyInstance(service.getClassLoader(), newClass<? >[] { service },new InvocationHandler() {
          private final Platform platform = Platform.get();

          @Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
              throws Throwable {
            // If the method is a method from Object then defer to normal invocation.
            if (method.getDeclaringClass() == Object.class) {
              return method.invoke(this, args);
            }
            if (platform.isDefaultMethod(method)) {
              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            ServiceMethod<Object, Object> serviceMethod =
                (ServiceMethod<Object, Object>) loadServiceMethod(method);
            OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
            returnserviceMethod.adapt(okHttpCall); }}); }Copy the code

You can see that an instance object of the network interface is created as a dynamic proxy. The invoke method of anonymous InvocationHandler is invoked when we invoke the network request method.

The Invoke method parses the method passed in through loadServiceMethod into a serviceMethod object, which is then wrapped into an okHttpCall object and eventually adapted into different return types through Adapter.

Let’s focus on the loadServiceMethod method:

ServiceMethod<? ,? > loadServiceMethod(Method method) { ServiceMethod<? ,? > result = serviceMethodCache.get(method);if(result ! =null) return result;

    synchronized (serviceMethodCache) {
      result = serviceMethodCache.get(method);
      if (result == null) {
        result = new ServiceMethod.Builder<>(this, method).build(); serviceMethodCache.put(method, result); }}return result;
  }
Copy the code

Focus on the build method:

    public ServiceMethod build(a) {
      callAdapter = createCallAdapter();
      responseType = callAdapter.responseType();
      if (responseType == Response.class || responseType == okhttp3.Response.class) {
        throw methodError("'"
            + Utils.getRawType(responseType).getName()
            + "' is not a valid response body type. Did you mean ResponseBody?");
      }
      responseConverter = createResponseConverter();

      for (Annotation annotation : methodAnnotations) {
        parseMethodAnnotation(annotation);
      }

      if (httpMethod == null) {
        throw methodError("HTTP method annotation is required (e.g., @GET, @POST, etc.).");
      }

      if(! hasBody) {if (isMultipart) {
          throw methodError(
              "Multipart can only be specified on HTTP methods with request body (e.g., @POST).");
        }
        if (isFormEncoded) {
          throw methodError("FormUrlEncoded can only be specified on HTTP methods with "
              + "request body (e.g., @POST)."); }}int parameterCount = parameterAnnotationsArray.length;
      parameterHandlers = newParameterHandler<? >[parameterCount];for (int p = 0; p < parameterCount; p++) {
        Type parameterType = parameterTypes[p];
        if (Utils.hasUnresolvableType(parameterType)) {
          throw parameterError(p, "Parameter type must not include a type variable or wildcard: %s",
              parameterType);
        }

        Annotation[] parameterAnnotations = parameterAnnotationsArray[p];
        if (parameterAnnotations == null) {
          throw parameterError(p, "No Retrofit annotation found.");
        }

        parameterHandlers[p] = parseParameter(p, parameterType, parameterAnnotations);
      }

      if (relativeUrl == null && !gotUrl) {
        throw methodError("Missing either @%s URL or @Url parameter.", httpMethod);
      }
      if(! isFormEncoded && ! isMultipart && ! hasBody && gotBody) {throw methodError("Non-body HTTP method cannot contain @Body.");
      }
      if(isFormEncoded && ! gotField) {throw methodError("Form-encoded method must contain at least one @Field.");
      }
      if(isMultipart && ! gotPart) {throw methodError("Multipart method must contain at least one @Part.");
      }

      return new ServiceMethod<>(this);
    }
Copy the code

You can see that the build method handles all the annotations above the Method, such as parsing POST or GET requests. This is why we can implement complex network request functionality with simple annotations.

4 What are compile-time annotations

Now that you have a general understanding of runtime annotations, runtime annotations are easy to understand and can be retrieved by reflection as code runs.

According to the previous annotation classification, compile-time annotations are retained at compile time, i.e., the.class file, and are not retained in dex, i.e., they are not available at runtime, so what is the use of compile-time annotations?

Let’s review what compile-time annotations are:

As mentioned above, APT(Annotation Processing Tool) is an Annotation Processing Tool (also known as compile-time Annotation, compile-time code generation). It is a javAC Tool that scans and processes annotations at compile time. The annotation processor takes Java code (or compiled bytecode) as input and generates a. Java file as output. This is simply a.java file generated by annotations at compile time.

From the above explanation, we know that annotations are usually used in conjunction with annotation processors at compile time, by parsing annotations, retrieving information from annotations, and then generating code to help us write our own code. This is a bit abstract, but let’s use a simple example to illustrate.

Compile – time annotation project combat – achieve automatic generation of subclasses

5.1 Example Overview

Wechat sharing requires us to build a WxAPI package under the application package, and then put a WXEntryActivity. This forced addition of a top-level package is really unacceptable for people with OBSessive-compulsive disorder. So can we use APT technology, dynamically generate this WXEntryActivity? The answer, of course, is yes.

The final result is as follows:

@SubTypeAutoGenerate("com.flyme.videoclips.wxapi.WXEntryActivity")
public class BaseWXEntryActivity extends Activity implements IWXAPIEventHandler {... }Copy the code

The generated code looks like this:

// Automatically generated code, please do not change
package com.flyme.videoclips.wxapi;

import com.flyme.videoclips.util.wxapi.BaseWXEntryActivity;

public class WXEntryActivity extends BaseWXEntryActivity {}Copy the code

We can easily generate WXEntryActivity with an @SubTypeAutoGenerate annotation, where the argument to the @SubTypeAutoGenerate annotation is the generated full class name.

5.2 Project Architecture

Annotations the Lib-Compiler Java project stores annotations

The Android main project introduces the annotation project and annotation handler in the following way:

implementation project(path: ':lib-annotations')
kapt project(path: ':lib-compiler')
Copy the code

Annotation projects are then introduced into the annotation processor, usually along with AutoService and JavaPoet (for code generation).

    implementation project(path: ':lib-annotations')
    implementation "com.google.auto.service:auto-service:$versions.auto_service"
    kapt "com.google.auto.service:auto-service:$versions.auto_service"
    implementation "com.squareup:javapoet:$versions.javapoet"
Copy the code

5.3 Implementation

As we know from the above requirements, we need to define an annotation to be used by the Android main project and processor:

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface SubTypeAutoGenerate {
    String value(a) default "";
}
Copy the code

Here we define a compile-time annotation that can only be used on the type. The value of the annotation is the full class name of the class we want to generate.

Now we need to implement the annotation handler. Here we wrap a base class:

public abstract class BaseProcessor extends AbstractProcessor {
    // Code generation related utility classes
    protected Filer mFiler;
    // Print the relevant utility classes
    protected Messager mMessager;
    // Elements is the tool class associated with the operation
    protected Elements mElementUtils;

    // Init method, where you can get some utility classes
    @Override
    public final synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();
        mMessager = processingEnvironment.getMessager();
        mElementUtils = processingEnvironment.getElementUtils();
    }

    // Specify the supported source version
    @Override
    public final SourceVersion getSupportedSourceVersion(a) {
        return SourceVersion.latestSupported();
    }
    
    // Specify supported annotation types
    @Override
    public Set<String> getSupportedAnnotationTypes(a) {
        Set<String> types = new LinkedHashSet<>();
        for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
            types.add(annotation.getCanonicalName());
        }
        return types;
    }

    protected abstract Set<Class<? extends Annotation>> getSupportedAnnotations();

}
Copy the code

The core responsibility of the annotation processor is to scan annotations and generate code based on the scanned information, as shown in the following example:

// This @autoService annotation saves some of the annotation processor configuration
@AutoService(Processor.class)
public class SubTypeAutoGenerateProcessor extends BaseProcessor {

    @Override
    protected Set<Class<? extends Annotation>> getSupportedAnnotations() {
        Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
        annotations.add(SubTypeAutoGenerate.class);
        return annotations;
    }
    
    // The core method of the annotation processor
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        // Get the Element marked with an annotation
        Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(SubTypeAutoGenerate.class);
        for (Element element : elements) {
            if(! (elementinstanceof TypeElement)) {
                continue;
            }
            // Force TypeElement
            TypeElement typeElement = (TypeElement) element;

            // Get the full class name, package name, and simple class name to be generated
            SubTypeAutoGenerate subTypeAutoGenerate = typeElement.getAnnotation(SubTypeAutoGenerate.class);
            String subTypeQualifiedName = subTypeAutoGenerate.value();
            String subTypePackage = Utils.getPackage(subTypeQualifiedName);
            String subTypeSimpleName = Utils.getSimpleName(subTypeQualifiedName);
    
            // Use JavaPoet apis to generate code. You can also use StringBuilder directly to concatenate code
            TypeSpec.Builder classBuilder = TypeSpec.classBuilder(subTypeSimpleName)
                    .addModifiers(Modifier.PUBLIC)
                    .superclass(ClassName.get(typeElement));
            try {
                JavaFile.builder(subTypePackage, classBuilder.build())
                        .addFileComment(Constant.COMMON_COMMENT)
                        .build()
                        .writeTo(mFiler);
            } catch(Exception e) { e.printStackTrace(); }}return false; }}Copy the code

Attach the code for the utility class:

public class Utils {

    public static String getPackage(String qualifiedName) {
        return qualifiedName.substring(0, qualifiedName.lastIndexOf("."));
    }

    public static String getSimpleName(String qualifiedName) {
        return qualifiedName.substring(qualifiedName.lastIndexOf(".") + 1); }}Copy the code

By compiling the project, you can see the generated WXEntryActivity

6 Explore the core principles of common third-party frameworks

  • Explore ButterKnife
  • Explore Dagger
  • Explore EventBus
  • Explore ARouter
  • Inside the Room

6.1 Explore the core principle of ButterKnife

ButterKnife is a view binding framework with the following core principles:

  1. Add the BindView annotation to the View member
  2. The annotation handler scans the BindView annotation and generates a helper class xxx_ViewBinding for each annotated class. The constructor of this class will inject XXX properties
  3. Call butterknip.bind (this) internally to construct an xxx_ViewBinding by reflection to complete the injection

6.2 Explore the core principle of Dagger

Dagger is a framework for implementing IOC via dependency injection. The core principle is:

  1. Annotate @inject to mark the injected property and generate the injection class xxx_MembersInjector to help Inject the property of this class
  2. Mark a dependency provider with the @Inject annotation constructor or with the @Module annotation, corresponding to the generated injection class xxx_Factory, to help provide dependencies
  3. The @Component annotation Bridges the injected property with the dependent provider, generating the bridge class DaggerXxxComponent
  4. Finally, dependency injection can be done by calling the Component injection method generated by APT

6.3 Discovering the Core principles of EventBus

EventBus is a very useful EventBus framework. Its core principles are:

Earlier version (Reflection scheme) :

  1. The event receiving method is defined through the runtime annotation @SUBSCRIBE
  2. By registering an observer with the Register method, all methods containing the @SUBSCRIBE annotation are reflected internally and encapsulated to the observer Map collection
  3. By sending an event through the POST method, EventBus iterates through the observer Map collection looking for eligible observers for callback
  4. Removing the observer by unRegister removes the object from the observer Map collection to prevent memory leaks

Latest version (APT scheme) :

  1. In version 3.0, EventBus uses an annotation handler to process the information contained in the @Subscribe() annotation at compile time by scanning it and parsing it, and then generating Java classes to hold all the subscriber information about the subscription, which is faster than using reflection to get the subscriber information at run time
  2. In The EventBus Builder, you can add it via addIndex, so you don’t need to reflect when looking for subscribers
  3. The rest is the same as above

EventBus 3.0 source code analysis

6.4 Explore the core principles of ARouter

ARouter is a very useful routing framework for componentized development. Its core principles are:

  1. Mark activities/fragments with route annotations, etc
  2. The annotation processor scans routing annotations to generate routing table injection classes
  3. The route injection method of the routing table injection class is constructed and called by reflection when ARouter is initialized
  4. With a routing table, you can easily complete the routing function

6.5 Explore the core principles of Room

Room is a database framework provided in Google JetPack. Its core principles are:

  1. Define a database operation interface or abstract class, marked with @DAO
  2. Define the Database facade with the @database tag
  3. The annotation processor scans these annotations, generates implementation classes for Dao and Database, and performs Database operations
  4. Reflection generates an instance of the Database through the build method of RoomDatabase.Builder

conclusion

Through this article, I believe you have already understood most of the knowledge needed to master annotations. Since the focus of this article is APT, we will summarize APT at last

APT advantages

  • Code is tagged, information is gathered at compile time, and processing is done.
  • Generate a set of independent code to help the code run
  • The controllability of the generated code location (code can be generated at any package location), which is more closely related to the original code
  • More reliable automated code generation
  • Automatically generated code can be as simple as possible, increasing operational efficiency without worrying about writing efficiency

APT shortcomings

  • APT is often misunderstood that code insertion can be implemented. However, APT and code insertion are fundamentally different. Code insertion is modifying existing code
  • APT generates code automatically, but requires active invocation at run time
  • APT code is generated in the Build directory and can only be operated at run time through interfaces (with reflection) and so on. This means that the generated code must have a fixed set of templates

APT Application Scenarios

  • When a scenario requires a lot of repetitive code, consider APT for optimization
  • For some scenarios that use reflection, consider APT for optimization
  • For example, a lot of findViewById, database operation code

Refer to the article

Android compile time annotation framework series easy to learn, I heard that you do not understand the Dagger2 reference source address