Fundamentals 22 min read

Mastering AspectPro: A Lightweight HarmonyOS Hook Framework for Seamless AOP

AspectPro is a lightweight HarmonyOS runtime hook framework that, together with aspectProPlugin, enables comprehensive AOP capabilities such as method interception, parameter manipulation, and handling of non-writable properties, while the article details its core functions, limitations of system Aspect, and step‑by‑step implementation strategies.

Huolala Tech
Huolala Tech
Huolala Tech
Mastering AspectPro: A Lightweight HarmonyOS Hook Framework for Seamless AOP

1. What is AspectPro

AspectPro

is a lightweight HarmonyOS runtime hook framework (used together with aspectProPlugin to achieve most code hooking).

The core capabilities of AspectPro include:

1. Align with HarmonyOS system Aspect capabilities

2. Simplify usage, no need to distinguish static methods

3. Support hooking method behavior (e.g., Button onClick events)

4. Support hooking methods inside inner classes (e.g., HttpClient.Builder().build())

5. Support hooking methods with writeable = false using aspect‑pro‑plugin (e.g., router.pushUrl)

6. Support hooking methods and altering parameters or return values aspect-pro-plugin is a lightweight HarmonyOS compile‑time code modification framework.

1. Support scanning specified folders/files: -hook xxx

2. Support keeping specified folders/files: -keep xxx

3. Support replace‑hook code in specified folder/file: -replace xxx to yyy

4. Support automatic import insertion during replace: -replace xxx to yyy [import xxx import bbb]

5. Support extension (aspectProPluginHvigorfileCode is plugin source, rename to hvigorfile for local development)

6. Support custom configuration rules (see aspectProPluginConfig.txt)

GitHub project and documentation: AspectPro

2. What is AOP?

Aspect‑Oriented Programming (AOP) is a technique that uses pre‑compilation and runtime dynamic proxies to achieve unified maintenance of program functionality. Its core idea is to separate concerns by inserting code that handles cross‑cutting concerns, thereby reducing coupling, improving maintainability, reusability, and development efficiency.

3. Current Situation – System AOP Related APIs

HarmonyOS implements AOP mainly through instrumentation, providing the Aspect class with addBefore , addAfter , and replace interfaces. These allow pre‑, post‑insertion and replacement of class methods at runtime, offering flexible operation for various business scenarios.

4. Why Develop AspectPro?

Core reasons: Android projects use AOP for monitoring injection; during HarmonyOS migration, system Aspect APIs cannot directly achieve certain scenarios (e.g., hooking button click callbacks, network requests, router navigation). AspectPro is created to fill these gaps and share the experience. Monitoring scenarios: App foreground/background switch Page lifecycle Control click events Network requests Page router navigation …

AspectPro must at least support the following capabilities:

Support system Aspect capabilities Support hooking specific function‑parameter methods (e.g., Button#onClick ClickEvent) Support hooking methods that return different class instances each time (e.g., HttpClient.Builder().build()) Support hooking methods with writeable = false (e.g., router.pushUrl)

5. Why System Aspect Cannot Meet These Requirements?

5.1 Why Hooking Control Click Events Fails

util.Aspect.addBefore(Button, "onClick", true, () => {
  Logger.w(TAG, "1.util.Aspect add before ---- Button#onClick() , do your business ...");
});

The code successfully inserts a hook before Button.onClick, but the actual click event registers the callback ck inside onClick. Therefore the hook needs to target ck, not onClick.

Summary: The real target is the callback function passed to Button.onClick , not the method itself.

5.2 Why Hooking Network Requests Fails

Background: HarmonyOS network library is based on Axios & HttpClient. HttpClient provides EventListener for timing and Interceptor for request/response data. Hooking HttpClient.Builder could add listeners and interceptors, but each new HttpClient.Builder() creates a new class definition without a unified prototype, and the methods are instance methods.
// pseudo code
util.Aspect.addBefore(HttpClient.Builder, "build", false, (instance) => {
  instance?.addEventListener(new MyEventListener2(instance?._eventListeners));
  instance?.addInterceptor(new MyInterceptor());
});

Because each builder instance has its own class definition, there is no common prototype to hook, so system Aspect cannot handle this scenario.

Summary: System Aspect works only for properties with a unified prototype; the builder scenario does not satisfy this.

5.3 Why Hooking Router Navigation Fails

Current code uses HarmonyOS API router.pushXXX . Directly hooking router.pushUrl fails because router is a namespace, not a class, and its properties are non‑writable.
// pseudo code
import router from '@ohos.router';
util.Aspect.addBefore(router, "pushUrl", true, () => {
  Logger.w(TAG, "1.util.Aspect add before ---- Router#pushUrl() , do your business ...");
});

The underlying reason is that the router object's properties cannot be overwritten, so any attempt to replace them silently fails.

Summary: Router object properties are non‑writable, preventing hook injection.

6. How AspectPro Extends to Implement the Required Functions

Key knowledge points: Prototype chain Property definition Hvigor compilation flow & plugin development

6.1 Support System Aspect Capabilities

static addAfter(target, action, fn): void {
  AspectPro.wrapMethod(target, action, (origin) => function (...args) {
    let result = origin.apply(this, args);
    fn.apply(this, args);
    return result;
  });
}

static addBefore(target, action, fn): void {
  AspectPro.wrapMethod(target, action, (origin) => function (...args) {
    fn.apply(this, args);
    return origin.apply(this, args);
  });
}

static replace(target, action, fn): void {
  AspectPro.wrapMethod(target, action, () => function (...args) {
    return fn.apply(this, args);
  });
}

private static wrapMethod(target, action, wrapper) {
  let origin = target.prototype[action] || target[action];
  if (origin) {
    let isPrototype = !!target.prototype[action];
    let destination = isPrototype ? target.prototype : target;
    destination[action] = wrapper(origin);
  } else {
    Logger.e(HOOK_TAG, `hook failed originMethod:${origin}`);
  }
}
Principle: In ECMAScript, static methods are class properties, while instance methods reside on the prototype.

6.2 Hook Specific Function‑Parameter Methods

function hookMethod(target, action, beforeFn?, afterFn?) {
  wrapMethod(target, action, (originalMethod) => function (callback) {
    const wrappedCallback = (...args) => {
      beforeFn?.apply(this, args);
      callback.apply(this, args);
      afterFn?.apply(this, args);
      if (!beforeFn && !afterFn) {
        Logger.w(TAG, `hookMethodAction()->${target + '->' + action}, but do nothing, just log ???`);
      }
    };
    originalMethod.call(this, wrappedCallback);
  });
}

6.3 Hook Methods Returning Different Class Instances

function hookMethod(target: any, propertyName: string, methodName: string, beforeFn?, afterFn?) {
  const propertyDescriptor = Object.getOwnPropertyDescriptor(target, propertyName);
  if (propertyDescriptor && propertyDescriptor.get) {
    Object.defineProperty(target, propertyName, {
      get() {
        const originTarget = propertyDescriptor.get!.call(this);
        const originMethod = originTarget.prototype[methodName];
        originTarget.prototype[methodName] = function (...args: any[]) {
          beforeFn?.call(this, this, args);
          let result = originMethod.apply(this, args);
          afterFn?.call(this, this, args, result);
          if (!beforeFn && !afterFn) {
            Logger.w(TAG, `hookMethodProperty()->${target.name}: Modified ${propertyName}.${methodName}() , but do nothing ,just log ???`);
          }
          return result;
        };
        return originTarget;
      }
    });
  } else {
    Logger.e(TAG, `hookMethodProperty()->Property ${propertyName} is not a getter on target class`);
  }
}

6.4 Hook Non‑Writable Properties (e.g., router.pushUrl)

Solution: Use compile‑time instrumentation to replace the non‑writable code with a writable counterpart, then apply runtime hooks on the replaced code.

6.5 aspectProPlugin Workflow

The plugin reads aspectProPluginConfig.txt to determine files and replacement rules, injects modifications before compilation, compiles to .abc, then resets the source files to their original state.

/* AspectPro‑Plugin principle (source rollback scheme) */
function aspectProPlugin(): HvigorPlugin {
  return {
    pluginId: 'localAspectProPlugin',
    apply: (node) => {
      node.registerTask({
        name: 'aspectProPluginInjectTask',
        run: (taskContext) => {
          filesAndRules = injectAspectProPlugin(node);
        },
        postDependencies: ['default@CompileArkTS']
      });
      node.registerTask({
        name: 'resetAspectProPluginInjectTask',
        run: () => {
          reInjectAspectProPlugin(filesAndRules.allFiles, filesAndRules.replaceRules);
        },
        dependencies: ['default@CompileArkTS'],
        postDependencies: ['assembleHap']
      });
    }
  };
}

Configuration file example ( aspectProPluginConfig.txt) defines -hook, -keep, and -replace directives to specify which files to modify and how.

7. AspectPro SDK & Plugin Quick Start

1. Add SDK dependency: ohpm i @huolala/aspectpro 2. Call hookMethod API

// 1. hook HttpClient#Builder()#build()
AspectPro.hookMethod({
  target: router,
  methodNameOrProperty: 'pushUrl',
  beforeFn: () => {
    Logger.w(TAG, "AspectPro hookedMethod-> before ---- Router#pushUrl() , do your business ...");
  }
});

// 2. hook router#pushUrl()
AspectPro.hookMethod({
  target: HttpClient,
  methodNameOrProperty: 'Builder',
  beforeFn: (context, args) => {
    const builderContext = context as InstanceType<typeof HttpClient.Builder>;
    builderContext._eventListeners = new MyEventListener();
    builderContext.addInterceptor(new MyInterceptor());
  },
  propertyMethodNameOrType: 'build'
});

// 3. hook TestClass1#a()
AspectPro.hookMethod({
  target: TestClass1,
  methodNameOrProperty: 'a',
  afterFn: () => {
    Logger.w(TAG, "AspectPro hookedMethod-> afterFn ---- TestClass1#a() , do your business ...");
  }
});

AspectPro.addBefore(Button, "onClick", () => {
  Logger.w(TAG, "1.AspectPro add before ---- Button#onClick()#action , do your business ...");
}, true);

AspectPro.addAfter(TestClass1, "b", () => {
  Logger.w(TAG, "1.AspectPro add after ---- TestClass1#b() , do your business ...");
});

AspectPro.replace(TestClass1, "c", (origin, ...args) => {
  // modify parameters
  let changedArgs = [...args];
  changedArgs[0] = new String("change param 1");
  // call original method
  const result = origin(...changedArgs);
  Logger.w(TAG, "1.AspectPro replace ---- TestClass1#c() , do your business ... result:" + result);
  // optionally modify return value
  return result;
});

After adding the plugin dependency and configuration file, developers can invoke the above APIs to achieve seamless AOP across HarmonyOS applications.

AOPHarmonyOSPlugin DevelopmentRuntime InstrumentationAspectProHook Framework
Huolala Tech
Written by

Huolala Tech

Technology reshapes logistics

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.