Android AOP programming -- AspectJ Syntax & Practice

Posted by dynamicallystatic on Thu, 20 Jan 2022 21:25:10 +0100

In the last article Android AOP programming (I) -- AspectJ basic knowledge In, I recorded some basic knowledge of AOP programming using AspectJ in Android, but the most important use of AspectJ is the syntax for the aspect. It is not difficult to find the aspect, but how to write rules to match the aspect. This article mainly records the syntax of AspectJ and uses an example to explain the application of AspectJ.

AspectJ syntax collation

The following syntax collation of AspectJ comes from the network collection and has not been verified one by one. Please point out any errors.

execution

Execute using the execution (< match expression >) method

Matching patterndescribe
* public * *(..)Implementation of any public method
* cn.javass..IPointcutService.*()cn. Any parameterless method in the IPointcutService interface under javass package and all sub packages
* cn.javass..*.*(..)cn. Any method of any class under javass package and all sub packages
* cn.javass..IPointcutService.*(*)cn. There is only one parameter method for the IPointcutService interface under javass package and all sub packages
* (!cn.javass..IPointcutService+).*(..)Any method other than "IPointcutService interface and subtype under cn.javass package and all sub packages"
* cn.javass..IPointcutService+.*()cn.javass package and any parameterless methods of IPointcutService interface and subtype under all sub packages
* cn.javass..IPointcut*.test*(java.util.Date)cn. The prefix type of IPointcut under javass package and all sub packages starts with test, and only one parameter type is Java util. For the method of date, note that the matching is based on the parameter type of the method signature, not the parameter type passed in during execution. For example, define the method: public void test(Object obj); Even if Java. Net is passed in at execution time util. Date will not match;
* cn.javass..IPointcut*.test*(..) throws IllegalArgumentException, ArrayIndexOutOfBoundsExceptioncn. Any method of IPointcut prefix type under javass package and all sub packages, and throw IllegalArgumentException and ArrayIndexOutOfBoundsException exceptions
* (cn.javass..IPointcutService+ && java.io.Serializable+).*(..)Any implementation of CN The IPointcutService interface and Java. Net under javass package and all sub packages io. Any method of the type of the serializable interface
@java.lang.Deprecated * *(..)Any holding @ Java Lang. deprecated annotation method
@java.lang.Deprecated @cn.javass..Secure * *(..)Any holding @ Java Lang.deprecated and @ CN Javass... Method of Secure annotation
@(java.lang.Deprecated || cn.javass..Secure) * *(..)Any holding @ Java Lang. deprecated or @ CN Javass... Method of Secure annotation
(@cn.javass..Secure *) *(..)Any return value type holds @ CN Javass... Secure method
* (@cn.javass..Secure *).*(..)The type of any defined method holds @ CN Javass... Secure method
* *(@cn.javass..Secure (*) , @cn.javass..Secure (*))Any method whose signature has two parameters and both parameters are marked by @ Secure, such as public void test(@Secure String str1, @Secure String str1);
** (@ cn.javass..Secure *) or * (@ cn.javass..Secure *)Any method with one parameter, and the parameter type holds @ CN javass…Secure; Such as public void test(Model model); And the @ secure annotation is held on the Model class
* *(@cn.javass..Secure (@cn.javass..Secure *) ,@ cn.javass..Secure (@cn.javass..Secure *))Any method with two parameters, and both parameters are @ CN Javass... Secure tag; And the types of these two parameters hold @ CN javass…Secure;
* *(java.util.Map<cn.javass..Model, cn.javass..Model>, ..)Any with a Java util. The method of map parameter, and the parameter type is < CN javass…Model,cn.javass... Model > is a generic parameter; Note that only the first parameter matched is Java util. Map, excluding subtypes; Such as public void test (HashMap < model, model > map, string STR); Will not match, and "* * (Java. Util. HashMap < CN. Javass... Model,cn.javass... Model >,...)" must be used for matching; Public void test (map, int i); Will also not match because the generic parameters do not match
* *(java.util.Collection<@cn.javass..Secure *>)Any method with a parameter (type java.util.Collection), and the parameter type has a generic parameter, which holds @ CN. On the generic parameter type Javass... Secure annotation; Such as public void test (Collection); Hold @ CN. On model type javass…Secure
* *(java.util.Set<? extends HashMap>)Any method with one parameter, and the passed in parameter type has a generic parameter, which inherits from HashMap;
* *(java.util.List<? super HashMap>)Any method with one parameter, and the passed in parameter type has a generic parameter, which is the base type of HashMap; Such as public voi test (map);
* *(*<@cn.javass..Secure *>)Any method with a parameter, and the parameter type has a generic parameter, which holds @ CN Javass... Secure annotation;

within

Execute using the within (< match expression >) method

Matching patterndescribe
within(cn.javass..*)cn. Any method execution under javass package and sub package
within(cn.javass..IPointcutService+)cn.javass package or any method of IPointcutService type and subtype under all sub packages
within(@cn.javass..Secure *)Hold CN Any method of any type of javass... Secure annotation must declare this annotation on the target object, and the annotation declared on the interface has no effect on it

this

Use "this" to match the execution method of the current AOP proxy object type; Note that the type of AOP proxy object matches, which may include the introduction of interface methods; Note that the expression used in this must be a fully qualified name of type, and wildcards are not supported;

Matching patterndescribe
this(cn.javass.spring.chapter6.service.IPointcutService)The current AOP object implements any method of the IPointcutService interface
this(cn.javass.spring.chapter6.service.IIntroductionService)The current AOP object implements any method of the IIntroductionService interface, or it may be the incoming interface

target

Use "target (type fully qualified name)" to match the execution method of the current target object type; Note that the type matching of the target object does not include the type matching of the incoming interface; Note that the expression used in target must be a fully qualified name of type, and wildcards are not supported;

Matching patterndescribe
target(cn.javass.spring.chapter6.service.IPointcutService)The current target object (non AOP object) implements any method of the IPointcutService interface
target(cn.javass.spring.chapter6.service.IIntroductionService)Any method that the current target object (non AOP object) implements the IIntroductionService interface cannot be the incoming interface

args

Use "args (parameter type list)" to match the currently executed method. The passed in parameter is the execution method of the specified type; Note that it matches the parameter type passed in, not the parameter type matching the method signature; The parameter in the parameter type list must be a fully qualified name, which is not supported by wildcards; Args is a dynamic pointcut. This pointcut is very expensive and should not be used in non special cases;

Matching patterndescribe
args (java.io.Serializable,..)Any method that starts with "the incoming parameter type is java.io.Serializable" and can be executed with any parameter of any type. The parameter type specified by args is dynamically matched at runtime

@within

Use "@ within (annotation type)" to match, so hold the method within the specified annotation type; The annotation type must also be a fully qualified type name;

Matching patterndescribe
@within cn.javass.spring.chapter6.Secure)Class method with Secure annotation for the type corresponding to any target object; This annotation must be declared on the target object, and declared on the interface has no effect on it

@target

Use "@ target (annotation type)" to match the execution method of the current target object type, where the target object holds the specified annotation; The annotation type must also be a fully qualified type name;

Matching patterndescribe
@target (cn.javass.spring.chapter6.Secure)Class method of any target object holding Secure annotation; This annotation must be declared on the target object, and declared on the interface has no effect on it

@args

Use "@ args (annotation list)" to match the currently executed method. The parameters passed in hold the execution of the specified annotation; The annotation type must also be a fully qualified type name;

Matching patterndescribe
@args (cn.javass.spring.chapter6.Secure)Any method that accepts only one parameter, and the parameters passed in when the method runs hold annotation CN javass. spring. chapter6. Secure; Dynamic pointcuts, similar to the arg indicator;

@annotation

Use "@ annotation (annotation type)" to match the method that the current execution method holds the specified annotation; The annotation type must also be a fully qualified type name;

Matching patterndescribe
@annotation(cn.javass.spring.chapter6.Secure )Annotation CN. Is held on the current execution method javass. spring. chapter6. Secure will be matched

Application of AspectJ in Android

Handle the problem of repeatedly triggering click events during Android quick click

In Android, we often have such scenes: click the button to jump from the current page A to another page B. if the click speed is very fast, we will find that multiple pages B may be opened. For example, the following code is in the center of the MainActivity page, and click the button to open OtherActivity:

import android.content.Intent;
import android.os.Bundle;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(MainActivity.this, OtherActivity.class));
            }
        });
    }
}

If you test on a real machine, you will find that OtherActivity may be opened twice or even three times when you quickly click the button. In order to deal with the problems caused by this kind of fast click, you generally do this:
Write a tool class to provide a method to judge the difference between the current click time and the last click time. If the time difference is relatively short (such as 500 milliseconds), it is considered to be a fast click, the click event will not be processed, such as the following code:

public class ClickUtil {
    
    private static long lastClickTime = 0L;
    
    // Is it a quick click
    public static boolean isFastClick() {
        long curTime = System.currentTimeMillis();
        if (curTime - lastClickTime >= 500L) {
            lastClickTime = curTime;
            return true;
        }
        return false;
    }
    
}

Invoke the above code in clicking event related logic:

if (!ClickUtil.isFastClick()) {
    startActivity(new Intent(MainActivity.this, OtherActivity.class));
}

This method can prevent quick clicks, but there are some problems:

  1. The code is intrusive and needs to be modified in the original code logic
  2. It's complicated. If there are many places that need to handle click events too fast, you need to add a lot of code

We can use AspectJ aspect oriented programming to deal with this problem, such as writing the following pointcut Code:

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class MethodAspect {

    // The time interval for triggering click events is 600 milliseconds, that is, only one click is allowed within 600 milliseconds
    private static final long CLICK_INTERVAL = 600L;
    // Last clicked
    private static long lastClickTime = 0L;

    // Allow click events
    private boolean isClickEnabled() {
        long curTime = System.currentTimeMillis();
        if (curTime - lastClickTime > CLICK_INTERVAL) {
            lastClickTime = curTime;
            return true;
        }
        return false;
    }
	
	// This method will match Android The onClick method in the OnClickListener interface under the view package and all sub packages, and the method parameter is Android view. View
	// Note that @ Around is used instead of @ Before, and the method parameter is ProceedingJoinPoint
    @Around("execution(* android.view..OnClickListener.onClick(android.view.View))")
    public void clickableDetect(ProceedingJoinPoint joinPoint) {
        if (isClickEnabled()) {
        	// If the click event is allowed, the original logic is executed
            try {
                joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
    }

}

There is no need to make any changes in MainActivity. The above pointcut code will automatically match the click event of the button, modify the compiled bytecode, and add the logical judgment of whether the click is allowed around the original click logic. We can view MainActivity in the app / build / intermediates / javac / debug / classified directory Class decompiled code as follows:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";

    public MainActivity() {
    }

    protected void onCreate(Bundle savedInstanceState) {
        MethodAspect.aspectOf().beforeOnCreate();
        super.onCreate(savedInstanceState);
        this.setContentView(2131427356);
        this.findViewById(2131230807).setOnClickListener(new OnClickListener() {
            public void onClick(View v) {
                JoinPoint var3 = Factory.makeJP(ajc$tjp_0, this, this, v);
                onClick_aroundBody1$advice(this, v, var3, MethodAspect.aspectOf(), (ProceedingJoinPoint)var3);
            }

            static {
                ajc$preClinit();
            }
        });
    }
}

It can be seen that there are more logic processed by AspectJ in the onClick method body. Testing fast clicks on the real machine will no longer trigger click events multiple times.

The advantage of using AspectJ AOP programming to deal with the problem of fast click multiple triggers over the direct coding above is that the code is non intrusive and does not need to modify the original click event logic. Through AspectJ framework, the code can be automatically inserted before and after the click event logic to complete the logical judgment of whether the click is too fast, but is this method perfect?

Not perfect!

We know that there are many ways to handle click events in Android:

  • You can directly set setOnClickListener for a View, and then pass in anonymous View OnClickListener
  • You can implement View. On a class Onclicklistener interface, and then set the click event of a View to this
  • You can define a member variable listener in the class, whose type is View Onclicklistener, and then specify the click event of a View as listener
  • ...

There are many other ways to add click events to the View. In addition, not all click events in the View are page jumps. Some click events may be triggered repeatedly without any problem. If the above AspectJ matching onClick method is used to handle them, there may be accidental injuries; In addition, if there are many click event methods related to onClick in a project, using AspectJ to match and modify these methods will add a great burden to the compilation project (the compilation time will become longer), so this method is not perfect. Next, another method based on AspectJ is used to more gracefully deal with the problem that click events are triggered repeatedly, Custom annotations are used.

First, we define the following notes:

// Act on the method
@Target(ElementType.METHOD)
// Annotations are retained until bytecode stage
@Retention(RetentionPolicy.CLASS)
public @interface ClickOnce {
}

The function we want to achieve is to use the @ ClickOnce annotation method to trigger only once in 600 milliseconds;

Then write the following code for the pointcut method:

@Aspect
public class MethodAspect {

    // The time interval for triggering click events is 600 milliseconds, that is, only one click is allowed within 600 milliseconds
    private static final long CLICK_INTERVAL = 600L;
    // Last clicked
    private static long lastClickTime = 0L;

    // Allow click events
    private boolean isClickEnabled() {
        long curTime = System.currentTimeMillis();
        if (curTime - lastClickTime > CLICK_INTERVAL) {
            lastClickTime = curTime;
            return true;
        }
        return false;
    }

	// Matches any method that uses the @ ClickOnce annotation
    @Around("execution(@ClickOnce * *(..))")
    public void clickOnce(ProceedingJoinPoint joinPoint) {
        if (isClickEnabled()) {
            try {
                joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
        }
    }

}

Next, test whether the @ ClickOnce annotation works normally. Add two buttons in MainActivity to add click events for these two buttons in two different ways. Click or jump to OtherActivity:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

		// Set click event handler directly
        findViewById(R.id.btn2).setOnClickListener(new View.OnClickListener() {
            @Override
            @ClickOnce
            public void onClick(View v) {
                startActivity(new Intent(MainActivity.this, OtherActivity.class));
            }
        });

		// Use view. On class Onclicklistener interface binds the click event handler for the button in this way
        findViewById(R.id.btn3).setOnClickListener(this);
    }

    @ClickOnce
    private void toOtherActivity() {
        startActivity(new Intent(MainActivity.this, OtherActivity.class));
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn3) {
            toOtherActivity();
        }
    }
}

Then we compile the project and look again at the generated mainactivity Decompiled code of class file:

public class MainActivity extends AppCompatActivity implements OnClickListener {
    public MainActivity() {
    }

    protected void onCreate(Bundle savedInstanceState) {
        MethodAspect.aspectOf().beforeOnCreate();
        super.onCreate(savedInstanceState);
        this.setContentView(2131427356);
        this.findViewById(2131230808).setOnClickListener(new OnClickListener() {
            @ClickOnce
            public void onClick(View v) {
                JoinPoint var3 = Factory.makeJP(ajc$tjp_0, this, this, v);
                onClick_aroundBody1$advice(this, v, var3, MethodAspect.aspectOf(), (ProceedingJoinPoint)var3);
            }

            static {
                ajc$preClinit();
            }
        });
        this.findViewById(2131230809).setOnClickListener(this);
    }

    @ClickOnce
    private void toOtherActivity() {
        JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
        toOtherActivity_aroundBody1$advice(this, var1, MethodAspect.aspectOf(), (ProceedingJoinPoint)var1);
    }

    public void onClick(View v) {
        if (v.getId() == 2131230809) {
            this.toOtherActivity();
        }

    }

    static {
        ajc$preClinit();
    }
}

You can see the new code inserted by AspectJ in the method body annotated by @ ClickOnce. We run the project on the real machine. The test can see that the problem of fast click repeatedly triggering click events is eliminated, which proves that the above code can work normally.

This way of using custom annotations is a step further than the way of directly matching the onClick method with AspectJ. It not only does not invade the original code logic, but also is more flexible: you need to prevent using custom annotations when clicking repeatedly, otherwise you don't need them.

summary

AspectJ is a Java AOP framework. It can act on Java compiled bytecode files to realize some functions. Compared with object-oriented OOP programming, AOP is more flexible and elegant in dealing with some aspects. However, the use of AspectJ must be careful, and the matching rules for aspects must be tested in detail, Improper matching rules may lead to long code compilation time, and may handle logic that we don't need to deal with, resulting in some errors.

reference resources

https://blog.csdn.net/sunlihuo/article/details/52701548

Source code

The source code of this article can be downloaded here: https://github.com/yubo725/android-aspectj-demo/tree/v0.2

Topics: Java Android AOP aspectj