CodeGym /Blogue Java /Random-PT /O que é AOP? Princípios da programação orientada a aspect...
John Squirrels
Nível 41
San Francisco

O que é AOP? Princípios da programação orientada a aspectos

Publicado no grupo Random-PT
Olá, rapazes e moças! Sem entender os conceitos básicos, é bastante difícil mergulhar em estruturas e abordagens para construir funcionalidades. Portanto, hoje falaremos sobre um desses conceitos - AOP, também conhecido como programação orientada a aspectos . O que é AOP?  Princípios da programação orientada a aspectos - 1Este tópico não é fácil e raramente é usado diretamente, mas muitos frameworks e tecnologias o usam sob o capô. E, claro, às vezes durante as entrevistas, você pode ser solicitado a descrever em termos gerais que tipo de animal é esse e onde pode ser aplicado. Então vamos dar uma olhada nos conceitos básicos e alguns exemplos simples de AOP em Java . Agora então, AOP significa programação orientada a aspectos, que é um paradigma destinado a aumentar a modularidade das diferentes partes de um aplicativo, separando preocupações transversais. Para conseguir isso, um comportamento adicional é adicionado ao código existente sem fazer alterações no código original. Em outras palavras, podemos pensar nisso como adicionar funcionalidades adicionais aos métodos e classes sem alterar o código modificado. Por que isso é necessário? Mais cedo ou mais tarde, concluímos que a típica abordagem orientada a objetos nem sempre resolve efetivamente certos problemas. E quando esse momento chega, o AOP vem em socorro e nos dá ferramentas adicionais para a construção de aplicativos. E ferramentas adicionais significam maior flexibilidade no desenvolvimento de software, o que significa mais opções para resolver um determinado problema.

Aplicando AOP

A programação orientada a aspectos é projetada para executar tarefas transversais, que podem ser qualquer código que pode ser repetido muitas vezes por diferentes métodos, que não podem ser completamente estruturados em um módulo separado. Assim, o AOP nos permite manter isso fora do código principal e declará-lo verticalmente. Um exemplo é usar uma política de segurança em um aplicativo. Normalmente, a segurança passa por muitos elementos de um aplicativo. Além do mais, a política de segurança do aplicativo deve ser aplicada igualmente a todas as partes existentes e novas do aplicativo. Ao mesmo tempo, uma política de segurança em uso pode evoluir. Este é o lugar perfeito para usar o AOP . Além disso, outro exemplo é o registro. Existem várias vantagens em usar a abordagem AOP para registrar em vez de adicionar manualmente a funcionalidade de registro:
  1. O código para registro é fácil de adicionar e remover: tudo o que você precisa fazer é adicionar ou remover algumas configurações de algum aspecto.

  2. Todo o código-fonte para registro é mantido em um único local, portanto, você não precisa procurar manualmente todos os locais onde é usado.

  3. O código de log pode ser adicionado em qualquer lugar, seja em métodos e classes que já foram escritos ou em novas funcionalidades. Isso reduz o número de erros de codificação.

    Além disso, ao remover um aspecto de uma configuração de design, você pode ter certeza de que todo o código de rastreamento foi removido e que nada foi perdido.

  4. Aspectos são códigos separados que podem ser melhorados e usados ​​repetidamente.
O que é AOP?  Princípios da programação orientada a aspectos - 2O AOP também é usado para tratamento de exceções, armazenamento em cache e extração de certas funcionalidades para torná-lo reutilizável.

Princípios básicos da AOP

Para avançar neste tópico, vamos primeiro conhecer os principais conceitos de AOP. Conselho — Lógica ou código adicional chamado de um ponto de junção. Os conselhos podem ser realizados antes, depois ou em vez de um ponto de junção (mais sobre eles abaixo). Possíveis tipos de conselhos :
  1. Antes — esse tipo de aviso é iniciado antes que os métodos de destino, ou seja, os pontos de junção, sejam executados. Ao usar aspectos como classes, usamos a anotação @Before para marcar o conselho como vindo antes. Ao usar aspectos como arquivos .aj , este será o método before() .

  2. Depois — o conselho que é executado após a conclusão da execução dos métodos (pontos de junção), tanto na execução normal quanto no lançamento de uma exceção.

    Ao usar aspectos como classes, podemos usar a anotação @After para indicar que esse é um conselho que vem depois.

    Ao usar aspectos como arquivos .aj , este é o método after() .

  3. After Returning — este conselho é executado somente quando o método de destino termina normalmente, sem erros.

    Quando os aspectos são representados como classes, podemos usar a anotação @AfterReturning para marcar o conselho como sendo executado após a conclusão bem-sucedida.

    Ao usar aspectos como arquivos .aj , este será o método de retorno after() (Object obj) .

  4. After Throwing — este conselho destina-se a casos em que um método, ou seja, ponto de junção, lança uma exceção. Podemos usar esse conselho para lidar com certos tipos de falha na execução (por exemplo, para reverter uma transação inteira ou log com o nível de rastreamento necessário).

    Para aspectos de classe, a anotação @AfterThrowing é usada para indicar que esse conselho é usado após o lançamento de uma exceção.

    Ao usar aspectos como arquivos .aj , este será o método de lançamento after() (Exception e) .

  5. Ao redor — talvez um dos tipos mais importantes de conselho. Ele envolve um método, ou seja, um ponto de junção que podemos usar para, por exemplo, escolher se desejamos ou não executar um determinado método de ponto de junção.

    Você pode escrever um código de aviso que é executado antes e depois que o método de ponto de junção é executado.

    O conselho around é responsável por chamar o método do ponto de junção e os valores de retorno se o método retornar algo. Em outras palavras, neste conselho, você pode simplesmente simular a operação de um método de destino sem chamá-lo e retornar o que quiser como resultado de retorno.

    Dados os aspectos como classes, usamos a anotação @Around para criar conselhos que envolvem um ponto de junção. Ao usar aspectos na forma de arquivos .aj , esse método será o método around() .

Join Point — o ponto em um programa em execução (ou seja, chamada de método, criação de objeto, acesso a variável) onde o conselho deve ser aplicado. Em outras palavras, esse é um tipo de expressão regular usada para encontrar locais para injeção de código (locais onde os conselhos devem ser aplicados). Pointcut — um conjunto de pontos de junção . Um pointcut determina se o conselho fornecido é aplicável a um determinado ponto de junção. Aspect — um módulo ou classe que implementa funcionalidade transversal. Aspect altera o comportamento do código restante aplicando conselhos em pontos de junção definidos por algum pointcut . Em outras palavras, é uma combinação de conselhos e pontos de junção. Introdução— alterar a estrutura de uma classe e/ou alterar a hierarquia de herança para adicionar a funcionalidade do aspecto ao código estrangeiro. Alvo — o objeto ao qual o conselho será aplicado. Tecelagem — o processo de vincular aspectos a outros objetos para criar objetos proxy aconselhados. Isso pode ser feito em tempo de compilação, tempo de carregamento ou tempo de execução. Existem três tipos de tecelagem:
  • Tecelagem em tempo de compilação — se você tiver o código-fonte do aspecto e o código em que usa o aspecto, poderá compilar o código-fonte e o aspecto diretamente usando o compilador AspectJ;

  • Tecelagem pós-compilação (tecelagem binária) — se você não pode ou não quer usar transformações de código fonte para tecer aspectos no código, você pode pegar classes previamente compiladas ou arquivos jar e injetar aspectos neles;

  • Load-time weaving — é apenas um entrelaçamento binário que é atrasado até que o classloader carregue o arquivo de classe e defina a classe para a JVM.

    Um ou mais carregadores de classes de entrelaçamento são necessários para suportar isso. Eles são fornecidos explicitamente pelo tempo de execução ou ativados por um "agente de tecelagem".

AspectJ — Uma implementação específica do paradigma AOP que implementa a capacidade de executar tarefas transversais. A documentação pode ser encontrada aqui .

Exemplos em Java

A seguir, para uma melhor compreensão do AOP , veremos pequenos exemplos no estilo "Hello World". Logo de cara, observarei que nossos exemplos usarão tecelagem em tempo de compilação . Primeiro, precisamos adicionar a seguinte dependência em nosso arquivo pom.xml :

<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>1.9.5</version>
</dependency>
Como regra, o compilador ajc especial é como usamos aspectos. O IntelliJ IDEA não o inclui por padrão, portanto, ao escolhê-lo como o compilador do aplicativo, você deve especificar o caminho para a distribuição 5168 75 AspectJ . Esta foi a primeira forma. A segunda, que é a que usei, é registrar o seguinte plugin no arquivo pom.xml :

<build>
  <plugins>
     <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>aspectj-maven-plugin</artifactId>
        <version>1.7</version>
        <configuration>
           <complianceLevel>1.8</complianceLevel>
           <source>1.8</source>
           <target>1.8</target>
           <showWeaveInfo>true</showWeaveInfo>
           <<verbose>true<verbose>
           <Xlint>ignore</Xlint>
           <encoding>UTF-8</encoding>
        </configuration>
        <executions>
           <execution>
              <goals>
                 <goal>compile</goal>
                 <goal>test-compile</goal>
              </goals>
           </execution>
        </executions>
     </plugin>
  </plugins>
</build>
Depois disso, é uma boa ideia reimportar do Maven e executar mvn clean compile . Agora vamos prosseguir diretamente para os exemplos.

Exemplo nº 1

Vamos criar uma classe Main . Nela, teremos um ponto de entrada e um método que imprime um nome passado no console:

public class Main {
 
  public static void main(String[] args) {
  printName("Tanner");
  printName("Victor");
  printName("Sasha");
  }
 
  public static void printName(String name) {
     System.out.println(name);
  }
}
Não há nada complicado aqui. Passamos um nome e exibimos no console. Se executarmos o programa agora, veremos o seguinte no console:
Tanner Victor Sasha
Agora, é hora de aproveitar o poder do AOP. Agora precisamos criar um arquivo de aspecto . Eles são de dois tipos: o primeiro tem a extensão de arquivo .aj . A segunda é uma classe comum que usa anotações para implementar recursos AOP . Vejamos primeiro o arquivo com a extensão .aj :

public aspect GreetingAspect {
 
  pointcut greeting() : execution(* Main.printName(..));
 
  before() : greeting() {
     System.out.print("Hi, ");
  }
}
Este arquivo é como uma classe. Vejamos o que está acontecendo aqui: pointcut é um conjunto de join points; greeting() é o nome deste pointcut; : execução indica aplicá-lo durante a execução de todas as ( * ) chamadas do método Main.printName(...) . Em seguida, vem um conselho específico — before() — que é executado antes que o método de destino seja chamado. : greeting() é o ponto de corte ao qual este conselho responde. Bem, e abaixo vemos o corpo do próprio método, que é escrito na linguagem Java, que entendemos. Quando executamos main com este aspecto presente, obteremos esta saída do console:
Oi, Tanner Oi, Victor Oi, Sasha
Podemos ver que toda chamada ao método printName foi modificada graças a um aspecto. Agora vamos dar uma olhada em como ficaria o aspecto como uma classe Java com anotações:

@Aspect
public class GreetingAspect{
 
  @Pointcut("execution(* Main.printName(String))")
  public void greeting() {
  }
 
  @Before("greeting()")
  public void beforeAdvice() {
     System.out.print("Hi, ");
  }
}
Após o arquivo de aspecto .aj , tudo fica mais óbvio aqui:
  • @Aspect indica que esta classe é um aspecto;
  • @Pointcut("execution(* Main.printName(String))") é o ponto de corte que é acionado para todas as chamadas para Main.printName com um argumento de entrada cujo tipo é String ;
  • @Before("greeting()") é um conselho que é aplicado antes de chamar o código especificado no ponto de corte greeting() .
A execução de main com este aspecto não altera a saída do console:
Oi, Tanner Oi, Victor Oi, Sasha

Exemplo nº 2

Suponha que temos algum método que executa algumas operações para clientes e chamamos esse método de main :

public class Main {
 
  public static void main(String[] args) {
  performSomeOperation("Tanner");
  }
 
  public static void performSomeOperation(String clientName) {
     System.out.println("Performing some operations for Client " + clientName);
  }
}
Vamos usar a anotação @Around para criar uma "pseudo-transação":

@Aspect
public class TransactionAspect{
 
  @Pointcut("execution(* Main.performSomeOperation(String))")
  public void executeOperation() {
  }

  @Around(value = "executeOperation()")
  public void beforeAdvice(ProceedingJoinPoint joinPoint) {
     System.out.println("Opening a transaction...");
     try {
        joinPoint.proceed();
        System.out.println("Closing a transaction...");
     }
     catch (Throwable throwable) {
        System.out.println("The operation failed. Rolling back the transaction...");
     }
  }
  }
Com o método continue do objeto ProceedingJoinPoint , chamamos o método wrap para determinar sua localização no conselho. Portanto, o código no método acima joinPoint.proceed(); é Before , enquanto o código abaixo dele é After . Se executarmos main , obtemos isso no console:
Abrindo uma transação... Realizando algumas operações para o Cliente Tanner Fechando uma transação...
Mas se lançarmos uma exceção em nosso método (para simular uma operação com falha):

public static void performSomeOperation(String clientName) throws Exception {
  System.out.println("Performing some operations for Client " + clientName);
  throw new Exception();
}
Em seguida, obtemos esta saída do console:
Abrindo uma transação... Executando algumas operações para o cliente Tanner A operação falhou. Revertendo a transação...
Então, o que conseguimos aqui é um tipo de capacidade de tratamento de erros.

Exemplo nº 3

Em nosso próximo exemplo, vamos fazer algo como logar no console. Primeiro, dê uma olhada em Main , onde adicionamos uma pseudo lógica de negócios:

public class Main {
  private String value;
 
  public static void main(String[] args) throws Exception {
     Main main = new Main();
     main.setValue("<some value>");
     String valueForCheck = main.getValue();
     main.checkValue(valueForCheck);
  }
 
  public void setValue(String value) {
     this.value = value;
  }
 
  public String getValue() {
     return this.value;
  }
 
  public void checkValue(String value) throws Exception {
     if (value.length() > 10) {
        throw new Exception();
     }
  }
}
Em main , usamos setValue para atribuir um valor à variável de instância value . Em seguida, usamos getValue para obter o valor e, em seguida, chamamos checkValue para ver se ele tem mais de 10 caracteres. Se sim, uma exceção será lançada. Agora vamos ver o aspecto que usaremos para registrar o trabalho dos métodos:

@Aspect
public class LogAspect {
 
  @Pointcut("execution(* *(..))")
  public void methodExecuting() {
  }
 
  @AfterReturning(value = "methodExecuting()", returning = "returningValue")
  public void recordSuccessfulExecution(JoinPoint joinPoint, Object returningValue) {
     if (returningValue != null) {
        System.out.printf("Successful execution: method — %s method, class — %s class, return value — %s\n",
              joinPoint.getSignature().getName(),
              joinPoint.getSourceLocation().getWithinType().getName(),
              returningValue);
     }
     else {
        System.out.printf("Successful execution: method — %s, class — %s\n",
              joinPoint.getSignature().getName(),
              joinPoint.getSourceLocation().getWithinType().getName());
     }
  }
 
  @AfterThrowing(value = "methodExecuting()", throwing = "exception")
  public void recordFailedExecution(JoinPoint joinPoint, Exception exception) {
     System.out.printf("Exception thrown: method — %s, class — %s, exception — %s\n",
           joinPoint.getSignature().getName(),
           joinPoint.getSourceLocation().getWithinType().getName(),
           exception);
  }
}
O que está acontecendo aqui? @Pointcut("execution(* *(..))") juntará todas as chamadas de todos os métodos. @AfterReturning(value = "methodExecuting()", returning = "returningValue") é um aviso que será executado após a execução bem-sucedida do método de destino. Temos dois casos aqui:
  1. Quando o método tem um valor de retorno — if (returningValue! = Null) {
  2. Quando não há valor de retorno — else {
@AfterThrowing(value = "methodExecuting()", throw = "exception") é um warning que será acionado em caso de erro, ou seja, quando o método lançar uma exceção. E, portanto, executando main , obteremos uma espécie de log baseado em console:
Execução bem-sucedida: method — setValue, class — Main Execução bem-sucedida: method — getValue, class — Main, return value — <some value> Exceção lançada: method — checkValue, class — Exceção principal — java.lang.Exception Exceção lançada: method — main, class — Principal, exceção — java.lang.Exception
E como não lidamos com as exceções, ainda obteremos um rastreamento de pilha: O que é AOP?  Princípios da programação orientada a aspectos - 3Você pode ler sobre exceções e tratamento de exceções nestes artigos: Exceções em Java e Exceções: captura e tratamento . Isso é tudo para mim hoje. Hoje conhecemos o AOP , e vocês puderam ver que essa fera não é tão assustadora quanto algumas pessoas imaginam. Adeus a todos!
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION