跪拜 Guibai
← Back to the summary

AspectJ Bytecode Weaving Replaces Boilerplate Network Checks on Android

Background: Starting from a network disconnection prompt

In older versions of JD.com's app, when the network was disconnected, all button clicks simply had no response. It wasn't like now, where clicking still navigates to the page, but a placeholder or prompt for no network is shown.

How is this achieved?

Do you have to add a check inside every button's click callback? Like this:

Button(
    onClick = {
        if (isNetworkDisconnected()) {
            return@Button
        }

        onButtonClick()
    },
) {
    Text("An ordinary button")
}

// This code is in MainActivity
private fun onButtonClick() {
    Toast.makeText(this, "Network available, executing business logic", Toast.LENGTH_SHORT).show()
}

private fun isNetworkDisconnected(): Boolean {
    // Determine if network is disconnected
    return false
}

You don't need to do this. Writing this for every button is tedious and cumbersome. We have a more elegant way: Aspect-Oriented Programming (AOP).

What is AOP?

AOP is a programming paradigm that extracts common behaviors (such as network checks, logging, transaction handling) from the main business logic, so that we can add extra functionality before and after code execution without modifying the original business code.

The familiar dynamic proxies and lifecycle listeners actually apply this idea. Next, I will use another framework that takes this idea to the extreme—AspectJ—to implement the global network disconnection interception feature mentioned at the beginning.

Introducing and Configuring AspectJ

First, introduce the plugin and dependency in build.gradle.kts (Module :app):

import io.freefair.gradle.plugins.android.aspectj.AjcWeave
import java.util.Properties

plugins {
    // Post-compile weaving plugin (specific to Android projects)
    id("io.freefair.android.aspectj.post-compile-weaving") version "8.13.0"
}

dependencies {
    // AspectJ core runtime library
    implementation("org.aspectj:aspectjrt:1.9.22")
}

// AjcWeave needs android.jar to resolve android.* types during weaving
tasks.withType<AjcWeave>().configureEach {
    val localProps = Properties().apply {
        rootProject.file("local.properties").takeIf { it.exists() }?.inputStream()?.use { load(it) }
    }
    val sdkDir = file(
        localProps.getProperty("sdk.dir")
            ?: System.getenv("ANDROID_HOME")
            ?: System.getenv("ANDROID_SDK_ROOT")
            ?: error("Android SDK not found (set sdk.dir / ANDROID_HOME)")
    )
    // Note: I am using API 37 here; you can modify it according to your project's actual compileSdk version
    val androidJar = sdkDir.resolve("platforms").listFiles()
        ?.filter { it.isDirectory && it.name.startsWith("android-37") }
        ?.sortedByDescending { it.name }
        ?.map { it.resolve("android.jar") }
        ?.firstOrNull { it.isFile }
        ?: error("android.jar for API 37 not found under ${sdkDir.resolve("platforms")}")

    ajcOptions {
        bootclasspath.from(androidJar)
        compilerArgs.add("-Xlint:cantFindType=ignore")
    }
}

This plugin runs after the Android Gradle Plugin (AGP) finishes, then modifies the already compiled bytecode in its own weaving task.

Due to current compatibility limitations of the freefair plugin, we also need to disable configuration cache in gradle.properties:

# freefair AspectJ AjcWeave task does not yet support configuration cache
org.gradle.configuration-cache=false

Defining Pointcuts and Processing Logic

Just like with reflection, we need an anchor (annotation) so the system knows which code to modify.

First, define a custom annotation:

@Retention(AnnotationRetention.BINARY) // Retain in bytecode so the plugin can see it
@Target(AnnotationTarget.FUNCTION) // Apply to methods
annotation class NetworkCheck

Next, add the aspect class that finds the anchor and handles the interception logic:

@Aspect // Declare this as an aspect class, to be processed by AspectJ
class SectionAspect {

    /**
     * Find the pointcut (annotation) to process
     *
     * `* *(..)` means intercept all matched methods
     */
    @Pointcut("execution(@com.example.aoplearn.NetworkCheck * *(..))")
    fun networkCheckBehavior() {
    }

    /**
     * Process the aspect, i.e., the logic added before and after
     *
     * Check the network before method execution; prompt and intercept if disconnected, proceed if connected
     */
    @RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
    @Around("networkCheckBehavior()")
    fun networkCheckPoint(joinPoint: ProceedingJoinPoint): Any? {
        // Get the object to which the intercepted method belongs; here it is MainActivity
        val context = getContext(joinPoint.target) ?: return joinPoint.proceed()

        val hasNetwork = isNetworkAvailable(context)
        if (!hasNetwork) {
            Toast.makeText(context, "Network unavailable, please check your connection", Toast.LENGTH_SHORT).show()
            // Intercept, return null directly, do not execute the original method
            return null
        }
        // Proceed, execute the original method
        return joinPoint.proceed()
    }

    /**
     * Get context from the object
     */
    private fun getContext(any: Any): Context? =
        when (any) {
            is Activity -> {
                any as? Context
            }
            // Note: Pure Compose environments do not have AndroidX Fragments by default, so this is commented out
//            is Fragment -> {
//                any.activity as? Context
//            }

            is View -> {
                any.context
            }

            else -> null
        }


    /**
     * Determine if the network is available
     */
    @RequiresPermission(Manifest.permission.ACCESS_NETWORK_STATE)
    private fun isNetworkAvailable(context: Context): Boolean {
        val connectivityManager =
            context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        val network = connectivityManager.activeNetwork ?: return false
        val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false
        return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
    }
}

Finally, don't forget to add the network state access permission in AndroidManifest.xml: <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

After this, we only need to add the @NetworkCheck annotation anywhere a network check is required, and the network disconnection interception is automatically implemented, without writing repetitive check code.

Revealed: How is it implemented?

Compile-time "Weaving"

How is this operation implemented? Is it reflection?

No. As we mentioned earlier, it is achieved by modifying bytecode.

The entire process actually happens during the project's build phase: the AspectJ compiler (ajc) intervenes in the compilation process, scanning and finding all pointcuts annotated with @NetworkCheck via the @Pointcut annotation.

Then, when generating the .class bytecode files, it directly "copies and pastes" our aspect code (the method annotated with @Around) into these pointcut methods, thus achieving seamless logic replacement.

Verification via Decompilation

Let's verify through decompilation whether the bytecode has been modified:

  1. First, build a Release package.

  2. Then use jadx to decompile this APK file. (Download link: Release 1.5.6 · skylot/jadx)

After decompilation, you can see that the internal logic of our original click method onHelloWorldBtnClick() has become this:

private final void onHelloWorldBtnClick() {
    JoinPoint joinPointMakeJP = Factory.makeJP(ajc$tjp_0, this, this);
    onHelloWorldBtnClick_aroundBody1$advice(this, joinPointMakeJP, SectionAspect.aspectOf(), (ProceedingJoinPoint) joinPointMakeJP);
}

And the original actual business logic and aspect logic have been extracted and replaced into the following two static methods:

// The original business logic has been extracted into this _aroundBody0 method
private static final /* synthetic */ void onHelloWorldBtnClick_aroundBody0(MainActivity mainActivity, JoinPoint joinPoint) {
    Toast.makeText(mainActivity, "Network available, executing business logic", 0).show();
}

// The woven-in aspect logic
private static final /* synthetic */ Object onHelloWorldBtnClick_aroundBody1$advice(MainActivity mainActivity, JoinPoint joinPoint, SectionAspect sectionAspect, ProceedingJoinPoint joinPoint2) {
    Intrinsics.checkNotNullParameter(joinPoint2, "joinPoint");
    Object target = joinPoint2.getTarget();
    Intrinsics.checkNotNullExpressionValue(target, "getTarget(...)");
    
    // Corresponds to getContext in the aspect
    Context context = sectionAspect.getContext(target);

    if (context == null) {
        onHelloWorldBtnClick_aroundBody0(mainActivity, joinPoint2);
        return null;
    }

    // Corresponds to the network check in the aspect
    if (!sectionAspect.isNetworkAvailable(context)) {
        Toast.makeText(context, "Network unavailable, please check your connection", 0).show();
        return null;
    }
    
    // Network available, proceed to execute the original business logic
    onHelloWorldBtnClick_aroundBody0(mainActivity, joinPoint2);
    return null;
}

Summary

AOP can highly decouple code, and it has many application scenarios, such as:

It also has drawbacks: first, it increases compilation time; second, when bugs occur, the debugging chain becomes longer.

Comments

Top 1 of 6 from juejin.cn, machine-translated. The original thread is authoritative.

KawaNull 1 likes

Before, I was just like you, but now I wouldn't really recommend using this thing. Young man, give it up.

雨白

AspectJ, you mean, or something else?

KawaNull  → 雨白  · 1 likes

Yeah, I used to use AspectJ for handling permissions and event tracking too, but not anymore. Just using a singleton is more practical—check the method reference and you immediately know where it is.