One, foreword

Last time in AOP concepts and common means (1) we defined the concept of AOP, and learned the basic use and principle of APT, Transform, the following look at the compiler preprocessing these two ways:

  1. Transform + Javassit
  2. APT + AST

Javassist implements Transform

When we introduced Transform last time, we only learned about ASM. ASM is powerful and can implement various requirements, but it requires a certain level of understanding of the bytecode specification, which is expensive to learn. Let’s take a look at Javassist again.

A simple introduction

Javassist(Java Assist) is also a bytecode modification tool. It can be modified not only based on bytecode like ASM, but also at the source level. That is, you can modify the original Java class as you normally write code without being familiar with the bytecode specification, which greatly reduces the learning cost.

The sample Demo

1. Into the ClassPath

Javassist allows us to modify the source code directly without using the bytecode, by having to read the original Class file so that it can generate the final bytecode.

The ClassPool is used to store the addresses of class files or classloaders that need to be processed. We need to inject the path into the ClassPool as the Transform processes each input, otherwise we will probably get a NotFountException.

abstract class BaseTransform(private val extension: BaseExtension) : Transform() {
    protected val classPool: ClassPool = ClassPool.getDefault().apply {
        // key!! Into the android. The jar
        appendClassPath(extension.bootClasspath[0].absolutePath)
    }
    
    @Throws(TransformException::class, InterruptedException::class, IOException::class)
    final override fun transform(transformInvocation: TransformInvocation){... transformInvocation.inputs.forEach { input -> input.jarInputs.forEach { jarInput ->// Inject the JAR address
                classPool.appendClassPath(jarInput.file.absolutePath)
                ...
            }
            input.directoryInputs.forEach { directoryInput ->
                // Inject the directory addressclassPool.appendClassPath(directoryInput.file.absolutePath) ... }}}}Copy the code
2. Work with individual classes

The Transform finds a single class that needs to be processed, and then finds the corresponding class by className for modification

    /** * Write back to the output path of transform by ByteArray */
    override fun handleFileBytes(className: String): ByteArray {
        val targetClass = classPool.get(className)
        handleClass(targetClass)
        
        return targetClass.toBytecode()
    }
    
    override fun handleClass(targetClass: CtClass) {
        // Is a subclass of Activity
        if(! targetClass.subclassOf(classPool["android.app.Activity"]) {return
        }
        val onCreateMethods = targetClass.getDeclaredMethods("onCreate")
        for (onCreateMethod in onCreateMethods) {
            // Determine if it is an onCreate lifecycle method
            val params = onCreateMethod.parameterTypes
            if(params.size ! =1 || params[0] != classPool["android.os.Bundle"]) {
                continue
            }
            // Real processing
            try {
                case2(targetClass, onCreateMethod)
            } catch (e: CannotCompileException) {
                println("$name.handleClass CannotCompileException: $onCreateMethod")}}}/** * insert after method * Note * 1. Only Java code can be inserted, regardless of whether the current class is Kotlin or not * 2. It must be a usable expression, not uncompiled code, such as a single parenthesis */
    private fun case1(onCreateMethod: CtMethod) {
        classPool.importPackage("android.widget.Toast")
        onCreateMethod.insertAfter(
            """ showJavassistToast(); Toast.makeText(this, JAVASSIST_SINGE_MSG, Toast.LENGTH_LONG).show(); "" ")}/** * addCatch must add a return at the end of the block. * /
    private fun case2(targetClass: CtClass, onCreateMethod: CtMethod) {
        classPool.importPackage("android.util.Log")
        onCreateMethod.addCatch(
            """
                Log.e("${targetClass.name}\n" + log.getStackTraceString ()The ${'$'}e)); return; "" ", classPool.get("java.lang.NullPointerException"))}Copy the code

This code adds a try-catch to all activities’ onCreate methods.

What you can do with it

As we’ve seen before, Javassist has some limitations, so we prefer ASM. A quick look at Javassist-API, however, shows that it can do almost everything:

  1. Instrument (CodeConverter()), ctmethod.instrument (ExprEditor)
  2. Insert statements: ctmethod.insertbefore, ctmethod.insertafter, ctmethod.insertat
  3. Overall replacement: ctmethod.setBody
  4. Overall catch: ctmethod.addcatch
  5. Modify the modifier: CtMethod CtField/CtConstructor. SetModifiers
  6. .

For more details on the API, see the official documentation: github.com/jboss-javas…

Can you modify it at run time?

Javassist also allows you to dynamically modify code at run time, in two ways:

  1. CtClass also provides Class
    toClass(ClassLoader loader), so that we can modify any code directly while writing business code, and then use toClass to build the modified object. Note, however, that this is not an AOP-type modification of the original class file, but rather a build-as-you-go, valid only for the moment.

  2. Dynamic Proxy ProxyFactory, which breaks through the Proxy and InvocationHandler provided by JDK, can handle dynamic Proxy classes. But tests found white happy a 😂 for android if use ProxyFactory will collapse, the reason is that modified the JDK code, android SecurityManager. GetClassContext must return null null pointer.

    val proxyFactory = ProxyFactory().apply {
        superclass = ProxyTest::class.java
        setFilter { it.name == "test"}}val proxyClass = proxyFactory.createClass() // The null pointer crashes
    (proxyClass as Proxy).setHandler { self, thisMethod, _, args ->
        val result = thisMethod.invoke(self, args) as String
        return@setHandler result + result
    }
    val proxyTest = proxyClass.newInstance() as ProxyTest
    Copy the code

Javassist processing principle

To summarize

Javassist has a lower learning cost compared with ASM and can basically meet all requirements. Javassist can be preferred for Transform.

In addition, Javassist can dynamically modify class files at run time, and you can also consider using Javassist to fix tripartite library bugs.

Abstract syntax tree AST

The core of AST (Abstract Syntax Tree) is to transform Code into a description (object, method, operator, flow control statement, declaration/assignment, inner class, etc.), so that we can easily view and modify Code at run time. The transformation process can be considered as a run-time compilation.

Work way

Apps around you

The Lombok plug-in in the IDE, syntax highlighting, formatting code, auto-completion, and code obfuscating compression all take advantage of AST.

Experience as: astexplorer.net/

Extend APT with AST

As we’ve seen before, the annotation processor APT can only be used to generate code, not modify it, but with AST that’s different. The Lombok plugin in the IDE works by using APT to dynamically modify the AST and add new logic to existing classes.

Now that we know how it works, let’s just write Hello World.

The sample Demo

  1. Javasdk/Contents/Home/lib/view provides the AST API in the jar, we need to refer to the library

  2. Generate AST

    class ASTProcessor : AbstractProcessor() {
    
        // The generated AST
        private lateinit var trees: Trees
        // Used to generate new code
        private lateinit var treeMaker: TreeMaker
        // Used for build naming
        private lateinit var names: Names
    
        override fun init(processingEnv: ProcessingEnvironment?). {
            super.init(processingEnv)
            if (processingEnv is JavacProcessingEnvironment) {
                trees = Trees.instance(processingEnv)
                treeMaker = TreeMaker.instance(processingEnv.context)
                names = Names.instance(processingEnv.context)
            }
        }
    
    }    
    Copy the code
  3. Modify the AST

    override fun process(typeElementSet: MutableSet, roundEnvironment: RoundEnvironment): Boolean {
        for (typeElement in typeElementSet) {
            val elements = roundEnvironment.getElementsAnnotatedWith(typeElement)
            for (element in elements) {
                // Find the subtree corresponding to Element
                val jcTree = trees.getTree(element) as JCTree
                jcTree.accept(myVisitor)
            }
        }
        return false
    }
    
    private val myVisitor = object : TreeTranslator() {
        /** * The class in the visitor pattern defines visit */
        override fun visitClassDef(tree: JCClassDecl) {
            super.visitClassDef(tree)
            // defs refers to defined content, including methods, parameters, inner classes, etc
            for (jcTree in tree.defs) {
                // If it is a parameter
                if (jcTree is JCVariableDecl) {
                    tree.defs.append(makeGetterMethod(jcTree))
                }
            }
        }
    }
    
    /** * build the get method */
    private fun makeGetterMethod(variable: JCVariableDecl): JCMethodDecl? {
        // this
        val ident = treeMaker.Ident(names.fromString("this"))
        // this.xx
        val select = treeMaker.Select(ident, variable.name)
        // return this.xxx
        val jcStatement: JCStatement = treeMaker.Return(select)
        // Insert the entire expression into the code block
        val jcBlock = treeMaker.Block(0, List.nil<JCStatement? >().append(jcStatement))return treeMaker.MethodDef(
            treeMaker.Modifiers(Flags.PUBLIC.toLong()), //public
            getterMethodName(variable),   // getXxx
            variable.vartype,             / / return type
            List.nil<JCTypeParameter>(),  // Generic parameter list
            List.nil<JCVariableDecl>(),   // Parameter list
            List.nil<JCExpression>(),     // List of exceptions thrown
            jcBlock,                      / / code block
            null)}Copy the code

rendering

AST+APT implementation of AOP limitations

After testing, it is found that AST operation can only be applied to annotationProcessor, kapt does not support: modification of AST does not take effect. So it’s still not feasible to use AST for AOP processing in the current situation where we use Kotlin on a large scale.

Thinking: Although AST cannot currently be combined with APT for AOP processing, this approach to modifying syntax trees provides new ideas that may be applied in other areas.