AspectD

AspectD 是一个用于 Dart 的 AOP(面向切面编程)框架。与其他传统的 AOP 框架一样,AspectD 提供了 call&execute 语法。此外,由于我们在 Flutter 中无法使用 dart:mirrors,AspectD 还提供了一种称为 inject 的方法来增强 Dart 代码操作。

此外,AspectD 在一个 dill transformer 容器中提供了这些功能,开发者可以在其上实现自己的 transformer,例如 hook、json、mirrors 等。

设计

Aspectd Diagram

假设您有一个名为 example 的 Flutter 项目,位于 hf_dir。

安装

1. 在 hf_dir/example 中创建一个名为 aspectd_impl 的 dart 包

flutter create --template=package aspectd_impl

2. 将 aspectd&example 依赖添加到 aspectd_impl 包

dependencies:
  flutter:
    sdk: flutter
  aspectd:
    git:
      url: [email protected]:XianyuTech/aspectd.git
      ref: stable/v2.2.3
  example:
    path: ../example

请记住,更改分支以匹配您的 Flutter 环境(目前支持 stable 分支)。
在 aspectd_impl 包中获取包依赖

flutter packages get

3. 修改 aspectd_impl 包

aspectd_impl.dart(入口点)

import 'package:example/main.dart' as app;
import 'aop_impl.dart';

void main()=> app.main();

aop_impl.dart(AOP 实现)

import 'package:aspectd/aspectd.dart';

@Aspect()
@pragma("vm:entry-point")
class ExecuteDemo {
  @pragma("vm:entry-point")
  ExecuteDemo();

  @Execute("package:example/main.dart", "_MyHomePageState", "-_incrementCounter")
  @pragma("vm:entry-point")
  void _incrementCounter(PointCut pointcut) {
    pointcut.proceed();
    print('KWLM called!');
  }
}

4. 修补 flutter_tools 以应用 aspectd.dart.snapshot

cd path-for-flutter-git-repo
git apply --3way path-for-aspectd-package/0001-aspectd.patch
rm bin/cache/flutter_tools.stamp

在 Windows 上,请使用 "git am --reject --whitespace=fix aspectd\0001-aspectd.patch" 来应用补丁。

由于 flutter_tools 目前不支持 hooks,因此 aspectd.patch 目前是必需的。随着 Flutter 的发展,此补丁有时可能会失败。但是,解决冲突会很简单,因为 AspectD 在构建 dill 时只添加两个 hooks。更多信息请参见 https://github.com/alibaba-flutter/aspectd/issues/5

如果您想自定义 aspectd_impl 包,请编辑 path-for-flutter-git-repo/flutter/packages/flutter_tools/lib/src/aspectd.dart 中定义的 aspectdImplPackageRelPath(aspectd_impl 包相对于 example 的 pubspec.yaml 的相对路径)和 aspectdImplPackageName(aspectd_impl 包文件夹名称和主入口文件名)。

const String aspectdImplPackageRelPath = '..';
const String aspectdImplPackageName = 'aspectd_impl';

步骤 1-3 是每次想将 aspectd_impl 添加到 Flutter(Dart)包时需要运行的。步骤 4 只需要运行一次,除非 dart-sdk 发生变化。例如,如果您升级了 Flutter,则需要检查是否需要重新运行步骤 4。

如果您使用的是不包含本地生成的 aspectd_impl 包的 example,请记住在 aspectd_impl 包中运行 flutter packages get 来获取 aspectd 并检查步骤 4。

如果您想要的 Flutter 版本尚未支持,请参见 UPGRADE.md 以获取更多信息。

教程

现在 AspectD 提供了三种进行 AOP 编程的方法。

call

特定函数的每个调用点都将被操作。

import 'package:aspectd/aspectd.dart';

@Aspect()
@pragma("vm:entry-point")
class CallDemo{
  @Call("package:app/calculator.dart","Calculator","-getCurTime")
  @pragma("vm:entry-point")
  Future<String> getCurTime(PointCut pointcut) async{
    print('Aspectd:KWLM02');
    print('${pointcut.sourceInfos.toString()}');
    Future<String> result = pointcut.proceed();
    String test = await result;
    print('Aspectd:KWLM03');
    print('${test}');
    return result;
  }

  @Call("package:app/calculator.dart","Calculator","+getCurTemporature")
  @pragma("vm:entry-point")
  String getCurTemporature(PointCut pointcut) {
    print('Aspectd:KWLM04');
    print('${pointcut.sourceInfos.toString()}');
    try{
      String res = pointcut.proceed();
    } catch (error, trace){
      print('Aspectd:KWLM05');
    }
    return null;
  }

  @Call("package:flutter/src/widgets/binding.dart","","+runApp")
  @pragma("vm:entry-point")
  static void runAppKWLM(PointCut pointcut){
    print('Aspectd:KWLM07');
    print('${pointcut.sourceInfos.toString()}');
    pointcut.proceed();
  }
}

在这种情况下,请注意需要使用 @Aspect() 来标记一个类,以便 aspectd 知道该类包含 AspectD 注释信息。
需要 @pragma("vm:entry-point"),以便类/函数不会被 tree-shaking 移除。
对于 @Call("package:app/calculator.dart","Calculator","-getCurTime"),有几点需要注意。目前 call/execute/inject 接受三个位置参数:包名、类名(如果过程是库方法,则此部分为空字符串)和函数名。函数名可能带有前缀('-' 或 '+'),'-' 表示实例方法,'+' 表示库静态方法(如 main)和类方法。对于 inject,还有一个名为 lineNum 的命名参数,以便 AspectD 知道在哪个行注入代码片段。lineNum 参数从 1 开始,代码片段将被注入到该行之前。

此外,当您想操作静态方法(包括库方法和类方法)时,您的 AOP 方法(此处为 runAppKWLM)也应声明为静态。使用 execute 命令时也需要满足此要求。

execute

特定函数的每个实现都将被操作。

import 'package:aspectd/aspectd.dart';

@Aspect()
@pragma("vm:entry-point")
class ExecuteDemo{
  @Execute("package:app/calculator.dart","Calculator","-getCurTime")
  @pragma("vm:entry-point")
  Future<String> getCurTime(PointCut pointcut) async{
    print('Aspectd:KWLM12');
    print('${pointcut.sourceInfos.toString()}');
    Future<String> result = pointcut.proceed();
    String test = await result;
    print('Aspectd:KWLM13');
    print('${test}');
    return result;
  }

  @Execute("package:app/calculator.dart","Calculator","+getCurTemporature")
  @pragma("vm:entry-point")
  String getCurTemporature(PointCut pointcut) {
    print('Aspectd:KWLM14');
    print('${pointcut.sourceInfos.toString()}');
    try{
      String res = pointcut.proceed();
    } catch (error, trace){
      print('Aspectd:KWLM15');
    }
    return null;
  }

  @Execute("package:flutter/src/widgets/binding.dart","","+runApp")
  @pragma("vm:entry-point")
  static void runAppKWLM(PointCut pointcut){
    print('Aspectd:KWLM17');
    print('${pointcut.sourceInfos.toString()}');
    pointcut.proceed();
  }
}

inject

对于如下的原始函数:(package:flutter/src/widgets/gesture_detector.dart)

  @override
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};

    if (onTapDown != null || onTapUp != null || onTap != null || onTapCancel != null) {
      gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
        () => TapGestureRecognizer(debugOwner: this),
        (TapGestureRecognizer instance) {
          instance
            ..onTapDown = onTapDown
            ..onTapUp = onTapUp
            ..onTap = onTap
            ..onTapCancel = onTapCancel;
        },
      );
    }
...
}
import 'package:aspectd/aspectd.dart';
import 'package:flutter/services.dart';

@Aspect()
@pragma("vm:entry-point")
class InjectDemo{
  @Inject("package:flutter/src/widgets/gesture_detector.dart","GestureDetector","-build", lineNum:452)
  @pragma("vm:entry-point")
  static void onTapBuild() {
    Object instance; //Aspectd Ignore
    Object context; //Aspectd Ignore
    print(instance);
    print(context);
    print('Aspectd:KWLM25');
  }
}

之后,原始的 build 函数将如下所示:

  @override
  Widget build(BuildContext context) {
    final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{};

    if (onTapDown != null || onTapUp != null || onTap != null || onTapCancel != null) {
      gestures[TapGestureRecognizer] = GestureRecognizerFactoryWithHandlers<TapGestureRecognizer>(
        () => TapGestureRecognizer(debugOwner: this),
        (TapGestureRecognizer instance) {
          instance
            ..onTapDown = onTapDown
            ..onTapUp = onTapUp
            ..onTap = onTap
            ..onTapCancel = onTapCancel;
        },
    	print(instance);
    	print(context);
    	print('Aspectd:KWLM25');
      );
    }
...
}

在使用注入时,请注意 //Aspectd Ignore 部分,我们需要成功编译 AOP 包,因此我们需要声明实例/上下文变量。但是,在注入到原始函数(此处为 build)时,变量声明

Object instance; //Aspectd Ignore 
Object context; //Aspectd Ignore

将被丢弃,以避免覆盖原始变量。

GitHub

https://github.com/XianyuTech/aspectd