After JDK 1.5, Java provided support for annotations, which function at run time just like normal code. The JSR-269 specification, implemented in JDK 1.6, provides a standard API for a set of plug-in annotation handlers that process annotations at compile time. It can be thought of as a set of compiler plug-ins that can read/modify/add arbitrary elements in the abstract syntax tree.

The APT technology developed in Android module introduces some knowledge of custom annotation processor. The registration of custom annotation processor can only be called by Java virtual machine. The method used in the fourth section of the blog above is manual registration, which is against the characteristics of lazy programmers. AutoService, and today’s blog post is about taking a look at Google’s open source library.

Let’s take a look at how AutoService is used.

1. Use

Define a simple interface:

public interface Display {
    String display();
}
Copy the code

There are two modules A and B that implement this interface, and then call these two implementation classes in the App Module. The lower level approach is to rely on the two modules directly in the App Module, and then call the implementation classes. This has two disadvantages. One is that app Module directly relies heavily on Module A and Module B. In addition, if you can’t get the dependent Module during development, it may be A third-party Module, so this method of strong dependence won’t work.

To see how AutoService is implemented, let’s look at the package structure. Interfaces simply contain the Display interface above, moduleA and Moduleb implement this interface, and app loads all implementation classes for this interface.

The moduleA and Moduleb implementations simply return a string, mainly the @autoService (display.class) annotation above, whose annotation value is the name of the interface that implements the moduleA and Moduleb classes.

// modulea
import com.google.auto.service.AutoService;

@AutoService(Display.class)
public class ADisplay implements Display{
    @Override
    public String display() {
        return "A Display";
    }
}

// moduleb
@AutoService(Display.class)
public class BDisplay implements Display {
    @Override
    public String display() {
        return "B Display"; }}Copy the code

If the ServiceLoader is used to load the Display interface, you can get all the implementation classes of the Display interface. In our case, the two implementors are ADisplay and BDisplay. DisplayFactory gets all the implementation classes by getDisplay.

import com.example.juexingzhe.interfaces.Display;

import java.util.Iterator;
import java.util.ServiceLoader;

public class DisplayFactory {
    private static DisplayFactory mDisplayFactory;

    private Iterator<Display> mIterator;

    private DisplayFactory() {
        ServiceLoader<Display> loader = ServiceLoader.load(Display.class);
        mIterator = loader.iterator();
    }

    public static DisplayFactory getSingleton() {
        if (null == mDisplayFactory) {
            synchronized (DisplayFactory.class) {
                if(null == mDisplayFactory) { mDisplayFactory = new DisplayFactory(); }}}return mDisplayFactory;
    }

    public Display getDisplay() {
        return mIterator.next();
    }

    public boolean hasNextDisplay() {
        returnmIterator.hasNext(); }}Copy the code

Use is such a few steps, relatively simple, the following look at the implementation principle of AutoService.

2. Implementation principle

First of all, a brief introduction to the compilation process of Javac can be roughly divided into three processes:

  • Parse and populate symbol tables
  • Annotation processing by the plug-in annotation processor
  • Analysis and bytecode generation process

Let’s take a look at the next image, which is from the Java virtual machine. First, we will do lexical and syntax analysis. Lexical analysis turns the character flow of the source code into Token sets, where keywords/variable names/literals/operator reads can become tokens. Lexical analysis process by com. Sun. View javac. ParserScanner class implements;

Syntax analysis is based on Token sequence construction process of the abstract syntax tree, the abstract syntax tree AST is a kind of used to describe the program code syntax structure of tree, said the syntax tree each node represents a read a syntax structure of program code, such as package/type/modifier/operator/interface/return value/code comments, etc., in the source of javac, Syntax analysis is made of com. Sun. View javac. Parser. The parser class implements, output at this stage of the abstract syntax tree by com. Sun. View javac. Tree. JCTree said. After these two steps, the compiler basically does not operate on the source file, and the subsequent operation reads are based on the abstract syntax tree.

After completing the grammar and lexical analysis is the process of populating the symbol table. A symbol table is a table consisting of a set of symbol addresses and symbol information. Fill in the symbol table process by com.sun.tools.javac.com p.E nter class implements.

As mentioned earlier, if the annotation processor makes changes to the syntax tree during annotation processing, the compiler will go back to parsing and populating the symbol table and reprocess it until no more changes have been made to the syntax tree by any of the plug-in annotation processors. Each loop is called a Round, as shown in the figure below.

After a brief review of compiling annotations, let’s look at the implementation of the AutoService annotation, which has three qualifications;

  • Cannot be inner class and anonymous class, must have a definite name
  • There must be a common, callable, parameterless constructor
  • Classes that use this annotation must implement the interface defined by the value parameter
@Documented @Target(TYPE) public @interface AutoService { /** Returns the interface implemented by this service provider. */ Class<? > value(); }Copy the code

There are annotations, you have to have a corresponding annotation handler, AutoServiceProcessor inheritance AbstractProcessor, normally we will achieve one of the three methods, support in getSupportedAnnotationTypes returned to the annotation type AutoService. Class; GetSupportedSourceVersion, support is used to specify the Java version, generally we are all support to the latest version, so direct return SourceVersion. LatestSupported (); The main thing is the process method.

public class AutoServiceProcessor extends AbstractProcessor {

   @Override
   public SourceVersion getSupportedSourceVersion() {
       return SourceVersion.latestSupported();
   }

   @Override
   public Set<String> getSupportedAnnotationTypes() {
     return ImmutableSet.of(AutoService.class.getName());
   }

   @Override
   public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
     try {
     return processImpl(annotations, roundEnv);
   } catch (Exception e) {
     // We don't allow exceptions of any kind to propagate to the compiler StringWriter writer = new StringWriter(); e.printStackTrace(new PrintWriter(writer)); fatalError(writer.toString()); return true; }}}Copy the code

MEATA_INF is generated by calling generateConfigFiles if the annotation handler has been processed in the previous loop. Call processAnnotations to handle annotations if there was no processing in the previous round. Returning true changes or generates the syntax tree; Return false to inform the compiler that the code in the Round has not changed.

  private boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    if (roundEnv.processingOver()) {
      generateConfigFiles();
    } else {
      processAnnotations(annotations, roundEnv);
    }

    return true;
  }
Copy the code

Before moving on to the code, take a look at two environment variables, RoundEnvironment and ProcessingEnvironment.

RoundEnvironment provides access to the syntax tree nodes in the current Round. Each syntax tree node is represented here as an Element. The javax.lang.model package defines 16 elements, including common elements: Packages, enumerations, classes, annotations, interfaces, enumerated values, fields, parameters, local variables, exceptions, methods, constructors, static statement blocks (static{} blocks), instance statements ({} blocks), parameterized types (reflection Angle brackets), and undefined other syntax tree nodes.

public enum ElementKind {
    PACKAGE,
    ENUM,
    CLASS,
    ANNOTATION_TYPE,
    INTERFACE,
    ENUM_CONSTANT,
    FIELD,
    PARAMETER,
    LOCAL_VARIABLE,
    EXCEPTION_PARAMETER,
    METHOD,
    CONSTRUCTOR,
    STATIC_INIT,
    INSTANCE_INIT,
    TYPE_PARAMETER,
    OTHER,
    RESOURCE_VARIABLE;

    private ElementKind() {
    }

    public boolean isClass() {
        return this == CLASS || this == ENUM;
    }

    public boolean isInterface() {
        return this == INTERFACE || this == ANNOTATION_TYPE;
    }

    public boolean isField() {
        returnthis == FIELD || this == ENUM_CONSTANT; }}Copy the code

Take a look at the source code for RoundEnvironment. The errorRaised method returns whether the last annotation handler generated an error. GetRootElements returns the root element generated by the previous round of annotation handlers; The last two methods return a collection of elements containing the specified annotation type, highlighting, and this is the method that our custom annotation handler will often deal with.

public interface RoundEnvironment {
    boolean processingOver();

    boolean errorRaised();

    Set<? extends Element> getRootElements();

    Set<? extends Element> getElementsAnnotatedWith(TypeElement var1);

    Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> var1);
}

Copy the code

Another parameter, ProcessingEnvironment, is created at the time of annotation handler initialization (when the init() method is executed) and represents a context provided by the annotation handler framework that is needed to create new code or output information to the compiler or retrieve other utility classes. Take a look at its source code.

  • MessagerUsed to report errors, warnings, and other prompts;
  • FilerUsed to create new source files, class files, and auxiliary files;
  • ElementsContains utility methods for manipulating Element;
  • TypesContains utility methods for manipulating the type TypeMirror;
public interface ProcessingEnvironment {
    Map<String, String> getOptions();

    Messager getMessager();

    Filer getFiler();

    Elements getElementUtils();

    Types getTypeUtils();

    SourceVersion getSourceVersion();

    Locale getLocale();
}
Copy the code

Now that we’ve covered some basic variables, let’s move on to the processAnnotations method, which seems a little long, but is very simple, The first step is to get all the elements annotated with the AutoService annotation through the RoundEnvironment’s getElementsAnnotatedWith(AutoService.class).

  private void processAnnotations(Set<? extends TypeElement> annotations,
      RoundEnvironment roundEnv) {

   // 1.
    Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(AutoService.class);

    log(annotations.toString());
    log(elements.toString());

    for (Element e : elements) {
      // TODO(gak): check for error trees?
      // 2.
      TypeElement providerImplementer = (TypeElement) e;
      // 3.
      AnnotationMirror providerAnnotation = getAnnotationMirror(e, AutoService.class).get();
      // 4.
      DeclaredType providerInterface = getProviderInterface(providerAnnotation);
      TypeElement providerType = (TypeElement) providerInterface.asElement();

      log("provider interface: " + providerType.getQualifiedName());
      log("provider implementer: "+ providerImplementer.getQualifiedName()); / / 5.if(! checkImplementer(providerImplementer, providerType)) { String message ="ServiceProviders must implement their service provider interface. "
            + providerImplementer.getQualifiedName() + " does not implement "
            + providerType.getQualifiedName();
        error(message, e, providerAnnotation);
      }

      // 6.
      String providerTypeName = getBinaryName(providerType);
      String providerImplementerName = getBinaryName(providerImplementer);
      log("provider interface binary name: " + providerTypeName);
      log("provider implementer binary name: " + providerImplementerName);

      providers.put(providerTypeName, providerImplementerName);
    }
  }

  public static Optional<AnnotationMirror> getAnnotationMirror(Element element,
      Class<? extends Annotation> annotationClass) {
    String annotationClassName = annotationClass.getCanonicalName();
    for (AnnotationMirror annotationMirror : element.getAnnotationMirrors()) {
      TypeElement annotationTypeElement = asType(annotationMirror.getAnnotationType().asElement());
      if (annotationTypeElement.getQualifiedName().contentEquals(annotationClassName)) {
        returnOptional.of(annotationMirror); }}return Optional.absent();
  }
Copy the code

AutoService can only work on non-internal non-anonymous classes or interfaces. The second step in the for loop is to force Element to TypeElement, which is the Element marked by AutoService (T). This may get confusing, but I mentioned earlier that each javAC is a circular process. When the AutoService annotation is first scanned, there is no T class object, so there is no reflection to get the annotation and its parameter value. The third step is called the AnnotationMirror, which represents an annotation and gets the annotation type and annotation parameters. Annotations in getAnnotationMirror will determine the T (by element. GetAnnotationMirrors () name is equal to AutoService, equal return this AutoService AnnotationMirror.

public interface AnnotationMirror {
    DeclaredType getAnnotationType();

    Map<? extends ExecutableElement, ? extends AnnotationValue> getElementValues();
}
Copy the code

Now that you have the annotation, the next step is to get the annotation’s parameter value, which is done in step 4 in the getProviderInterface method.

  private DeclaredType getProviderInterface(AnnotationMirror providerAnnotation) {

    Map<? extends ExecutableElement, ? extends AnnotationValue> valueIndex =
        providerAnnotation.getElementValues();
    log("annotation values: " + valueIndex);

    AnnotationValue value = valueIndex.values().iterator().next();
    return (DeclaredType) value.getValue();
  }
Copy the code

For the same reason as above, it is not possible to get the parameter value of the annotation through the following code reflection at this stage, because the class object is not available at this stage. So it takes a lot of trouble to get annotation values via AnnotationMirror, which in our case is display.class.

AutoService autoservice = e.getAnnotation(AutoService.class); Class<? > providerInterface = autoservice.value()Copy the code

The next step is to check whether type T implements the interface specified by the annotation parameter values, that is, whether ADisplay and BDisplay implement the Display interface. Step 6 is to get the interface name and implementation class name and register them in the map, similar to the form map

, i.e. key is the interface name and value is the implementation class that implements the interface, i.e. annotating the AutoService annotation.
,>

After the above steps have scanned all the AutoService annotated implementation classes and the corresponding interface mapping, and returned true in processImpl, the next Round is to generate the configuration file. Take a look at the generateConfigFiles method in the processImpl if branch.

  private void generateConfigFiles() { Filer filer = processingEnv.getFiler(); / / 1.for (String providerInterface : providers.keySet()) {
      String resourceFile = "META-INF/services/" + providerInterface;
      log("Working on resource file: " + resourceFile);
      try {
        SortedSet<String> allServices = Sets.newTreeSet();
        try {
          // 2.
          FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",
              resourceFile);
          log("Looking for existing resource file at " + existingFile.toUri());
          // 3.
          Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
          log("Existing service entries: " + oldServices);
          allServices.addAll(oldServices);
        } catch (IOException e) {
          // According to the javadoc, Filer.getResource throws an exception
          // if the file doesn't already exist. In practice this doesn't
          // appear to be the case.  Filer.getResource will happily return a
          // FileObject that refers to a non-existent file but will throw
          // IOException if you try to open an input stream for it.
          log("Resource file did not already exist.");
        }

        // 4.
        Set<String> newServices = new HashSet<String>(providers.get(providerInterface));
        if (allServices.containsAll(newServices)) {
          log("No new service entries being added.");
          return;
        }

        allServices.addAll(newServices);
        log("New service file contents: " + allServices);
        
        // 5.
        FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
            resourceFile);
        OutputStream out = fileObject.openOutputStream();
        ServicesFiles.writeServiceFile(allServices, out);
        out.close();
        log("Wrote to: " + fileObject.toUri());
      } catch (IOException e) {
        fatalError("Unable to create " + resourceFile + "," + e);
        return; }}}Copy the code

There are 5 steps to generate a configuration file.

  • The first step to traverse the mapping relation map will get above, we here is com. Example. Juexingzhe. Interfaces. The Display, then generate the file name resourceFile =META-INF/services/com.example.juexingzhe.interfaces.Display
  • The second step is to check if there is a configuration file in the output of class compilation.
  • The third step is to save the interface and all implementation classes to the configuration file in step 2allServicesIn the
  • Step 4 CheckprocessAnnotationsMethod output map map does not exist aboveallServicesIf the file does not exist, add the file. If the file does exist, return the file without generating a new file
  • The fifth step is to generate the configuration file through FilerresourceFileThe content of the file isallServicesAll implementation classes in.

Finally, let’s look at the result of compiling. In each module, configuration files are generated

Finally, all meta-INF file directories are merged in apK. You can see that the interface file Display contains all implementation classes in module annotated by AutoService annotation.

Finally, the ServiceLoader can be used to get all the implementation classes through reflection. The source code analysis of ServiceLoader can be found in SPI, another Android module developed in my blog.

3. Summary

This source code analysis is actually completed before the Android module development APT technology flag, until today to make up a little ashamed. AutoService source parsing is best done with APT and SPI.

This is also a common technical point in componentized technology, and we will update some knowledge about componentized Gradle later, if you need it, welcome to pay attention.

To the end.

Reference: Understanding the Java Virtual Machine in depth