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:
- Transform + Javassit
- 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:
- Instrument (CodeConverter()), ctmethod.instrument (ExprEditor)
- Insert statements: ctmethod.insertbefore, ctmethod.insertafter, ctmethod.insertat
- Overall replacement: ctmethod.setBody
- Overall catch: ctmethod.addcatch
- Modify the modifier: CtMethod CtField/CtConstructor. SetModifiers
- .
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:
-
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. -
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
-
Javasdk/Contents/Home/lib/view provides the AST API in the jar, we need to refer to the library
-
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
-
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.