AOP (Aspect-Oriented Programming) refers to aspect-oriented programming. AspectJ is one of the frameworks for implementing AOP, which handles bytecode internally for code injection.
Since 2001, AspectJ has been very mature and stable, and its simplicity is one of its strengths. As for its usage scenarios, check out some small examples in this article to give you an idea.
1. The integrated AspectJ
- Use gradle- Android – Aspectj -plugin
This method is easy to access. However, this plug-in has not been maintained for more than a year. Considering the compatibility of AGP, I am afraid that it will not be used in the future. Not recommended here. (There are special cases, which will be covered later in this article.)
- Regular Gradle configuration
This method will be relatively more configuration, but relatively controllable.
First in the project root directory build.gradle add:
classpath "Com. Android. Tools. Build: gradle: 2"
classpath 'org. Aspectj: aspectjtools: 1.9.6'
Copy the code
Then in the app’s build.gradle add:
dependencies {
...
implementation 'org. Aspectj: aspectjrt: 1.9.6'
}
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
final def log = project.logger
final def variants = project.android.applicationVariants
variants.all { variant ->
// Note that this control takes effect under debug, and you can control whether it takes effect by yourself
if(! variant.buildType.isDebuggable()) { log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
return
}
JavaCompile javaCompile = variant.javaCompileProvider.get()
javaCompile.doLast {
String[] args = ["-showWeaveInfo"."1.8"."-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
log.debug "ajc args: " + Arrays.toString(args)
MessageHandler handler = new MessageHandler(true)
new Main().run(args, handler)
for (IMessage message : handler.getMessages(null.true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break
case IMessage.WARNING:
log.warn message.message, message.thrown
break
case IMessage.INFO:
log.info message.message, message.thrown
break
case IMessage.DEBUG:
log.debug message.message, message.thrown
break}}}}Copy the code
You need to add configuration code (slightly different) for module use:
dependencies {
...
implementation 'org. Aspectj: aspectjrt: 1.9.6'
}
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
final def log = project.logger
android.libraryVariants.all{ variant ->
if(! variant.buildType.isDebuggable()) { log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
return
}
JavaCompile javaCompile = variant.javaCompileProvider.get()
javaCompile.doLast {
String[] args = ["-showWeaveInfo"."1.8"."-inpath", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-d", javaCompile.destinationDir.toString(),
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
log.debug "ajc args: " + Arrays.toString(args)
MessageHandler handler = new MessageHandler(true)
new Main().run(args, handler)
for (IMessage message : handler.getMessages(null.true)) {
switch (message.getKind()) {
case IMessage.ABORT:
case IMessage.ERROR:
case IMessage.FAIL:
log.error message.message, message.thrown
break
case IMessage.WARNING:
log.warn message.message, message.thrown
break
case IMessage.INFO:
log.info message.message, message.thrown
break
case IMessage.DEBUG:
log.debug message.message, message.thrown
break}}}}Copy the code
2.AspectJ basic syntax
Join Points
Join points to join the positions where we need to operate. Such as concatenating common methods, constructors, or static initializers, and whether to call methods outside or inside the method. Common types include Method Call, Method Execution, Constructor Call, Constructor Execution, and so on.
Pointcuts
Pointcuts are conditional Join Points that determine the pointcut location.
Pointcuts grammar | instructions |
---|---|
execution(MethodPattern) | Methods to perform |
call(MethodPattern) | Method is called |
execution(ConstructorPattern) | Constructor execution |
call(ConstructorPattern) | The constructor is called |
get(FieldPattern) | Reads the properties |
set(FieldPattern) | Set properties |
staticinitialization(TypePattern) | Static block initialization |
handler(TypePattern) | Exception handling |
The difference between execution and call is as follows:
Pattern Is as follows:
Pattern | Rules (note Spaces) |
---|---|
MethodPattern | [@annotation] [Access permission] Return value type [class name.] Method name (parameter) [throws Exception type] |
ConstructorPattern | [@annotation] [Access permission] [Class name.]new(parameter) [throws Exception type] |
FieldPattern | [@annotation] [access permission] Variable type [class name.] Variable name |
TypePattern | * Using things alone means matching any type,. Matches any string,. When used alone, matches any length or type, + Matches itself and subclasses, and one more. It’s an indefinite number. You can also use ‘&&, |
- In the above table, brackets are optional
- Example method matching:
1) Java.*.Date: Can represent java.sql.Date or java.util.Date2) Test* : can represent TestBase or TestDervied3) java.. * : represents any Subclass of Java4) java.. *Model+ : Java arbitrarypackageSubclasses with names ending in Model, such as TabelModel, TreeModel, etcCopy the code
- Example of parameter matching:
1) (int.char) : indicates that there are only two arguments and the first argument is of typeintThe second parameter type ischar
2) (String, ..) : indicates that there is at least one parameter. The first argument is of type String and the following arguments are of any type.3).. Represents the number and type of any parameter4) (Object ...) : represents an indefinite number of arguments of type Object, where... Not a wildcard, but the Java word for indefinite parameterCopy the code
Advice
Used to specify where code is inserted into Pointcuts.
Advice | instructions |
---|---|
@Before | Before executing JPoint |
@After | After executing JPoint |
@AfterReturning | Method is executed after the result is returned. |
@AfterThrowing | Handle unhandled exceptions. |
@Around | You can replace the original code. If the source code needs to be executed, use the ProceedingJoinPoint#proceed() method. |
After, Before examples
Here we implement a function that adds a Trace method to the onCreate method of all activities to measure the time taken by the onCreate method.
@Aspect // <- note that the addition will take effect to participate in the compilation
public class TraceTagAspectj {
@Before("execution(* android.app.Activity+.onCreate(..) )"
public void before(JoinPoint joinPoint) {
Trace.beginSection(joinPoint.getSignature().toString());
}
@After("execution(* android.app.Activity+.onCreate(..) )"
public void after(a) { Trace.endSection(); }}Copy the code
The compiled class code looks like this:
As you can see, it doesn’t insert the Trace function directly into the code, but rather goes through its own set of wrappers. If you want to plug in all functions, AspectJ has a lot of performance impact.
Most of the time, however, we’ll probably only plug in a small number of functions, and the performance impact of AspectJ will be negligible.
AfterReturning sample
Get the return value of the pointcut, like here we get a TextView and print its text value.
private TextView testAfterReturning(a) {
return findViewById(R.id.tv);
}
Copy the code
@Aspect
public class TextViewAspectj {
@AfterReturning(pointcut = "execution(* *.. *.testAfterReturning())", returning = "textView") // "textView" must be the same as the following parameter name
public void getTextView(TextView textView) {
Log.d("weilu"."text--->"+ textView.getText().toString()); }}Copy the code
The compiled class code looks like this:The log print:use@AfterReturning
You can make some changes to the result returned by the method (note the “=” assignment; String cannot be modified by this method).
AfterThrowing sample
Use @Afterthrowing when an exception occurs during method execution and the exception is not handled. For example, in the following example, we catch an exception and report it (using log output here)
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
testAfterThrowing();
}
private void testAfterThrowing(a) {
TextView textView = null;
textView.setText("aspectj"); }}Copy the code
@Aspect
public class ReportExceptionAspectj {
@AfterThrowing(pointcut = "call(* *.. *.testAfterThrowing())", throwing = "throwable") // "throwable" must have the same name as the following argument
public void reportException(Throwable throwable) {
Log.e("weilu"."throwable--->"+ throwable); }}Copy the code
The compiled class code looks like this:The log print:The thing to notice here is that the program is going to crash because it’s executingthrow var3
. If you don’t want to crash, you can use @around.
Around the sample
Following the example above, we try catch the exception code directly this time:
@Aspect
public class TryCatchAspectj {
@Pointcut("execution(* *.. *.testAround())")
public void methodTryCatch(a) {}@Around("methodTryCatch()")
public void aroundTryJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
try {
joinPoint.proceed(); // <- call the original code
} catch(Exception e) { e.printStackTrace(); }}}Copy the code
The compiled class code looks like this:@Around
Significantly more flexible, we can customize the effect of the substitution method, such as the return value of the substitution method mentioned above.
3. Advanced
withincode
Withincode represents the JPoints involved in the execution of a method and is usually used to filter pointcuts. For example, we have a Person object:
public class Person {
private String name;
private int age;
public Person(a) {
this.name = "weilu";
this.age = 18;
}
public String getName(a) {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge(a) {
return age;
}
public void setAge(int age) {
this.age = age; }}Copy the code
Person object in two set age, if we just want to let the constructor to take effect, let the method setAge fails, you can use @ Around (” execution (* com. Weilu. Aspectj. Demo. Person. SetAge (..) )”) But if there are more places to set age, we have trouble matching them one by one.
Here we can consider using the set Pointcuts:
public class FieldAspectJ {
@Around("set(int com.weilu.aspectj.demo.Person.age)")
public void aroundFieldSet(ProceedingJoinPoint joinPoint) throws Throwable {
Log.e("weilu"."around->" + joinPoint.getTarget().toString() + "#"+ joinPoint.getSignature().getName()); }}Copy the code
Due to theset(FieldPattern)
FieldPattern (); FieldPattern (); FieldPattern (); FieldPattern ();Then you can use itwithincode
Add filter criteria:
@Aspect
public class FieldAspectJ {
@Pointcut("! withincode(com.weilu.aspectj.demo.Person.new())")
public void invokePerson(a) {}@Around("set(int com.weilu.aspectj.demo.Person.age) && invokePerson()")
public void aroundFieldSet(ProceedingJoinPoint joinPoint) throws Throwable {
Log.e("weilu"."around->" + joinPoint.getTarget().toString() + "#"+ joinPoint.getSignature().getName()); }}Copy the code
Here are the results:
There’s also a within, which is similar to withincode. The difference is that its scope is a class, whereas WithinCode is a method. For example, within(com.weilu.activity.*) indicates any JPoint under this package.
args
Used to specify parameter conditions for the currently executing method. For example, in the previous example, if you need to specify an int as the first argument, the following arguments are unlimited. I could write it like this.
@Around("execution(* com.weilu.aspectj.withincode.Person.setAge(..) ) && args(int,..) ")
Copy the code
cflow
Cflow stands for call flow, and the condition of a Cflow is a pointcut
For example, a method calls b, C, and D. At this point, we need to count the time consuming of each method. If we follow the syntax we learned before, we need to write up to four pointcuts. The more methods, the more trouble.
Using cFlow, we can easily grasp the “call flow” of methods. Our test method is as follows:
private void test(a) {
testAfterReturning();
testAround();
testWithInCode();
}
Copy the code
The implementation is as follows:
@Aspect
public class TimingAspect {
@Around("execution(* *(..) ) && cflow(execution(* com.weilu.aspectj.demo.MainActivity.test(..) )))"
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = currentTimeMillis();
Object result = joinPoint.proceed();
long endTime = currentTimeMillis();
Log.e("weilu", joinPoint.getSignature().toString() + "- >" + (endTime - startTime) + " ms");
returnresult; }}Copy the code
cflow(execution(* com.weilu.aspectj.demo.MainActivity.test(..) ) represents the JPoint included in the call to the test method, including its own JPoint.
execution(* *(..) ) is used to remove the TimingAspect’s own code to avoid intercepting itself and forming an infinite loop.
Log results are as follows:
There is also cFlowBelow, which is similar to cFlow. The difference is that it does not include its own JPoint. That is, the example does not capture the time of the test method.
4. The real
Intercept click
The goal of click interception is to avoid repeated click events due to quick click controls. For example, open multiple pages, pop up multiple pop-ups, request multiple interfaces, I found before, it is easy to repeat this kind of situation on some models. So avoiding jitter is a common requirement in projects.
For example,butterknife
In their ownDebouncingOnClickListener
To avoid such problems.If you’re no longer using itbutterknife
, you can also copy this code. Replace them one by oneView.OnClickListener
. There was also the previous use of the Rxjava operator to handle the stabilization. But these methods are intrusive and require a lot of replacement work.
This scenario can be handled in an AOP manner. Intercept the onClick method to see if it can be clicked.
@Aspect
public class InterceptClickAspectJ {
// Time of last click
private Long lastTime = 0L;
// Click interval
private static final Long INTERVAL = 300L;
@Around("execution(* android.view.View.OnClickListener.onClick(..) )"
public void clickIntercept(ProceedingJoinPoint joinPoint) throws Throwable {
// Greater than the interval can be clicked
if (System.currentTimeMillis() - lastTime >= INTERVAL) {
// Record the click time
lastTime = System.currentTimeMillis();
// Execute the click event
joinPoint.proceed();
} else {
Log.e("weilu"."Repeat click"); }}}Copy the code
The implementation code is simple and looks like this:Consider that some view click events do not need to be stabilized, such as checkBox. Otherwise the checkBox state changes, but the event is not executed. We can define an annotation withwithincode
Filter methods that have this annotation. Specific needs can be expanded according to the actual project, here only to provide ideas.
Buried point
In the previous examples, AspectJ was used in a non-intrusive way. Here’s an invasive approach, which simply means using custom annotations and rules for using annotations as pointcuts. (In fact, you can also define a method name, as the entry rule)
First, define two annotations, one for passing fixed parameters such as eventName and eventId, which are also used as pointcuts, and one for passing dynamic parameters such as key.
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackEvent {
/** * Event name */
String eventName(a) default "";
/** * Event ID */
String eventId(a) default "";
}
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface TrackParameter {
String value(a) default "";
}
Copy the code
The Aspectj code looks like this:
@Aspect
public class TrackEventAspectj {
@Around("execution(@com.weilu.aspectj.tracking.TrackEvent * *(..) )"
public void trackEvent(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// Get the annotation on the method
TrackEvent trackEvent = signature.getMethod().getAnnotation(TrackEvent.class);
String eventName = trackEvent.eventName();
String eventId = trackEvent.eventId();
JSONObject params = new JSONObject();
params.put("eventName", eventName);
params.put("eventId", eventId);
// Get the annotation for the method parameters
Annotation[][] parameterAnnotations = signature.getMethod().getParameterAnnotations();
if(parameterAnnotations.length ! =0) {
int i = 0;
for (Annotation[] parameterAnnotation : parameterAnnotations) {
for (Annotation annotation : parameterAnnotation) {
if (annotation instanceof TrackParameter) {
// Obtain the key valueString key = ((TrackParameter) annotation).value(); params.put(key, joinPoint.getArgs()[i++]); }}}}/ / report
Log.e("weilu"."Reported data ---->" + params.toString());
try {
joinPoint.proceed();
} catch(Throwable throwable) { throwable.printStackTrace(); }}}Copy the code
Usage:
@trackEvent (eventName = "click button ", eventId = "100")
private void trackMethod(@TrackParameter("uid") int uid, String name) {
Intent intent = new Intent(this, KotlinActivity.class);
intent.putExtra("uid", uid);
intent.putExtra("name", name);
startActivity(intent);
}
trackMethod(10."weilu");
Copy the code
Here are the results:Due to the code problem of matching key value, it is recommended to write all parameters that need to be passed in dynamically in the first place to avoid overwriting the subscript.
There are also usage scenarios, such as permission control. In summary, AOP is good for separating out some common logic and injecting that part into business code via AOP. In this way, we can focus more on the implementation of the business and the code is clear.
5. Other questions
lambda
If we use a lambda in our code, for example, the click event will be:
tv.setOnClickListener(v -> Log.e("weilu"."Click event execution"));
Copy the code
In this case, the previous click pointcut is invalid, which involves the D8 desugar tool and invokedynamic bytecode instruction, which I can’t explain in detail here. Using lambda simply produces intermediate methods beginning with lambda$, so it can only be done as follows:
@Around("execution(* *.. lambda$*(android.view.View))")
Copy the code
This temporary processing is more troublesome, and you can see that the fault tolerance rate is relatively low, it is easy to cut into other irrelevant methods, so it is recommended that AOP do not use lambda.
configuration
The AspectJX plug-in, although less maintained recently, supports AAR, JAR, and Kotlin entry, and defaults to entry only for your own code.
In AspectJ regular configuration have code like this: “- inpath,” javaCompile. DestinationDir. The toString (), on behalf of only to weave the source file. When looking at the Aspectjx source code, it is found that the.jar file is added in the “-inputs” configuration, which enables class classes to be woven into the code. In this sense, AspectJ also supports weaving class files, but you need to configure them, which is cumbersome, so plug-ins like AspectJx are born.
For example, Kotlin needs to add the following configuration to the normal Gradle configuration:
def buildType = variant.buildType.name
String[] kotlinArgs = [
"-showWeaveInfo"."1.8"."-inpath", project.buildDir.path + "/tmp/kotlin-classes/" + buildType,
"-aspectpath", javaCompile.classpath.asPath,
"-d", project.buildDir.path + "/tmp/kotlin-classes/" + buildType,
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
MessageHandler handler = new MessageHandler(true)
new Main().run(kotlinArgs, handler)
Copy the code
Also be careful to write the corresponding Aspect class in Kotlin. After all, you need to inject Kotlin code, not in Java, but the other way around.
It is recommended to use AAR, JAR and Kotlin requirements of the plug-in, even if no later maintenance, can modify the source code to match GAP, relatively difficult.
This part has a lot of content and is boring at the same time. It has been organized intermittently for a week. Overview of AspectJ configuration in Android, as well as common syntax and usage scenarios. Enough for AspectJ.
Finally, the code involved in this article has been uploaded to Github for interested students to use for reference.
reference
- Application of AspectJ AOP in Android
- AOP: AspectJ comprehensive analysis in Android
- There are three ways to compile pilings: AspectJ, ASM, and ReDex
- Android has a record of introducing AspectJ