Most of you know that there is an important JAR in the JDK: tools.jar, but few of you know what’s really interesting in this package.

Javac entry and compilation process

When you use the javac command to compile the source file, you actually execute the com.sun.tools.javac. main #main method. And really do compilation, is com. Sun. View javac. Main. JavaCompiler class.

The compilation process of JavAC is divided into the following stages:

  • Parse and populate symbol table processing.
  • Plug-in annotation processing the annotation processing process.
  • Analysis and bytecode generation process.

The above processes would look like this one (from openJDK) :

This corresponds to the complie method in the JavaCompiler class mentioned above.

/** * Main method: compile a list of files, return all compiled classes * ... */ public void compile(Collection<JavaFileObject> sourceFileObjects, Collection<String> classnames, Iterable<? extends Processor> processors, Collection<String> addModules) { ... // Initialize the plugged-annotation handler initProcessAnnotations(Processors, sourceFileObjects, classnames); . // These method calls must be chained to avoid memory leaks processAnnotations(// process 2: Input to the symbol table stopIfError(compilestate. PARSE, initModules(stopIfError(compilestate. PARSE, ParseFiles (sourceFileObjects)))) // Process 1.1: lexical analysis, syntax analysis), classnames); . switch (compilePolicy) { ... Case BY_TODO: // Procedure 3: analysis and bytecode generation while (! Todo.isempty ()) generate(// Process 3.4: generate bytecode desugar(// Process 3.3: decode sugar flow(// Process 3.2: data flow analysis attribute(// Process 3.1: Labeled todo. Remove ())))); break; . }... }Copy the code

Today’s focus is on the annotation processor in JavAC.

Plug-in Annotation Processor (JSR-269)

In JDK5, Java provided support for annotations, but at the time, these annotations were the same as normal Java code and were only useful at run time.

The JSR-269 specification is implemented in JDK6, which provides a standard API for plug-in annotation processors to process annotations at compile time. As such, they are more like compiler plug-ins that allow us to read, modify, and add arbitrary elements to the abstract syntax tree.

If these plug-ins make 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 modifications have been made to the syntax tree by any plug-in annotation processor. Each cycle is called a Round.

AbstractProcessor

Annotation processing abstract class: javax.mail. The annotation. Processing. AbstractProcessor.

If you want to implement an annotation processor, you must inherit from the AbstractProcessor class, whose process() method is the procedure that the Javac compiler calls when executing the annotation processor.

    public abstract boolean process(Set<? extends TypeElement> annotations,
                                    RoundEnvironment roundEnv);
Copy the code

The first parameter of the method, Annotations, represents the collection of annotations to be processed by the annotation handler; The second argument, roundEnv, is the syntax tree node in the current Round.

Specific syntax tree node can see enumerated types: javax.mail. Lang. Model. Element. ElementKind, including the most common element in the Java code.

  • PACKAGE
  • CLASS
  • LOCAL_VARIABLE
  • FIELD
  • .

In addition, in the init method, we pass in the instance variable processingEnv, which represents the context provided by the annotation processor framework and is needed to create code, output information, and fetch utility classes.

So how do we get our code to execute to our own annotation handler at compile time?

Please see javac -help

$javac -help: javac <options> <source files> -processor <class1>[,<class2>,<class3>...] The name of the annotation handler to run; Bypassing the default search process-processorPath < path > to specify where to find the comment handler...Copy the code

Instead of adding this parameter every time you compile, you can use the Maven-compiler-plugin:

	<plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <executions>
                    <execution>
                        <id>default-compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                        <configuration>
                            <source>1.8</source>
                            <target>1.8</target>
                            <annotationProcessors>
				<annotationProcessor>xxx.xxx.xxx.xxx</annotationProcessor>
                            </annotationProcessors>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
Copy the code

(com. Google. Auto. Service. AutoService way no longer involved in this article, interested to search)

The specific implementation

The desired effect

The use of compile-time annotations, in the method to enter and exit the new log print parameter, parameter function.

The expected result is shown below, with the Java source code on the left and the compiled class file on the right.

Where the red arrows execute lines of code that generate code for the compile-time annotation processor.

use

  1. Based on SLF4J, define a member attribute in the target class and create aorg.slf4j.LoggerInstance, property name: logger.
  2. Add custom annotations to methods that need to print logs@AroundSlf4j.

Implementation approach

Only the key ideas and some major code are released here.

  1. Define a custom annotation @aroundslf4j

  2. In the annotation handler, all the elements annotated by the annotation are retrieved and the elements of type METHOD are filtered out.

  3. Find the “owner” of the element and iterate over its member variables to find a symbolic reference of type org.slf4j.Logger with the name Logger.

  4. Gets the name of the METHOD element being processed, the name of its class, and a list of its parameters, concatenated into log print format.

  5. Generate a JCTree node that calls the logger.info METHOD and add it to the METHOD node list.

  6. Recursively traverses all execution paths of the current method to find all nodes of type RETURN.

    6.1 According to the form of RETURN statement, create the corresponding JCTree node to invoke the logger.info method.

    6.2 Adding a Node to the position before a RETURN node.

  7. The end of the

Define annotations & annotation handlers

Note that this annotation exists only at the SOURCE level and is not useful beyond that.

@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.SOURCE)
public @interface AroundSlf4j {
}
Copy the code
@SupportedAnnotationTypes("AroundSlf4j")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class AroundSlf4jProcessor extends AbstractProcessor {
...
}
Copy the code

Access to targetMETHODThe element

In the annotation handler’s process method, you can get all JCTree nodes marked by the @aroundSLf4J annotation. The element whose owner is interface needs to be filtered out.

Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(AroundSlf4j.class); elementsAnnotatedWith.forEach(ele->{ if(ele.getKind() == ElementKind.METHOD && ! ((Symbol.MethodSymbol) ele).owner.isInterface()){ ... }Copy the code

Gets a symbolic reference to a member property of the specified type and name

Simply iterate over all member attributes of the owner of the METHOD node, comparing them by name/type.

 private Symbol.VarSymbol getAvailableFieldInMethod(JCTree.JCMethodDecl jcMethodDecl,Class target,String name){
        Scope members = jcMethodDecl.sym.owner.members();
        Iterator<Symbol> iterator = members.getElements().iterator();
        while (iterator.hasNext()){
            Symbol next = iterator.next();
            if(ElementKind.FIELD.equals(next.getKind()) && next.getQualifiedName().toString().equals(name)
                && next.type.tsym.getQualifiedName().toString().equals(target.getName())){

                return (Symbol.VarSymbol) next;
            }
        }
        return null;
    }
Copy the code

Apply the enhancement Visitor to the target node

Here I create a named AroundSlf4jMethodVisitor increment of class, inherited from com. Sun. View javac. Tree. TreeTranslator.

Because the logger.info call needs to be generated in this enhanced class. So, of course, the logger member attributes you just found should be passed in.

 tree.accept(new AroundSlf4jMethodVisitor(treeMaker,names,logger));
Copy the code

Generates a print input parameter call statement and joins

It is easier to assemble the log print from the method name and class name.

To see how to generate a method call directly:

Use the utility class passed in AroundSlf4jProcessor: treeMaker.

JCTree.JCExpressionStatement beforeState = treeMaker.Exec(treeMaker.Apply( List.nil(), Select(treemaker.ident (logger.name),names.fromString("info")), // enter loggerargs.tolist ());Copy the code

You then need to add the statement you just generated to the original method section tree.

Because the print entry should be in the first code of the method. So, use the prepend method to add a nod to the section.

jcMethodDecl.body.stats = jcMethodDecl.body.stats.prepend(beforeState);
Copy the code

Recursively traverse the method execution path to find allRETURNstatements

As for the statement types in Java code, I continue to recurse only the BLOCK code, if statement if, and FOR_LOOP types, which can cover most execution branches.

 private void walkReturnExpression(List<JCTree.JCStatement> statement){
        for(int i = 0 ;i< statement.size();i++){
            JCTree.JCStatement jcStatement = statement.get(i);
            if(jcStatement == null){
                continue;
            }
            switch (jcStatement.getKind()){
                case BLOCK:
                    walkReturnExpression(((JCTree.JCBlock)jcStatement).stats);
                    break;

                case IF:
                    ((JCTree.JCIf)jcStatement).getThenStatement().accept(new AroundSlf4jBlockVisitor(treeMaker,names,logger));
                    JCTree.JCStatement current = ((JCTree.JCIf)jcStatement).getElseStatement();
                    walkReturnExpression(List.of(current));
                    break;
                case FOR_LOOP:
                    JCTree.JCBlock body = (JCTree.JCBlock) ((JCTree.JCForLoop) jcStatement).body;
                    walkReturnExpression(body.stats);
                    break;

                default:
                    System.out.println(jcStatement);
            }
        }
    }
Copy the code

Next, add the print log call before RETURN.

        jcMethodDecl.body.stats.stream().filter( c-> Tree.Kind.RETURN == c.getKind() ).findFirst().ifPresent( r->{
            StatementHelper statementHelper = new StatementHelper(treeMaker,names);
            JCTree.JCExpressionStatement endLogging = statementHelper.createEndLoggingStatementByReturn(logger, (JCTree.JCReturn) r);
            jcMethodDecl.body.stats = SunListUtils.prependBeforeItem(jcMethodDecl.body.stats.iterator(),endLogging,r);
        });
Copy the code

The print log call precedes the return statement. So, it should be the penultimate code.

I wrote here a tool method: before the elements specified in the List of new elements SunListUtils. PrependBeforeItem.

The resources

  • In-depth Understanding of the Java Virtual Machine
  • Its source