AspectD: Uma Solução AOP Eficaz para Flutter

Open Sourced por Alibaba Xianyu Tech Team

Alibaba Tech em HackerNoon.com Segue Jul 11 · 9 min ler

Este artigo faz parte da série Utilizando Flutter do Alibaba .

Github. AspectD para Flutter

fundo

Com o rápido desenvolvimento da estrutura Flutter, mais e mais empresas começam a usar o Flutter para refatorar ou construir novos produtos. No entanto, na prática, descobrimos que, por um lado, o Flutter tem uma alta eficiência de desenvolvimento, excelente desempenho e bom desempenho em várias plataformas. Por outro lado, o Flutter também enfrenta problemas, como plug-ins ausentes ou imperfeitos, recursos básicos e o framework subjacente.

Por exemplo, no processo de implementação de gravação e reprodução automatizadas, descobrimos que o código da estrutura Flutter (nível de dardo) precisa ser modificado para atender aos requisitos durante a reprodução automática da gravação. Isso leva ao risco de o framework se tornar vulnerável a invasões. Para resolver esse problema e reduzir o custo de manutenção no processo de iteração, a primeira solução que consideramos é a Programação Orientada a Aspectos.

AOP (Orientação Orientada a Aspectos) pode inserir dinamicamente o código em um método e posição específicos da classe em tempo de compilação (ou tempo de execução), adicionando recursos ao código existente de forma dinâmica e uniforme, sem modificar o código-fonte.

Mas a questão é, como podemos implementar o AOP para o Flutter? Este artigo enfoca o AspectD, uma estrutura de programação AOP orientada por dardos desenvolvida pela equipe de tecnologia da Xianyu.

AspectD: Framework AOP Orientado a Dardos

Se o recurso AOP é suportado em tempo de execução ou em tempo de compilação, depende das características da própria linguagem. Por exemplo, no iOS, o próprio Objective C fornece poderosos recursos de tempo de execução e dinâmicos, tornando o AOP de tempo de execução fácil de usar. No Android, Java pode não apenas implementar proxies estáticos em tempo de compilação (como AspectJ) com base na modificação de bytecode, mas também implementar proxies dinâmicos de tempo de execução (como Spring AOP) com base em aprimoramentos de tempo de execução.

E quanto ao Dart? Em primeiro lugar, o suporte à reflexão do Dart é fraco. Somente Introspection é suportado, enquanto Modification não é suportado. Em segundo lugar, o Flutter desativa a reflexão quanto ao tamanho e robustez do pacote.

Portanto, projetamos e implementamos uma solução AOP baseada na modificação em tempo de compilação, AspectD.

Detalhes do design

Cenários típicos de AOP

O código AspectD a seguir ilustra um cenário típico de aplicativo AOP:

 aop.dart import 'package:example/main.dart' as app; 
import 'aop_impl.dart';
void main()=> app.main(); aop_impl.dart 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!') ;
}
}

Projeto de API orientado a desenvolvedores

Design de PointCut

 @Call("package:app/calculator.dart","Calculator","-getCurTime") 

PointCut precisa ser totalmente caracterizado como adicionar a lógica AOP, por exemplo, de que forma (Call / Execute) e para qual biblioteca, qual classe (este item está vazio no caso do método de biblioteca) e qual método.
Estrutura de dados do PointCut:

 @pragma('vm:entry-point') 
class PointCut {
final Map<dynamic> sourceInfos;
final Object target;
final String function;
final String stubId;
final List<dynamic> positionalParams;
final Map<dynamic, dynamic> namedParams;
@pragma('vm:entry-point')
PointCut(this.sourceInfos, this.target, this.function, this.stubId,this.positionalParams, this.namedParams);
@pragma('vm:entry-point')
Object proceed(){
return null;
}
}

Ele contém as informações do código-fonte (como o nome da biblioteca, o nome do arquivo e o número da linha), o objeto de chamada do método, o nome da função e as informações de parâmetro.

Por favor, note a anotação @pragma('vm:entry-point') aqui. Sua lógica central é o Tree-Shaking. Na compilação AOT (Ahead of Time), se não puder ser chamado pela entrada Main da aplicação, será descartado como código inútil. A lógica de injeção do código AOP não é invasiva, então, obviamente, ele não será chamado pela entrada principal. Portanto, essa anotação é necessária para dizer ao compilador para não descartar essa lógica.

O método prossar aqui é semelhante ao método ProceedingJoinPoint.proceed () em AspectJ, e a lógica original pode ser chamada chamando o método pointcut.proceed (). O corpo do método prossegue na definição original está vazio e seu conteúdo será gerado dinamicamente no tempo de execução.

Design de Conselhos

 @pragma("vm:entry-point") 
Future<String> getCurTime(PointCut pointcut) async{
...
return result;
}

O efeito de @pragma("vm:entry-point") aqui é o mesmo descrito acima. O objeto pointCut é passado para o método AOP como um parâmetro, para que os desenvolvedores possam obter informações relevantes sobre a chamada do código-fonte para implementar sua própria lógica ou chamar a lógica original através de pointcut.proceed ().

Design do Aspecto

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

A anotação Aspect pode permitir que a classe de implementação AOP, como ExecuteDemo, seja facilmente identificada e extraída, e também pode ser usada como um switch. Ou seja, se quisermos desabilitar essa lógica AOP, basta remover a anotação @Aspect.

Compilação do código AOP

Contém a entrada principal no projeto original

Como podemos ver acima, import 'package:example/main.dart' as app; é introduzido em aop.dart, que permite que todo o código de todo o projeto de exemplo seja incluído ao compilar aop.dart.

Compilação no modo de depuração

A introdução da import 'aop_impl.dart'; em aop.dart permite que o conteúdo em aop_impl.dart seja compilado no modo de depuração, mesmo que não seja explicitamente dependente de aop.dart

Compilação no modo de lançamento

Na compilação AOT (no modo Release), a lógica Tree-Shaking faz com que o conteúdo em aop_impl.dart não seja compilado no Dill quando eles não são chamados pela entrada Main no aop. O impacto pode ser evitado adicionando @pragma("vm:entry-point") .

Quando usamos AspectD para escrever o código AOP e geramos produtos intermediários compilando aop.dart, para que Dill contenha o código original do projeto e o código AOP, precisamos considerar como modificá-lo. No AspectJ, as modificações são implementadas por meio de operações no arquivo de classe. No AspectD, ele é implementado ao operar o arquivo Dill.

Operação de endro

O arquivo Dill, também conhecido como Dart Intermediate Language, é um conceito na compilação da linguagem Dart. A compilação de instantâneo de script ou AOT requer Dill como intermediário.

Estrutura do Dill

Podemos usar o dump_kernel.dart fornecido pelo pacote da VM no DART SDK para imprimir a estrutura interna do Dill.

 dart bin/dump_kernel.dart /Users/kylewong/Codes/AOP/aspectd/example/aop/build/app.dill /Users/kylewong/Codes/AOP/aspectd/example/aop/build/app.dill.txt 

Transformação do Dill

O Dart fornece um método Transformado do Kernel para o Kernel, que pode transformar o Dill através da travessia AST recursiva do arquivo Dill

Com base na anotação AspectD compilada por desenvolvedores, as bibliotecas, classes e métodos que precisam ser adicionados com o código AOP específico podem ser extraídos da parte de transformação de AspectD e, em seguida, recursos, como Chamada / Execução, podem ser implementados por meio de operações em classes de destino durante a recursão de AST.

A seguir, parte de uma lógica típica de transformação:

 @override 
MethodInvocation visitMethodInvocation(MethodInvocation methodInvocation) {
methodInvocation.transformChildren(this);
Node node = methodInvocation.interfaceTargetReference?.node;
String uniqueKeyForMethod = null;
if (node is Procedure) {
Procedure procedure = node;
Class cls = procedure.parent as Class;
String procedureImportUri = cls.reference.canonicalName.parent.name;
uniqueKeyForMethod = AspectdItemInfo.uniqueKeyForMethod(
procedureImportUri, cls.name, methodInvocation.name.name, false, null);
}
else if(node == null) {
String importUri = methodInvocation?.interfaceTargetReference?.canonicalName?.reference?.canonicalName?.nonRootTop?.name;
String clsName = methodInvocation?.interfaceTargetReference?.canonicalName?.parent?.parent?.name;
String methodName = methodInvocation?.interfaceTargetReference?.canonicalName?.name;
uniqueKeyForMethod = AspectdItemInfo.uniqueKeyForMethod(
importUri, clsName, methodName, false, null);
}
if(uniqueKeyForMethod ! = null) {
AspectdItemInfo aspectdItemInfo = _aspectdInfoMap[uniqueKeyForMethod];
if (aspectdItemInfo?.mode == AspectdMode.Call &&
! _transformedInvocationSet.contains(methodInvocation) && AspectdUtils.checkIfSkipAOP(aspectdItemInfo, _curLibrary) == false) {
return transformInstanceMethodInvocation(
methodInvocation, aspectdItemInfo);
}
}
return methodInvocation;
}

Atravessando o objeto AST em Dill (a função visitMethodInvocation aqui), combinada com a anotação AspectD escrita por desenvolvedores ( aspectdInfoMap e aspectdItemInfo aqui), podemos transformar o objeto AST original (o methodInvocation aqui) para alterar a lógica original do código, o processo de transformação.

Sintaxe suportada pelo AspectD

Ao contrário dos avanços BeforeAroundAfter fornecidos no AspectJ, apenas uma abstração unificada está disponível no AspectD, que é, Around.

Em termos de modificar o método original internamente, dois tipos, Chamar e Executar, estão disponíveis. O PointCut do primeiro é o ponto de chamada, e o PointCut do segundo é o ponto de execução.

Ligar

 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;
}
}

Executar

 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;
}

Injetar

Somente Call e Execute são suportados, o que obviamente não é suficiente para o Flutter (Dart). Por um lado, Flutter não permite reflexão. Para dizer o mínimo, mesmo que o Flutter permita a reflexão, ainda não é suficiente e não pode atender às necessidades.

Para um cenário típico, se a classe “y” no arquivo x.dart define um método privado “m” ou uma variável de membro “p” no código de dart a ser injetado, então ele não pode ser acessado em aop_impl.dart, não para mencionar a obtenção de múltiplas propriedades contínuas de variáveis privadas. Por outro lado, pode não ser suficiente apenas operar o método inteiro. Podemos precisar inserir a lógica de processamento no meio do método.

Para resolver esse problema, uma sintaxe, Inject, é projetada em AspectD. Veja o seguinte exemplo:

A biblioteca Flutter contém o seguinte código relacionado a gestos:

 @override 
Widget build(BuildContext context) {
final Map<TapGestureRecognizer> 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;
},
);
}

Se quisermos adicionar uma lógica de processamento para a instância e o contexto após onTapCancel, Call e Execute não são viáveis. No entanto, com o Inject, apenas algumas instruções simples são necessárias para resolver o problema:

 import 'package:aspectd/aspectd.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');
}
}

Através da lógica de processamento acima, o método GestureDetector.build em Dill após a compilação é o seguinte:

Além disso, comparado com Call / Execute, o parâmetro de entrada de Inject possui um parâmetro chamado lineNum adicional, que pode ser usado para especificar o número de linha específico da lógica de inserção.

Suporte ao processo de compilação

Embora possamos compilar aop.dart para compilar o código de engenharia original e o código AspectD no arquivo dill e implementar a transformação da hierarquia Dill por meio de Transform para implementar o AOP, o padrão Flutter (flutter tools) não suporta esse processo, então pequenas alterações no processo de construção ainda são necessárias.

No AspectJ, esse processo é implementado pelo Ajc do compilador Java não padrão. Na AspectD, o suporte para AspectD pode ser implementado anexando o Patch a flutter_tools.

 kylewong@KyleWongdeMacBook-Pro fluttermaster % git apply --3way /Users/kylewong/Codes/AOP/aspectd/0001-aspectd.patch 
kylewong@KyleWongdeMacBook-Pro fluttermaster % rm bin/cache/flutter_tools.stamp
kylewong@KyleWongdeMacBook-Pro fluttermaster % flutter doctor -v
Building flutter tool...

Prática e consideração

Com base no AspectD, removemos com êxito todo o código invasivo para a estrutura Flutter e implementamos os mesmos recursos de quando o código intrusivo não foi removido, suportando a gravação e a reprodução de centenas de scripts e a operação estável e confiável da regressão automática .

Do ponto de vista da AspectD, Chamada / Execução pode nos ajudar a implementar facilmente recursos, como rastreamento de desempenho (duração da chamada de métodos chave), aprimoramento de log (obtenção de informações detalhadas sobre o local onde um método é especificamente chamado) e gravação e reprodução Doom. (como o registro de compilação e a reprodução de seqüências numéricas aleatórias). A sintaxe do Inject é mais poderosa. Ele pode implementar a injeção gratuita de lógica por meios semelhantes ao código-fonte e suportar cenários complexos, como gravação de aplicativos e regressão automática (por exemplo, gravação e reprodução de eventos de toque do usuário).

Além disso, o princípio AspectD é baseado na transformação Dill. Com o poder de Dill, os desenvolvedores podem operar livremente em produtos compilados Dart. Além disso, essa transformação tem como alvo objetos AST, que estão próximos ao nível do código-fonte e não são apenas poderosos, mas também confiáveis. Seja a substituição lógica ou a transformação do modelo Json ?>, ela fornece uma nova perspectiva e possibilidade.

Conclusão

Como uma nova estrutura AOP orientada para Flutter desenvolvida pela equipe de tecnologia da Xianyu, a AspectD suporta cenários mainstream de AOP e é de código aberto no Github. AspectD para Flutter

Se você tiver dúvidas ou sugestões durante o uso, sinta-se à vontade para enviar um problema ou uma solicitação de recebimento .

(Artigo original de Wang Kang ? ?)

Texto original em inglês.