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:-
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.
-
Todo o código-fonte para registro é mantido em um único local, portanto, você não precisa procurar manualmente todos os locais onde é usado.
-
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.
- Aspectos são códigos separados que podem ser melhorados e usados repetidamente.
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 :-
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() .
- 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() .
-
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) .
-
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) .
-
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() .
-
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".
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:
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:
@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() .
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:
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:
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:
- Quando o método tem um valor de retorno — if (returningValue! = Null) {
- Quando não há valor de retorno — else {
GO TO FULL VERSION