AOP application scenario actual combat - develop efficiency improvement tool based on AspectJX

Posted by MikeyNoedel on Sat, 20 Jun 2020 10:43:16 +0200

Article catalog

preface

about AOP thought and AspectJX framework As we all know, AspectJ provides developers with the basic ability to implement AOP, through which they can achieve functions that meet their business needs.

Here, we use the AspectJX framework to achieve some interesting functions related to performance improvement. The configuration and use of the AspectJX framework are README There are detailed steps in, which can also be referred to Official demo.

For the syntax description in AspectJ, see:
https://github.com/hiphonezhu/Android-Demos/blob/master/AspectJDemo/AspectJ.pdf
https://github.com/HujiangTechnology/AspectJX-Demo/blob/master/docs/aspectj_manual.md

Scene practice

Log printing

In daily development, Log is often printed in a key method to output a string and parameter variable value for analysis and debugging, or Log is printed before and after method execution to view the time-consuming of method execution.

Pain spot

If you need to add logs to multiple key methods in the main business process to check whether the input parameters and return results of method execution are correct, you can only add Log calls at the beginning of each method to print out each parameter. If the method has a return value, add the Log printout return value before return. If there are multiple if branches in this method for return, you must print Log before each branch return.

The time consumption of statistical method needs to record the time at the beginning of the method, calculate the time before each return and print the Log. Not only tedious, but also easy to miss.

solve

You can mark a comment on the method that you want to print the log, weave code into the method that you want to mark the comment at compile time, and automatically print the input and output information and time-consuming information of the method at run time.

Definition notes
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AutoLog {

    /** logcat Filter Tags */
    String tag();

    /** Print log level (default VERBOSE) */
    LogLevel level() default LogLevel.VERBOSE;
}

AutoLog.java

Customize an annotation AutoLog to mark the method you want to print the log.

Defining cuts and pointcuts
@Aspect
public class LogAspect {

    /**
     * Pointcut, all method bodies with AutoLog annotation added
     */
    @Pointcut("execution (@com.cdh.aop.toys.annotation.AutoLog * *(..))")
    public void logMethodExecute() {
    }
    
    // Advice ···
}

Create a log facet, LogAspect, in which a pointcut is defined to weave code into all methods with AutoLog annotation added.

execution in the pointcut indicates code weaving in the method body@ com.cdh.aop.toys.annotation.AutoLog Indicates the method with the annotation added. The first star indicates unlimited return type, and the second star indicates matching any method name )Represents an unlimited method input parameter.

@Aspect
public class LogAspect {

    // Pointcut ···
    
    /**
     * Weave the method of the pointcut defined above. Around is used to replace the internal code of the original method
     */
    @Around("logMethodExecute()")
    public Object autoLog(ProceedingJoinPoint joinPoint) {
        try {
            // Get the signature information of the woven method. MethodSignature contains the details of the method
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            // Get AutoLog annotation added on method
            AutoLog log = methodSignature.getMethod().getAnnotation(AutoLog.class);
            if (log != null) {
                // For splicing log details
                StringBuilder sb = new StringBuilder();

                // Splicing method name
                String methodName = methodSignature.getMethod().getName();
                sb.append(methodName);

                // Splicing the values of each parameter
                Object[] args = joinPoint.getArgs();
                if (args != null && args.length > 0) {
                    sb.append("(");
                    for (int i=0; i<args.length; i++) {
                        sb.append(args[i]);
                        if (i != args.length-1) {
                            sb.append(",");
                        }
                    }
                    sb.append(")");
                }

                // Record when execution started
                long beginTime = System.currentTimeMillis();
                // Execute the original method code and get the return value
                Object result = joinPoint.proceed();
                // Calculation method execution time
                long costTime = System.currentTimeMillis() - beginTime;

                if (methodSignature.getReturnType() != void.class) {
                    // If the return type of the method is not void, the splicing return value
                    sb.append(" => ").append(result);
                }

                // Splicing time
                sb.append(" | ").append("cost=").append(costTime);

                // Class name and line number of splicing method
                String className = methodSignature.getDeclaringType().getSimpleName();
                int srcLine = joinPoint.getSourceLocation().getLine();
                sb.append(" | [").append(className).append(":").append(srcLine).append("]");

                // Print the Log, and call the corresponding method of Log class with the tag and level set by AutoLog annotation
                LogUtils.log(log.level(), log.tag(), sb.toString());

                return result;
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }
        return null;
    }
}

LogAspect.java

You can use Around to replace the logic in the original method, or you can use ProceedingJoinPoint.proceed Continue with the original method logic. In addition to executing the original method logic, the method parameter information splicing and time-consuming calculation are also carried out, and finally the log output is printed.

Here we have finished a basic function of log facet weaving. Next, we can add comments on the method that we want to print the log automatically.

Use example

Write a few method calls at will, and add AutoLog annotation to these methods.

public class AddOpWithLog extends BaseOp {

    public AddOpWithLog(BaseOp next) {
        super(next);
    }

    @Override
    @AutoLog(tag=TAG, level=LogLevel.DEBUG)
    protected int onOperate(int value) {
        return value + new Random().nextInt(10);
    }
}

AddOpWithLog.java

public class SubOpWithLog extends BaseOp {

    public SubOpWithLog(BaseOp next) {
        super(next);
    }

    @Override
    @AutoLog(tag=TAG, level=LogLevel.WARN)
    protected int onOperate(int value) {
        return value - new Random().nextInt(10);
    }
}

SubOpWithLog.java

public class MulOpWithLog extends BaseOp {

    public MulOpWithLog(BaseOp next) {
        super(next);
    }

    @Override
    @AutoLog(tag=TAG, level=LogLevel.WARN)
    protected int onOperate(int value) {
        return value * new Random().nextInt(10);
    }
}

MulOpWithLog.java

public class DivOpWithLog extends BaseOp {

    public DivOpWithLog(BaseOp next) {
        super(next);
    }

    @Override
    @AutoLog(tag=TAG, level=LogLevel.DEBUG)
    protected int onOperate(int value) {
        return value / (new Random().nextInt(10)+1);
    }
}

DivOpWithLog.java

@AutoLog(tag = BaseOp.TAG, level = LogLevel.DEBUG)
public void doWithLog(View view) {
    BaseOp div = new DivOpWithLog(null);
    BaseOp mul = new MulOpWithLog(div);
    BaseOp sub = new SubOpWithLog(mul);
    BaseOp add = new AddOpWithLog(sub);
    int result = add.operate(100);
    Toast.makeText(this, result+"", Toast.LENGTH_SHORT).show();
}

MainActivity.java

Run the doWithLog method to view the logcat output log:

The effect is as shown in the figure, printing the method name, the value of each input parameter and the direct result return value (if void, the return value will not be printed), and the execution time of the method (in ms).

Thread switching

Thread switching operations are often involved in daily development, such as network requests, file IO and other time-consuming operations that need to be executed in the self thread, and UI operations that need to be executed in the main thread.

Pain spot

Each time you switch a thread, you need to create a Runnable to execute the business logic in its run method, or use AsyncTask and Executor (you need to use Handler to switch back the main thread). You need to add these codes at the method call or inside the method body to switch the thread running.

If a method can be marked, the method can be automatically executed in the main or sub thread, which can make the method calling process clear and greatly reduce the amount of code.

solve

In the same way, we can use annotations to mark methods, and automatically switch threads when the compiler weaves in the code of thread calls.
Note: the implementation scheme here is relatively weak, and only one idea and demonstration is provided.

Definition notes
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AutoThread {
    /**
     * Specifies that the method runs on the main / sub thread
     * Optional enumeration value: MAIN (expected to run on the MAIN thread) BACKGROUND (expected to run on the sub thread)
     */
    ThreadScene scene();
    /**
     * Set whether to block and wait for the method to complete execution before returning (default true)
     */
    boolean waitUntilDone() default true;
}

AutoThread.java
The custom annotation AutoThread is used to mark the method that you want to automatically switch threads.

Defining cuts and pointcuts
@Aspect
public class ThreadAspect {

    @Pointcut("execution (@com.cdh.aop.toys.annotation.AutoThread * *(..))")
    public void threadSceneTransition() {
    }
    
    // Advice ···
}

ThreadAspect.java
A tangent ThreadAspect and pointcut threadSceneTransition are defined here.

execution in the pointcut indicates code weaving in the method body@ com.cdh.aop.toys.annotation.AutoThread Indicates the method with the annotation added. The first star indicates unlimited return type, and the second star indicates matching any method name )Represents an unlimited method input parameter.

@Aspect
public class ThreadAspect {

    // Pointcut ···
    
    @Around("threadSceneTransition()")
    public Object executeInThread(final ProceedingJoinPoint joinPoint) {
        // Result is used to save the execution result of the original method
        final Object[] result = {null};
        try {
            MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
            // Get the method annotation AutoThread we added
            AutoThread thread = methodSignature.getMethod().getAnnotation(AutoThread.class);
            if (thread != null) {
                // Gets the ThreadScene value set in the annotation,
                ThreadScene threadScene = thread.scene();
                if (threadScene == ThreadScene.MAIN && !ThreadUtils.isMainThread()) {
                    // If it is expected to run on the main thread, but it is not currently on the main thread
                    // Switch to main thread execution
                    ThreadUtils.runOnMainThread(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                // Execute the original method and save the results
                                result[0] = joinPoint.proceed();
                            } catch (Throwable throwable) {
                                throwable.printStackTrace();
                            }
                        }
                    }, thread.waitUntilDone());
                } else if (threadScene == ThreadScene.BACKGROUND && ThreadUtils.isMainThread()) {
                    // If you expect to run on a child thread, but are currently on the main thread
                    // Switch to sub thread execution
                    ThreadUtils.run(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                // Execute the original method and save the results
                                result[0] = joinPoint.proceed();
                            } catch (Throwable throwable) {
                                throwable.printStackTrace();
                            }
                        }
                    }, thread.waitUntilDone());
                } else {
                    // Run directly on the current thread
                    result[0] = joinPoint.proceed();
                }
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }
        // Return original method return value
        return result[0];
    }
}

Here, Around is used to replace the original method logic. Before executing the original method, the line thread judges first, and then switches to the corresponding thread to execute the original method.

Thread switching method

As you can see above, when you need to switch the main thread, call ThreadUtils.runOnMainThread To execute the original method, take a look at the internal implementation of this method:

/**
 * Main thread execution
 *
 * @param runnable Tasks to be performed
 * @param block Wait for execution to complete
 */
public static void runOnMainThread(Runnable runnable, boolean block) {
    if (isMainThread()) {
        runnable.run();
        return;
    }

    // Using CountDownLatch to block the current thread
    CountDownLatch latch = null;
    if (block) {
        latch = new CountDownLatch(1);
    }
    // Save Runnable and CountDownLatch with Pair
    Pair<Runnable, CountDownLatch> pair = new Pair<>(runnable, latch);
    // Send Pair parameter to main thread for processing
    getMainHandler().obtainMessage(WHAT_RUN_ON_MAIN, pair).sendToTarget();

    if (block) {
        try {
            // Wait for CountDownLatch to drop to 0
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

private static class MainHandler extends Handler {

    MainHandler() {
        super(Looper.getMainLooper());
    }

    @Override
    public void handleMessage(Message msg) {
        if (msg.what == WHAT_RUN_ON_MAIN) {
            // Take out Pair parameter
            Pair<Runnable, CountDownLatch> pair = (Pair<Runnable, CountDownLatch>) msg.obj;
            try {
                // Take out the Runnable parameter to run
                pair.first.run();
            } finally {
                if (pair.second != null) {
                    // Make CountDownLatch drop to 1, and here it will drop to 0, wake up the previous blocking waiting
                    pair.second.countDown();
                }
            }
        }
    }
}

ThreadUtils.java
The way to switch to the main thread is to use the main thread Handler. If the wait result is returned, the CountDownLatch will be created, blocking the current calling thread and waiting for the main thread to execute the task before returning.

Let's see how to switch the execution of sub threads ThreadUtils.run :

/**
 * Sub thread execution
 *
 * @param runnable Tasks to be performed
 * @param block Wait for execution to complete
 */
public static void run(final Runnable runnable, final boolean block) {
    Future future = getExecutorService().submit(new Runnable() {
        @Override
        public void run() {
            // Running in child thread through thread pool
            runnable.run();
        }
    });

    if (block) {
        try {
            // Waiting for execution results
            future.get();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

To switch to a sub thread is to submit tasks for execution through the thread pool.

Use example

Also write a few methods and add AutoThread annotation

public class AddOpInThread extends BaseOp {

    public AddOpInThread(BaseOp next) {
        super(next);
    }

    @Override
    @AutoThread(scene = ThreadScene.BACKGROUND)
    protected int onOperate(int value) {
        // Print the thread on which the method runs
        Log.w(BaseOp.TAG, "AddOpInThread onOperate: " + java.lang.Thread.currentThread());
        return value + new Random().nextInt(10);
    }
}

AddOpInThread.java
Method annotation specifies to run on a child thread.

public class SubOpInThread extends BaseOp {

    public SubOpInThread(BaseOp next) {
        super(next);
    }

    @Override
    @AutoThread(scene = ThreadScene.MAIN)
    protected int onOperate(int value) {
        // Print the thread on which the method runs
        Log.w(BaseOp.TAG, "SubOpInThread onOperate: " + java.lang.Thread.currentThread());
        return value - new Random().nextInt(10);
    }
}

SubOpInThread.java
Specifies to run on the main thread.

public class MulOpInThread extends BaseOp {

    public MulOpInThread(BaseOp next) {
        super(next);
    }

    @Override
    @AutoThread(scene = ThreadScene.MAIN)
    protected int onOperate(int value) {
        // Print the thread on which the method runs
        Log.w(BaseOp.TAG, "MulOpInThread onOperate: " + java.lang.Thread.currentThread());
        return value * new Random().nextInt(10);
    }
}

MulOpInThread.java
Specifies to run on the main thread.

public class DivOpInThread extends BaseOp {

    public DivOpInThread(BaseOp next) {
        super(next);
    }

    @Override
    @AutoThread(scene = ThreadScene.BACKGROUND)
    protected int onOperate(int value) {
        // Print the thread on which the method runs
        Log.w(BaseOp.TAG, "DivOpInThread onOperate: " + java.lang.Thread.currentThread());
        return value / (new Random().nextInt(10)+1);
    }
}

DivOpInThread.java
Specifies to run on a child thread.

Next, call the method:

public void doWithThread(View view) {
    BaseOp div = new DivOpInThread(null);
    BaseOp mul = new MulOpInThread(div);
    BaseOp sub = new SubOpInThread(mul);
    BaseOp add = new AddOpInThread(sub);
    int result = add.operate(100);
    Toast.makeText(this, result+"", Toast.LENGTH_SHORT).show();
}

MainActivity.java

Run the doWithThread method to view the logcat output log:

You can see that the first method has been switched to run in the sub thread, the second and third methods are running in the main thread, and the fourth method is running in the sub thread.

Thread name detection

Generally, when we create and use a Thread, we need to set a name for it to facilitate the analysis and positioning of the business module to which the Thread belongs.

Pain spot

In the process of development, there are omissions or nonstandard use of threads in the introduced third-party libraries, such as directly creating threads to run, or anonymous threads. When you want to analyze a thread, you will see many threads of Thread-1, 2 and 3. If you have a clear name, you can easily see the business that the thread belongs to at a glance.

solve

By intercepting all Thread.start Call time to detect the thread name before start. If it is the default name, a warning is given and the thread name is automatically modified.

Defining cuts and pointcuts

Here, thread related weaving operations are all placed in a tangent ThreadAspect:

@Aspect
public class ThreadAspect {

    private static final String TAG = "ThreadAspect";
    
    @Before("call (* java.lang.Thread.start(..))")
    public void callThreadStart(JoinPoint joinPoint) {
        try {
            // Get the object of joinPoint, that is, the Thread instance that executes the start method
            Thread thread = (Thread) joinPoint.getTarget();
            // Thread name detected by regular
            if (ThreadUtils.isDefaultThreadName(thread)) {
                // Print warning information (thread object and location of the method call)
                LogUtils.e(TAG, "Discovery startup thread[" + thread + "]Thread name not customized! [" + joinPoint.getSourceLocation() + "]");
                // Set the thread name and splice the context this object at the method call
                thread.setName(thread.getName() + "-" + joinPoint.getThis());
            }
        } catch (Throwable t) {
            t.printStackTrace();
        }
    }
}

ThreadAspect.java

Before means weaving in before the pointcut, call means calling the method, and the first star means unlimited return type, java.lang.Thread.start means the start method that exactly matches the thread class ( )Represents an unlimited method parameter.

This pointcut will be called in all thread.start Weaves in the name detection and name setting code in front of the place.

Thread name detection

If the thread has no name set, the default name will be used. You can see the construction method of the thread.

/* For autonumbering anonymous threads. */
private static int threadInitNumber;
private static synchronized int nextThreadNum() {
    return threadInitNumber++;
}

public Thread() {
    // The third parameter is the default name
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

When creating a Thread, a default name will be set, and the Thread number will be incremented. Therefore, you can determine whether the Thread has a custom name by matching the name.

see ThreadUtils.isDefaultThreadName method:

public static boolean isDefaultThreadName(Thread thread) {
    String name = thread.getName();
    String pattern = "^Thread-[1-9]\\d*$";
    return Pattern.matches(pattern, name);
}

Judging by regular expression, if the match is complete, it means that the current name is the default name.

Use example

Create several threads, set the name and unset the name respectively, and then start the run.

public void renameThreadName(View view) {
    // Name not set
    new Thread(new PrintNameRunnable()).start();

    // Set name
    Thread t = new Thread(new PrintNameRunnable());
    t.setName("myname-thread-test");
    t.start();
}

private static class PrintNameRunnable implements Runnable {
    @Override
    public void run() {
        // Print thread name
        Log.d(TAG, "thread name: " + Thread.currentThread().getName());
    }
}

To view the logcat output log after running:


You can see that a thread is detected to start without a custom name set and the method call location is printed out.


When the thread is started, print the current thread name in Runnable, you can see that the thread name has been set, and you can know the context in which the thread is started.

Inspection by MIIT

The Ministry of industry and information technology issued a document to require APP not to collect user and device related information, such as imei, device id, device installed application list, address book and other information that can uniquely identify user and user device privacy before users agree to the privacy agreement.

Note that the user consent privacy agreement here is different from the APP permission application and belongs to the privacy agreement at the business level. If the user does not agree to the privacy agreement, even if all the permissions of the APP are opened in the system application settings, the relevant information cannot be obtained in the business code.

As shown in the figure, only with the user's consent can the required information be obtained in the business code.

Pain spot

It is easy to neglect to check all the places in the code that involve the acquisition of privacy information. In case of any omission, it will be punished by the Ministry of industry and information technology. Moreover, some of the three-party SDK s do not strictly follow the requirements of the Ministry of industry and information technology, and will privately obtain information about users and equipment.

solve

Check code can be woven in front of all places calling the privacy information API, covering its own business code and three-party SDK code to intercept.

Note that the call behavior in dynamically loaded code and the behavior in the native layer cannot be completely intercepted.

Intercept API direct calls
@Aspect
public class PrivacyAspect {

    // Intercept the call to obtain the list information of mobile phone installation applications
    private static final String POINT_CUT_GET_INSTALLED_APPLICATION = "call (* android.content.pm.PackageManager.getInstalledApplications(..))";
    private static final String POINT_CUT_GET_INSTALLED_PACKAGES = "call (* android.content.pm.PackageManager.getInstalledPackages(..))";

    // Intercept the call to get imei and device id
    private static final String POINT_CUT_GET_IMEI = "call (* android.telephony.TelephonyManager.getImei(..))";
    private static final String POINT_CUT_GET_DEVICE_ID = "call(* android.telephony.TelephonyManager.getDeviceId(..))";

    // Intercept the call of getLine1Number method
    private static final String POINT_CUT_GET_LINE_NUMBER = "call (* android.telephony.TelephonyManager.getLine1Number(..))";

    // Intercept calls to location
    private static final String POINT_CUT_GET_LAST_KNOWN_LOCATION = "call (* android.location.LocationManager.getLastKnownLocation(..))";
    private static final String POINT_CUT_REQUEST_LOCATION_UPDATES = "call (* android.location.LocationManager.requestLocationUpdates(..))";
    private static final String POINT_CUT_REQUEST_LOCATION_SINGLE = "call (* android.location.LocationManager.requestSingleUpdate(..))";
    
    // ···
    
    @Around(POINT_CUT_GET_INSTALLED_APPLICATION)
    public Object callGetInstalledApplications(ProceedingJoinPoint joinPoint) {
        return handleProceedingJoinPoint(joinPoint, new ArrayList<ApplicationInfo>());
    }

    @Around(POINT_CUT_GET_INSTALLED_PACKAGES)
    public Object callGetInstalledPackages(ProceedingJoinPoint joinPoint) {
        return handleProceedingJoinPoint(joinPoint, new ArrayList<PackageInfo>());
    }

    @Around(POINT_CUT_GET_IMEI)
    public Object callGetImei(ProceedingJoinPoint joinPoint) {
        return handleProceedingJoinPoint(joinPoint, "");
    }

    @Around(POINT_CUT_GET_DEVICE_ID)
    public Object callGetDeviceId(ProceedingJoinPoint joinPoint) {
        return handleProceedingJoinPoint(joinPoint, "");
    }

    @Around(POINT_CUT_GET_LINE_NUMBER)
    public Object callGetLine1Number(ProceedingJoinPoint joinPoint) {
        return handleProceedingJoinPoint(joinPoint, "");
    }

    @Around(POINT_CUT_GET_LAST_KNOWN_LOCATION)
    public Object callGetLastKnownLocation(ProceedingJoinPoint joinPoint) {
        return handleProceedingJoinPoint(joinPoint, null);
    }

    @Around(POINT_CUT_REQUEST_LOCATION_UPDATES)
    public void callRequestLocationUpdates(ProceedingJoinPoint joinPoint) {
        handleProceedingJoinPoint(joinPoint, null);
    }

    @Around(POINT_CUT_REQUEST_LOCATION_SINGLE)
    public void callRequestSingleUpdate(ProceedingJoinPoint joinPoint) {
        handleProceedingJoinPoint(joinPoint, null);
    }
    
    // ···
}

PrivacyAspect.java

Define a faceted PrivacyAspect, and the pointcut for the method that needs to be checked for calls. The code calling sensitive API is replaced by Around, and handleProceedingJoinPoint is called for processing. The first parameter is the connection point ProceedingJoinPoint, and the second parameter is the default return value (if the original method has a return value, the result will be returned).

Next, enter the handleProceedingJoinPoint method:

private Object handleProceedingJoinPoint(ProceedingJoinPoint joinPoint, Object fakeResult) {
    if (!PrivacyController.isUserAllowed()) {
        // If the user does not agree
        StringBuilder sb = new StringBuilder();
        // Print the method and location of the call
        sb.append("Executed when the user did not agree").append(joinPoint.getSignature().toShortString())
                .append(" [").append(joinPoint.getSourceLocation()).append("]");
        LogUtils.e(TAG,  sb.toString());
        // Returns an empty default value
        return fakeResult;
    }

    try {
        // Execute the original method and return the original result
        return joinPoint.proceed();
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }
    return fakeResult;
}

In this method, whether users agree or not is judged. If not, a null return value is returned. Otherwise, release and call the original method.

Intercept API reflection calls

Some third-party SDK s call sensitive API s through reflection and encrypt method name strings to bypass static checks. Therefore, reflection calls need to be intercepted.

@Aspect
public class PrivacyAspect {
    
    // Intercept reflected calls
    private static final String POINT_CUT_METHOD_INVOKE = "call (* java.lang.reflect.Method.invoke(..))";
    // Reflection method blacklist
    private static final List<String> REFLECT_METHOD_BLACKLIST = Arrays.asList(
            "getInstalledApplications",
            "getInstalledPackages",
            "getImei",
            "getDeviceId",
            "getLine1Number",
            "getLastKnownLocation",
            "loadClass"
    );
    
    @Around(POINT_CUT_METHOD_INVOKE)
    public Object callReflectInvoke(ProceedingJoinPoint joinPoint) {
        // Gets the method name called by the connection point
        String methodName = ((Method) joinPoint.getTarget()).getName();
        if (REFLECT_METHOD_BLACKLIST.contains(methodName)) {
            // Check if the method in the blacklist
            return handleProceedingJoinPoint(joinPoint, null);
        }

        try {
            // Execute the original method and return the original result
            return joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return null;
    }
}

Through interception Method.invoke To determine whether the method called by reflection is a method in the blacklist.

Intercept dynamically loaded calls
@Aspect
public class PrivacyAspect {

    // Intercept calls to load classes
    private static final String POINT_CUT_DEX_FIND_CLASS = "call (* java.lang.ClassLoader.loadClass(..))";
    
    @Around(POINT_CUT_DEX_FIND_CLASS)
    public Object callLoadClass(ProceedingJoinPoint joinPoint) {
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }

        // Print information about this connection point
        StringBuilder sb = new StringBuilder();
        sb.append(joinPoint.getThis()).append("Dynamic loading in");
        Object[] args = joinPoint.getArgs();
        if (args != null && args.length > 0) {
            sb.append("\"").append(args[0]).append("\"");
        }
        sb.append("obtain").append(result);
        sb.append(" ").append(joinPoint.getSourceLocation());
        LogUtils.w(TAG, sb.toString());

        return result;
    }
}

When the loadClass is intercepted, print the location of the log output call.

Use example

public void interceptPrivacy(View view) {
    Log.d(TAG, "User consent: " + PrivacyController.isUserAllowed());

    // Get mobile phone installation application information
    List<ApplicationInfo> applicationInfos = DeviceUtils.getInstalledApplications(this);
    if (applicationInfos != null && applicationInfos.size() > 5) {
        applicationInfos = applicationInfos.subList(0, 5);
    }
    Log.d(TAG, "getInstalledApplications: " + applicationInfos);

    // Get mobile phone installation application information
    List<PackageInfo> packageInfos = DeviceUtils.getInstalledPackages(this);
    if (packageInfos != null && packageInfos.size() > 5) {
        packageInfos = packageInfos.subList(0, 5);
    }
    Log.d(TAG, "getInstalledPackages: " + packageInfos);

    // Get imei
    Log.d(TAG, "getImei: " + DeviceUtils.getImeiValue(this));
    // Get phone number
    Log.d(TAG, "getLine1Number: " + DeviceUtils.getLine1Number(this));
    // Get location information
    Log.d(TAG, "getLastKnownLocation: " + DeviceUtils.getLastKnownLocation(this));

    try {
        // Load a class
        Log.d(TAG, "loadClass: " + getClassLoader().loadClass("com.cdh.aop.sample.op.BaseOp"));
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }

    try {
        // Get mobile phone installation application information through reflection
        PackageManager pm = getPackageManager();
        Method method = PackageManager.class.getDeclaredMethod("getInstalledApplications", int.class);
        List<ApplicationInfo> list = (List<ApplicationInfo>) method.invoke(pm, 0);
        if (list != null && list.size() > 5) {
            list = list.subList(0, 5);
        }
        Log.d(TAG, "reflect getInstalledApplications: " + list);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

To view the logcat output log after running:


Printed warning information of sensitive API call and location of call.


The caller finally gets null values.

End

In integration AspectJX framework After packaging apk, you may encounter ClassNotFoundException. Decompiling apk finds that many classes are not typed in, even Application. Most of the reasons are the conflicts caused by the use of AspectJ framework in the dependent tripartite library, or the syntax of the pointcut written by oneself is wrong, or there is a problem in the woven code, for example, the return value of the method does not correspond, or conflicting notifications are defined for the same pointcut. If an error occurs, an error message is displayed in build.

If AOP and AspectJ framework are not used to implement the above requirements, there will be a lot of tedious work. Through the application of several simple scenarios, we can find that if we can understand the AOP idea in depth and master the use of AspectJ, it will greatly improve the efficiency of architecture design and development.

For the complete source code of the example in this paper, see Efficiency-Toys

Topics: Java Android Mobile SDK