CodeGym /Blog Java /Random-ES /¿Qué es AOP? Principios de la programación orientada a as...
John Squirrels
Nivel 41
San Francisco

¿Qué es AOP? Principios de la programación orientada a aspectos

Publicado en el grupo Random-ES
¡Hola, chicos y chicas! Sin comprender los conceptos básicos, es bastante difícil profundizar en los marcos y enfoques para construir la funcionalidad. Entonces, hoy hablaremos sobre uno de esos conceptos: AOP, también conocido como programación orientada a aspectos . ¿Qué es AOP?  Principios de la programación orientada a aspectos - 1Este tema no es fácil y rara vez se usa directamente, pero muchos marcos y tecnologías lo usan bajo el capó. Y, por supuesto, a veces durante las entrevistas, se le puede pedir que describa en términos generales qué tipo de bestia es y dónde se puede aplicar. Entonces, echemos un vistazo a los conceptos básicos y algunos ejemplos simples de AOP en Java . Ahora bien, AOP significa programación orientada a aspectos, que es un paradigma destinado a aumentar la modularidad de las diferentes partes de una aplicación al separar las preocupaciones transversales. Para lograr esto, se agrega un comportamiento adicional al código existente sin realizar cambios en el código original. En otras palabras, podemos pensar en ello como una función adicional que cuelga sobre métodos y clases sin alterar el código modificado. ¿Por qué es esto necesario? Tarde o temprano, llegamos a la conclusión de que el típico enfoque orientado a objetos no siempre puede resolver de manera efectiva ciertos problemas. Y cuando llega ese momento, AOP viene al rescate y nos brinda herramientas adicionales para construir aplicaciones. Y las herramientas adicionales significan una mayor flexibilidad en el desarrollo de software, lo que significa más opciones para resolver un problema en particular.

Aplicando POA

La programación orientada a aspectos está diseñada para realizar tareas transversales, que pueden ser cualquier código que se puede repetir muchas veces por diferentes métodos, que no se pueden estructurar completamente en un módulo separado. En consecuencia, AOP nos permite mantener esto fuera del código principal y declararlo verticalmente. Un ejemplo es el uso de una política de seguridad en una aplicación. Por lo general, la seguridad se ejecuta a través de muchos elementos de una aplicación. Además, la política de seguridad de la aplicación debe aplicarse por igual a todas las partes nuevas y existentes de la aplicación. Al mismo tiempo, una política de seguridad en uso puede evolucionar. Este es el lugar perfecto para usar AOP . Además, otro ejemplo es el registro. Hay varias ventajas al usar el enfoque AOP para el registro en lugar de agregar funciones de registro manualmente:
  1. El código para iniciar sesión es fácil de agregar y quitar: todo lo que necesita hacer es agregar o quitar un par de configuraciones de algún aspecto.

  2. Todo el código fuente para el registro se mantiene en un solo lugar, por lo que no necesita buscar manualmente todos los lugares donde se usa.

  3. El código de registro se puede agregar en cualquier lugar, ya sea en métodos y clases que ya se han escrito o en una nueva funcionalidad. Esto reduce el número de errores de codificación.

    Además, al eliminar un aspecto de una configuración de diseño, puede estar seguro de que todo el código de seguimiento se ha ido y que no se ha perdido nada.

  4. Los aspectos son código separado que se puede mejorar y usar una y otra vez.
¿Qué es AOP?  Principios de la programación orientada a aspectos - 2AOP también se usa para el manejo de excepciones, el almacenamiento en caché y la extracción de ciertas funciones para que sea reutilizable.

Principios básicos de AOP

Para avanzar más en este tema, primero conozcamos los conceptos principales de AOP. Consejo : Lógica adicional o código llamado desde un punto de unión. Los consejos se pueden realizar antes, después o en lugar de un punto de unión (más sobre ellos a continuación). Posibles tipos de consejos :
  1. Antes : este tipo de aviso se inicia antes de que se ejecuten los métodos de destino, es decir, los puntos de unión. Cuando usamos aspectos como clases, usamos la anotación @Before para marcar el consejo como anterior. Al usar aspectos como archivos .aj , este será el método before() .

  2. Después : consejo que se ejecuta después de que se completa la ejecución de los métodos (puntos de unión), tanto en la ejecución normal como cuando se lanza una excepción.

    Cuando usamos aspectos como clases, podemos usar la anotación @After para indicar que este es un consejo que viene después.

    Cuando se usan aspectos como archivos .aj , este es el método after() .

  3. Después de regresar : este consejo se realiza solo cuando el método de destino finaliza normalmente, sin errores.

    Cuando los aspectos se representan como clases, podemos usar la anotación @AfterReturning para marcar el consejo como ejecutándose después de completarse con éxito.

    Al usar aspectos como archivos .aj , este será el método de retorno after() (Object obj) .

  4. Después de lanzar : este consejo está destinado a instancias en las que un método, es decir, un punto de unión, lanza una excepción. Podemos usar este consejo para manejar ciertos tipos de ejecución fallida (por ejemplo, para revertir una transacción completa o iniciar sesión con el nivel de rastreo requerido).

    Para aspectos de clase, la anotación @AfterThrowing se usa para indicar que este consejo se usa después de lanzar una excepción.

    Al usar aspectos como archivos .aj , este será el método de lanzamiento after() (Excepción e) .

  5. Alrededor : quizás uno de los tipos de consejos más importantes. Rodea un método, es decir, un punto de unión que podemos utilizar para, por ejemplo, elegir si realizar o no un método de punto de unión dado.

    Puede escribir código de aviso que se ejecute antes y después de que se ejecute el método de punto de unión.

    El consejo around es responsable de llamar al método de punto de unión y los valores devueltos si el método devuelve algo. En otras palabras, en este consejo, simplemente puede simular la operación de un método de destino sin llamarlo y devolver lo que quiera como resultado de devolución.

    Dados los aspectos como clases, usamos la anotación @Around para crear consejos que envuelven un punto de unión. Al usar aspectos en forma de archivos .aj , este método será el método around() .

Punto de unión : el punto en un programa en ejecución (es decir, llamada de método, creación de objetos, acceso a variables) donde se debe aplicar el consejo. En otras palabras, este es un tipo de expresión regular utilizada para encontrar lugares para la inyección de código (lugares donde se deben aplicar los consejos). Pointcut : un conjunto de puntos de unión . Un punto de corte determina si el consejo dado es aplicable a un punto de unión dado. Aspecto : un módulo o clase que implementa una funcionalidad transversal. El aspecto cambia el comportamiento del código restante aplicando consejos en los puntos de unión definidos por algún punto de corte . En otras palabras, es una combinación de consejos y puntos de unión. Introducción— cambiar la estructura de una clase y/o cambiar la jerarquía de herencia para agregar la funcionalidad del aspecto al código foráneo. Destino : el objeto al que se aplicará el consejo. Tejido : el proceso de vincular aspectos a otros objetos para crear objetos proxy recomendados. Esto se puede hacer en tiempo de compilación, tiempo de carga o tiempo de ejecución. Hay tres tipos de tejido:
  • Tejido en tiempo de compilación : si tiene el código fuente del aspecto y el código donde usa el aspecto, entonces puede compilar el código fuente y el aspecto directamente usando el compilador AspectJ;

  • Tejido posterior a la compilación (tejido binario) : si no puede o no quiere usar transformaciones de código fuente para tejer aspectos en el código, puede tomar clases compiladas previamente o archivos jar e inyectar aspectos en ellos;

  • Tejido en tiempo de carga : este es solo tejido binario que se retrasa hasta que el cargador de clases carga el archivo de clase y define la clase para la JVM.

    Se requieren uno o más cargadores de clase de tejido para admitir esto. Son proporcionados explícitamente por el tiempo de ejecución o activados por un "agente de tejido".

AspectJ : una implementación específica del paradigma AOP que implementa la capacidad de realizar tareas transversales. La documentación se puede encontrar aquí .

Ejemplos en Java

A continuación, para una mejor comprensión de AOP , veremos pequeños ejemplos de estilo "Hello World". De inmediato, notaré que nuestros ejemplos usarán tejido en tiempo de compilación . Primero, debemos agregar la siguiente dependencia en nuestro archivo pom.xml :

<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>1.9.5</version>
</dependency>
Como regla general, el compilador ajc especial es cómo usamos aspectos. IntelliJ IDEA no lo incluye de forma predeterminada, por lo que al elegirlo como compilador de la aplicación, debe especificar la ruta a la distribución 5168 75 AspectJ . Esta fue la primera forma. La segunda, que es la que yo utilicé, es registrar el siguiente complemento en el archivo 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>
Después de esto, es una buena idea volver a importar desde Maven y ejecutar mvn clean compile . Ahora pasemos directamente a los ejemplos.

Ejemplo No. 1

Vamos a crear una clase principal . En él, tendremos un punto de entrada y un método que imprime un nombre pasado en la consola:

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);
  }
}
No hay nada complicado aquí. Le pasamos un nombre y lo mostramos en la consola. Si ejecutamos el programa ahora, veremos lo siguiente en la consola:
Tanner Víctor Sasha
Ahora bien, es hora de aprovechar el poder de AOP. Ahora necesitamos crear un archivo de aspecto . Son de dos tipos: el primero tiene la extensión de archivo .aj . La segunda es una clase ordinaria que usa anotaciones para implementar capacidades AOP . Primero veamos el archivo con la extensión .aj :

public aspect GreetingAspect {
 
  pointcut greeting() : execution(* Main.printName(..));
 
  before() : greeting() {
     System.out.print("Hi, ");
  }
}
Este archivo es como una clase. Veamos qué está pasando aquí: pointcut es un conjunto de puntos de unión; saludo() es el nombre de este punto de corte; : ejecución indica aplicarlo durante la ejecución de todas las ( * ) llamadas del método Main.printName(...) . Luego viene un consejo específico, before() , que se ejecuta antes de llamar al método de destino. : saludo() es el punto de corte al que responde este consejo. Bueno, y debajo vemos el cuerpo del método en sí, que está escrito en el lenguaje Java, que entendemos. Cuando ejecutamos main con este aspecto presente, obtendremos esta salida de consola:
Hola, Tanner Hola, Victor Hola, Sasha
Podemos ver que cada llamada al método printName ha sido modificada gracias a un aspecto. Ahora echemos un vistazo a cómo se vería el aspecto como una clase de Java con anotaciones:

@Aspect
public class GreetingAspect{
 
  @Pointcut("execution(* Main.printName(String))")
  public void greeting() {
  }
 
  @Before("greeting()")
  public void beforeAdvice() {
     System.out.print("Hi, ");
  }
}
Después del archivo de aspecto .aj , todo se vuelve más obvio aquí:
  • @Aspect indica que esta clase es un aspecto;
  • @Pointcut("execution(* Main.printName(String))") es el punto de corte que se activa para todas las llamadas a Main.printName con un argumento de entrada cuyo tipo es String ;
  • @Before("saludo()") es un consejo que se aplica antes de llamar al código especificado en el punto de corte de saludo() .
Ejecutar main con este aspecto no cambia la salida de la consola:
Hola, Tanner Hola, Victor Hola, Sasha

Ejemplo No. 2

Supongamos que tenemos algún método que realiza algunas operaciones para los clientes y llamamos a este método desde 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);
  }
}
Usemos la anotación @Around para crear una "pseudotransacción":

@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...");
     }
  }
  }
Con el método de proceder del objeto ProceedingJoinPoint , llamamos al método de ajuste para determinar su ubicación en el consejo. Por lo tanto, el código del método anterior joinPoint.proceed(); es Before , mientras que el código debajo es After . Si ejecutamos main , obtenemos esto en la consola:
Abriendo una transacción... Realizando algunas operaciones para Client Tanner Cerrando una transacción...
Pero si lanzamos una excepción en nuestro método (para simular una operación fallida):

public static void performSomeOperation(String clientName) throws Exception {
  System.out.println("Performing some operations for Client " + clientName);
  throw new Exception();
}
Entonces obtenemos esta salida de la consola:
Abriendo una transacción... Realizando algunas operaciones para el Cliente Tanner La operación falló. Revirtiendo la transacción...
Entonces, lo que terminamos aquí es una especie de capacidad de manejo de errores.

Ejemplo No. 3

En nuestro siguiente ejemplo, hagamos algo como iniciar sesión en la consola. Primero, eche un vistazo a Main , donde hemos agregado algo de pseudológica empresarial:

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();
     }
  }
}
En main , usamos setValue para asignar un valor a la variable de instancia de valor . Luego usamos getValue para obtener el valor y luego llamamos a checkValue para ver si tiene más de 10 caracteres. Si es así, se lanzará una excepción. Ahora veamos el aspecto que usaremos para registrar el trabajo de los 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);
  }
}
¿Que está pasando aqui? @Pointcut("execution(* *(..))") se unirá a todas las llamadas de todos los métodos. @AfterReturning(value = "methodExecuting()", return = "returningValue") es un consejo que se ejecutará después de la ejecución exitosa del método de destino. Aquí tenemos dos casos:
  1. Cuando el método tiene un valor devuelto: if (returningValue! = Null) {
  2. Cuando no hay valor de retorno — else {
@AfterThrowing(value = "methodExecuting()", throwing = "exception") es un consejo que se activará en caso de error, es decir, cuando el método lanza una excepción. Y en consecuencia, al ejecutar main , obtendremos una especie de registro basado en la consola:
Ejecución exitosa: método — setValue, clase — Principal Ejecución exitosa: método — getValue, clase — Principal, valor devuelto — <algún valor> Excepción lanzada: método — checkValue, clase — Excepción principal — java.lang.Exception Excepción lanzada: método — principal, clase — Principal, excepción — java.lang.Exception
Y dado que no manejamos las excepciones, aún obtendremos un seguimiento de la pila: ¿Qué es AOP?  Principios de la programación orientada a aspectos - 3puede leer sobre las excepciones y el manejo de excepciones en estos artículos: Excepciones en Java y Excepciones: captura y manejo . Eso es todo para mí hoy. Hoy nos familiarizamos con AOP , y pudiste ver que esta bestia no da tanto miedo como algunas personas creen. ¡Adiós a todos!
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION