preface
How to choose among the many AOP methods? Is the procedure of application too tedious and the grammar concept too dizzy?
In this article, we will show you in detail the various aspects of selection. We will cut down on the two classic open source libraries and take the backbone to understand the application ideas and key processes of AOP. It’s nice to be able to practice AOP while mastering open source libraries.
Six tips to help you choose the right AOP approach
In our most comprehensive discussion of AOP approaches above, we analyzed and compared the most popular AOP approaches. So how do we weigh the choices between reality and business needs?
1. Specify where you are applying AOP
If you are maintaining an existing project, you will either need to try it out on a small scale or choose a less intrusive AOP approach (e.g., APT proxy classes are manual, flexible, but too intrusive with many insertion points).
2. Identify similarities in entry points
The first step is to consider the number and similarity of pointcuts, and whether you would like to annotate them one by one, or use similarity to unify your pointcuts.
The second step is to consider whether the classes that apply the aspect are final, and whether similar methods are static or final. Cglib inherits the proxied class and overrides the proxied method, so the proxied class and method cannot be final.
3. Clarify the granularity and timing of weaving
How do I choose when to Weave? Woven at compile time, or after compile? When loading? Or runtime? By comparing the differences and advantages and disadvantages of various AOP methods in weaving timing, we can get the criterion for deciding how to choose weaving timing.
For common cases, Weave at compile time is the most intuitive approach. Because the source contains all of the application’s information, this approach generally supports the most variety of join points. With compile-time Weave, we can use AOP systems to perform fine-grained Weave operations, such as reading or writing fields. The resulting modules will lose a lot of information when source code is compiled, so a coarse-grained AOP approach is typically used.
At the same time, for traditional languages compiled as native code, such as C++, the compiled modules are often operating system platform dependent, which makes it difficult to establish a uniform load-time and run-time Weave mechanism. For languages compiled to native code, Weave is most feasible only at compile time. While Weave has the advantages of being powerful and versatile at compile time, its disadvantages are also obvious. First, it requires the programmer to provide all the source code, so it is not suitable for modular projects.
To solve this problem, we can choose an AOP approach that supports compiled Weave.
A new problem arises. If the main logic and Aspect of the application are developed as separate components, the most logical time to Weave is when the framework loads the Aspect code.
Weave is probably the most flexible of all AOP approaches at runtime, allowing a program to specify whether or not specific aspects of Weave are needed for a single object at run time.
Timing Weave is critical for AOP applications. We need to make different choices for specific applications. We can also combine multiple AOP approaches to achieve a more flexible Weave strategy.
4. Specify performance requirements and method number requirements
With the exception of the dynamic Hook approach, the performance impact of other AOP approaches is negligible. Dynamic AOP essentially uses dynamic proxies, which inevitably involve reflection. APT inevitably generates a large number of proxy classes and methods. How to balance, depending on your requirements for the project.
5. Determine whether to modify the original class
If you just want specific enhancements, you can use APT to read Java code at compile time, parse annotations, and generate Java code on the fly.
Here is the process for compiling code in Java:
It can be seen that APT works in the Annotation Processing stage, and eventually the code generated through the Annotation processor will be compiled into Java bytecode together with the source code. But compares it is a pity that you cannot modify existing Java file, such as in the existing class to add a new method or delete the old method, so through the APT can realize injection through the way of auxiliary class, this may slightly increase the project number and class number, but as long as the good control, does not have much of an impact to the project.
6. Specify when to call
The timing of APT requires active invocation, whereas other AOP methods inject code at the same time as pointcuts.
Second, analyze AOP from open source libraries
AOP practices are poorly written, and there are too many blog posts on how to practice AOP. How is this post different from other posts? What can we benefit from?
In fact, AOP practice is very simple, the key is to understand and apply, we first reference the practice of open source library, on this basis to abstract key steps, while the actual combat while reading open source library task, deliciously!
APT
1. Classic APT framework ButterKnife workflow
Go straight to the illustration above.
As you can see from the above, why should attributes and methods tagged with @bind, @onclick, etc., be public or protected? Because ButterKnife injects the View by being referenced by the proxy class. This. editText. Why is that? The answer is: performance. If you make views and methods private, the framework must be injected by reflection.
Want to dive into the source code details to learn more about ButterKnife?
- how-butterknife-actually-works
- ButterKnife source code analysis
- Remove the ButterKnife from the JakeWharton series
2. Imitation ButterKnife, master APT
Let’s strip away the details, extract the key flow, and see how ButterKnife applies APT.
You can see that the key steps are as follows:
- Custom annotation
- Writing annotation handlers
- Scan the annotation
- Write proxy class content
- Generating proxy classes
- Calling the proxy class
Let’s highlight the steps that we need to implement. As follows:
Gee, as you may have noticed, the last step is to invoke the proxy class or facade object when appropriate. This is one of the drawbacks of APT, which automatically generates code at any package location but requires active invocation at runtime.
APT hand-to-hand implementation can refer to JavaPoet – Gracefully generating code – 3.2 a simple example
3. Tool details
We use the following 3 tools in APT:
(1) Java Annotation Tool
The Java Annotation Tool gives us a bunch of API support.
- The Java Annotation Tool Filer helps you export Java source as a file.
- Elements of the Java Annotation Tool help us process all element nodes that are scanned during the scan, such as PackageElement, TypeElement, ExecuteableElement, and so on.
- The TypeMirror of the Java Annotation Tool helps us determine if an element is of the type we want.
(2)JavaPoet
Of course, you can generate Java source code directly through string concatenation, how simple how to, a diagram show JavaPoet’s power.
(3) APT plug-in
The annotation handler is already there, so how do you execute it? This time you need to use the Android -apt plugin, use it for two purposes:
- Allows configurations to be used only as annotations processor dependencies at compile time and not added to the final APK or library
- Set the source path so that the code generated by the annotation handler is properly referenced by Android Studio
After butterknife is introduced into the project, it is unnecessary to introduce APT. If butterknife continues to be introduced, Using incompatible plugins for the annotation processing will be reported
(4) AutoService
To run the annotation processor, there are tedious steps:
- Create a resources folder in the main directory of the Processors library.
- Create a meta-INF /services folder under resources.
- In the meta-inf/services directory folder to create javax.mail. Annotation. Processing. The Processor files;
- In javax.mail. The annotation. Processing. Write the full name of annotation Processor Processor file, including the package path;
Google’s AutoService can reduce our workload. Just add @AutoService(processor.class) to your annotation Processor and it will automatically do the above step.
4. Proxy execution
As mentioned earlier, APT does not implement code insertion as Aspectj does, but it can be implemented in a variant form. Annotates a set of methods, which APT proxies. Refer to CakeRun for this section
APT generates proxy classes that perform annotated initialization methods in a sequence, with some logical judgment added to decide whether to execute the method or not. Thus bypassing the class where Crash occurred.
AspectJ
1. Classic Aspectj framework Hugo workflow
J God’s framework as always small and beautiful, want to chew open source library source, you can read from J God’s open source library first.
Back to the topic, Hugo is a Debug log library developed by J God. It contains excellent ideas and popular techniques, such as annotations, AOP, AspectJ, Gradle plugin, Android-Maven-Gradle-Plugin, etc. Before interpreting Hugo’s source code, you need to have some understanding of these points first.
Let’s start with the flow chart and go into details:
2. How to weave the logic of a print log?
With only an @debuglog annotation, Hugo can help us print the input and output parameters, and count the method time. Custom annotations are easy to understand, but let’s focus on how Hugo handles the section.
Did you find anything?
Yes, the pointcut expression helps us describe exactly where to cut.
AspectJ’s pointcut expression consists of keywords and operation parameters, executed in execution(* helloWorld(..)). ), where execution is the keyword, often called a function for ease of understanding, and * helloWorld(..). Is an operation parameter, often called an input parameter to a function. There are many types of pointcut expression functions, such as method pointcut function, method input parameter pointcut function, target class pointcut function, etc. Hugo uses two types:
The function name | type | The ginseng | instructions |
---|---|---|---|
execution() | Method tangent point function | Method matches the pattern string | Represents method join points in all target classes that satisfy a matching pattern, such as execution(* helloWorld(..)) ) represents helloWorld methods in all target classes, returning arbitrary values and arguments |
within() | Target class tangential function | The class name matches the pattern string | Represents all join points of classes in a particular domain that meet a matching pattern, for example within(com.feelschaotic. Demo.*) represents all methods of all classes in com.feelschaotic |
Want a detailed introduction to AspectJ syntax?
- Look at AspectJ’s strong insertion into Android
- An in-depth understanding of Android AOP syntax is covered in great detail
Why Is AspectJ in Android so slick?
We only need three steps to introduce Hugo.
Not that AspectJ is unfriendly on Android? ! The AspectJ compiler (AJC, an extension of the Java compiler) is used to weave all classes that are affected by the aspect. What about adding some extra configuration to gradle compile tasks so that they compile and run correctly, etc.?
Hugo has already done this for us (so in step 2, we need to use Hugo’s Gradle plug-in while introducing Hugo, just for hook compilation).
4. The key process of peeling off the silk
Abstracting Hugo’s workflow, we get two Aspect workflows:
As mentioned earlier in choosing the right AOP approach # 2, AspectJ has two uses for Pointcut pointcuts:
- Using custom annotations to modify a pointcut and control it precisely is intrusive
OnCreate (Bundle savedInstanceState) {//... followTextView.setOnClickListener(view -> { onClickFollow(); }); unFollowTextView.setOnClickListener(view -> { onClickUnFollow(); }); } @SingleClick(clickIntervalTime = 1000) private voidonClickUnFollow() {
}
@SingleClick(clickIntervalTime = 1000)
private void onClickFollow() {
}
@Aspect
public class AspectTest {
@Around("execution(@com.feelschaotic.aspectj.annotations.SingleClick * *(..) )") public void onClickLitener(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { //... }}Copy the code
- No changes are required in the pointcut code, which cuts by similarity (such as class name, package name) and is non-invasive
Protected void onCreate(Bundle savedInstanceState) {//... followTextView.setOnClickListener(view -> { //... }); unFollowTextView.setOnClickListener(view -> { //... }); } @Aspect public class AspectTest { @Around("execution(* android.view.View.OnClickListener.onClick(..) )") public void onClickLitener(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { //... }}Copy the code
5. The biggest difference between AspectJ and APT
APT’s right to decide whether or not to use aspects is still in the business code, whereas AspectJ gives the right to decide whether or not to use aspects back to the aspects. It is possible to determine which methods of which classes will be propped at aspect writing time, logically without intruding into business code.
However, the use of AspectJ needs to match some explicit Join Points. If the function name and package location of Join Points change and the corresponding matching rules are not changed, it may cause that the specified content cannot be matched and the desired functions cannot be inserted in this place.
So how does AspectJ work? How is the injected code connected to the object code? Can you use it? Do you know how an AOP framework works?
Three, application
Javassist
Why Javassist?
Because in the practice process, we can learn bytecode staking technology foundation, even if the subsequent learning of hot repair, application of ASM, these foundations are universal. Although Javassist has lower performance than ASM, it is novice friendly, manipulating bytecode without requiring direct exposure to bytecode technology and knowledge of virtual machine instructions, because Javassist implements a small compiler for working with source code that accepts source code written in Java. It is then compiled into Java bytecode and inlined into the method body.
Without further ado, let’s get started. Before getting started, let’s understand a few concepts:
1. Getting started
(1) Gradle
The Javassist modification object is the compiled class bytecode. First, we need to know when the compilation is complete so that we can make changes to the.class file before it is converted to the.dex file.
Most Android projects are built using Gradle, so we need to understand Gradle workflow first. Gradle completes the process by executing tasks one by one. After each Task is executed, the project is packaged. Gradle is a script container that holds tasks.
(2) the Plugin
How did so many tasks in Gralde come from and who defined them? Is the Plugin!
Let’s recall that the first line of the build.gradle file in the App Module usually has the apply plugin: ‘com.android.application’, lib build.gradle will have the apply plugin: ‘com.android.library’ is a Plugin that provides tasks for project construction. Different plugins register different tasks and use different plugins, so module functions are different.
Gradle is just a framework. It is the plugin that adds tasks to Gradle scripts.
(3) the Task
If a Task’s responsibility is to compile.java to.class, does the Task need to get the Java file directory first? Do you need to tell the next Task class directory after processing?
Inputs and outputs are important concepts for Task execution flowcharts. Outputs the inputs of the next Task to the outputs of the previous Task.
There must be a Task that packages all the classes as dex, so how do we find this Task? How about inserting our own Task for code injection before? Use Transfrom!
(4) the Transform
Transfrom is a new API for Gradle 1.5 +, which is also a Task.
-
Under Gradle Plugin 1.5, the Task preDex packages the compiled classes of dependent modules into jars, and the Task dex packages all classes into dex.
To listen when a project is packaged as a.dex, you must define a custom Gradle Task, insert it into predex or before dex, and use Javassist CA class in this custom Task.
-
Gradle plugin 1.5 above, preDex and Dex both Task has been replaced by TransfromClassesWithDexForDebug
Transform is more convenient, we no longer need to insert in front of a Task. Tranfrom has its own execution timing and is automatically added to the Task execution sequence once registered, just before the class is packaged as a dex, so we can define a Transform.
(5) the Groovy
- Gradle is implemented in Groovy. To customize Gradle plug-ins, you need to use Groovy.
- Groovy language = Extension of Java language + syntax of many scripting languages, running on the JVM virtual machine, can be seamless with Java. Groovy is inexpensive for Java developers to learn.
2. Summary
So what do we need to do? The process is summarized as follows:
3. Actual combat — automatic TryCatch
Now that I’ve said that, it’s time to get real. Every time I see a project code filled with defensive try-catch, I just
Let’s implement the automatic try-catch function step by step following the flowchart:
(1) Custom Plugin
- Create a new module, select Library Module, and the module name must be buildSrc
- Delete all files under Module and replace build.gradle configuration with the following:
apply plugin: 'groovy'
repositories {
jcenter()
}
dependencies {
compile 'com. Android. Tools. Build: gradle: 2.3.3'
compile 'org. Javassist: javassist: 3.20.0 - GA'
}
Copy the code
-
Creating a Groovy directory
-
The new Plugin class
Note: to create a new class in the groovy directory, select File and use. Groovy as the file format.
import org.gradle.api.Plugin import org.gradle.api.Project import com.android.build.gradle.AppExtension class PathPlugin implements Plugin<Project> { @Override void apply(Project project) { project.logger.debug"================ custom plug-in successful! = = = = = = = = = ="}}Copy the code
To see the results immediately, go ahead to step 4 in the flowchart and add the Apply plugin to buiil.gradle in the App Module.
Run:
(2) Customize Transfrom
import com.android.build.api.transform.* import com.android.build.gradle.internal.pipeline.TransformManager import org.apache.commons.codec.digest.DigestUtils import org.apache.commons.io.FileUtils import org.gradle.api.Project class PathTransform extends Transform {Project Project TransformOutputProvider outputProvider // In the constructor we save the Project object for later use Public PathTransform(Project Project) {this. Project = Project} public PathTransform(Project Project) {this. Project = Project} TransfromClassesWithPreDexForXXXX @Override StringgetName() {
return "PathTransform"} / / by specifying the type of input we have to deal with the specified file type @ Override the Set < QualifiedContent. ContentType >getInputTypes() {// specifies the bytecode to handle all classes and jarsreturnTransformmanager.content_class} @override Set< qualifiedContent.scope >getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false} @Override void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { super.transform(transformInvocation) } @Override void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {this.outputProvider = outputProvider traversalInputs(inputs)} /** * Transform inputs: * One is directory, DirectoryInput * one is JAR package, JarInput * to be traversed */ private ArrayList<TransformInput> traversalInputs(Collection<TransformInput> inputs) {elsion.each { TransformInput input -> traversalDirInputs(input) traversalJarInputs(input)}} /** * traversalDirInputs(input)} / private ArrayList < DirectoryInput > traversalDirInputs (TransformInput input) {input. DirectoryInputs. Each {/ folder contains is * * * * Our hand-written classes * r.class, * buildConfig.class * R$XXX.class *, etc. */ println("it == ${it}") //TODO: code can be injected here!! / / get the output directory def dest = outputProvider. GetContentLocation (it. The name, it. ContentTypes, it scopes, Format.directory) // Copy the input DIRECTORY to the output DIRECTORY fileutils.copydirectory (it. }} private ArrayList<JarInput> traversalJarInputs(TransformInput input) { // There is no need for JAR injection.Copy the code
(3) Register Transfrom with the custom Plugin
Going back to the PathPlugin we just defined, register PathTransfrom in the Apply method:
def android = project.extensions.findByType(AppExtension)
android.registerTransform(new PathTransform(project))
Copy the code
Clean project, run again to make sure no errors are reported.
(4) Code injection
Now for the main part, we create a new class TryCatchInject and print out the method and class name that we have scanned:
This class is different from the class defined in front, there is no need to inherit the specified parent class, there is no need to implement the specified method, so I use short method + expressive name instead of annotation, if you have any questions please be sure to feedback to me, I can reflect on whether the writing is not clear enough.
import javassist.ClassPool
import javassist.CtClass
import javassist.CtConstructor
import javassist.CtMethod
import javassist.bytecode.AnnotationsAttribute
import javassist.bytecode.MethodInfo
import java.lang.annotation.Annotation
class TryCatchInject {
private static String path
private static ClassPool pool = ClassPool.getDefault()
private static final String CLASS_SUFFIX = ".class"Static void injectDir(String path, String packageName) { this.path = path pool.appendClassPath(path) traverseFile(packageName) } private static traverseFile(String packageName) { File dir = new File(path)if(! dir.isDirectory()) {return
}
beginTraverseFile(dir, packageName)
}
private static beginTraverseFile(File dir, packageName) {
dir.eachFileRecurse { File file ->
String filePath = file.absolutePath
if (isClassFile(filePath)) {
int index = filePath.indexOf(packageName.replace(".", File.separator))
boolean isClassFilePath = index != -1
if (isClassFilePath) {
transformPathAndInjectCode(filePath, index)
}
}
}
}
private static boolean isClassFile(String filePath) {
return filePath.endsWith(".class") && !filePath.contains('R') && !filePath.contains('R.class') && !filePath.contains("BuildConfig.class")
}
private static void transformPathAndInjectCode(String filePath, int index) {
String className = getClassNameFromFilePath(filePath, index)
injectCode(className)
}
private static String getClassNameFromFilePath(String filePath, int index) {
int end = filePath.length() - CLASS_SUFFIX.length()
String className = filePath.substring(index, end).replace('\ \'.'. ').replace('/'.'. ')
className
}
private static void injectCode(String className) {
CtClass c = pool.getCtClass(className)
println("CtClass:"+ c) defrostClassIfFrozen(c) traverseMethod(c) c.writeFile(path) c.detach() } private static void traverseMethod(CtClass c) { CtMethod[] methods = c.getDeclaredMethods()for (ctMethod in methods) {
println("ctMethod:"}} private static void defrostClassIfFrozen(CtClass c) {if (c.isFrozen()) {
c.defrost()
}
}
}
Copy the code
Call the injected class at the TODO tag in PathTransfrom
// Please note that replace com\ feelschaotic\ javassist with trycatchinjectdir (it.file.absolutePath,"com\\feelschaotic\\javassist")
Copy the code
Let’s clean again and run
We can cut directly by the package name of the method, or by the tag of the method (e.g., special input arguments, method signatures, method names, annotations on methods…). Given that we only need to catch exceptions for specific methods, I’m going to mark methods with custom annotations.
Define an annotation in the App Module
@target (ElementType.METHOD) @Retention(retentionPolicy.runtime) public @interface AutoTryCatch { Class[] value() default exception.class; }Copy the code
Then we need to get the annotation on the TryCatchInject traverseMethod method TODO using Javassist and then get the value of the annotation.
private static void traverseMethod(CtClass c) {
CtMethod[] methods = c.getDeclaredMethods()
for (ctMethod in methods) {
println("ctMethod:" + ctMethod)
traverseAnnotation(ctMethod)
}
}
private static void traverseAnnotation(CtMethod ctMethod) {
Annotation[] annotations = ctMethod.getAnnotations()
for (annotation in annotations) {
def canonicalName = annotation.annotationType().canonicalName
if (isSpecifiedAnnotation(canonicalName)) {
onIsSpecifiedAnnotation(ctMethod, canonicalName)
}
}
}
private static boolean isSpecifiedAnnotation(String canonicalName) {
PROCESSED_ANNOTATION_NAME.equals(canonicalName)
}
private static void onIsSpecifiedAnnotation(CtMethod ctMethod, String canonicalName) {
MethodInfo methodInfo = ctMethod.getMethodInfo()
AnnotationsAttribute attribute = methodInfo.getAttribute(AnnotationsAttribute.visibleTag)
javassist.bytecode.annotation.Annotation javassistAnnotation = attribute.getAnnotation(canonicalName)
def names = javassistAnnotation.getMemberNames()
if (names == null || names.isEmpty()) {
catchAllExceptions(ctMethod)
return
}
catchSpecifiedExceptions(ctMethod, names, javassistAnnotation)
}
private static catchAllExceptions(CtMethod ctMethod) {
CtClass etype = pool.get("java.lang.Exception")
ctMethod.addCatch('{com.feelschaotic.javassist.Logger.print($e); return; } ', etype)
}
private static void catchSpecifiedExceptions(CtMethod ctMethod, Set names, javassist.bytecode.annotation.Annotation javassistAnnotation) {
names.each { def name ->
ArrayMemberValue arrayMemberValues = (ArrayMemberValue) javassistAnnotation.getMemberValue(name)
if (arrayMemberValues == null) {
return
}
addMultiCatch(ctMethod, (ClassMemberValue[]) arrayMemberValues.getValue())
}
}
private static void addMultiCatch(CtMethod ctMethod, ClassMemberValue[] classMemberValues) {
classMemberValues.each { ClassMemberValue classMemberValue ->
CtClass etype = pool.get(classMemberValue.value)
ctMethod.addCatch('{ com.feelschaotic.javassist.Logger.print($e); return; } ', etype)
}
}
Copy the code
Done! Write a demo:
As you can see, the application didn’t crash. Logcat prints an exception.
Complete demo please stamp
Afterword.
The process of finishing this article is tortuous, and the final draft has completely deviated from the original draft outline. Originally, I wanted to document the application of AOP in detail, and put every method into practice step by step. But in the process of writing, I constantly question myself. Thinking of changing the direction of writing to AOP open source library source analysis, but it is difficult to avoid getting into the quagmire of large source analysis.
The original intention of this article lies in the practice of AOP, since it is practice, why not abandon the syntax details, abstract process, graphical steps, after all, after learning can really absorb the devil’s details, and the second is the exquisite idea.
Writing itself is a kind of thinking, to warn myself.