Programación orientada a aspectos con AspectJ, Spring AOP y la clase Proxy

Escrito por picodotdev el .
java planeta-codigo
Enlace permanente Comentarios

Los aspectos permiten separar código con distintas funcionalidades y centralizar un código común que sin utilizarlos está repartido por toda la aplicación. Son un concepto potente y una vez entendidos sus conceptos ofrecen muchas posibilidades para simplificar el código y mejorar su mantenimiento. Hay varias posibilidades, dos de las más utilizadas son AspectJ y Spring AOP, en el caso de que estas no se puedan utilizar el JDK incluye la clase Proxy para usos básicos aunque más limitados.

Java

Ciertas funcionalidades son transversales y están repartidas por toda la aplicación. Añadir y mezclar el código de esta funcionalidades con el código en los métodos hace que el código del método sea más complicado incluso puede que ese código de utilidad sea de mayor tamaño que el fundamental del método.

Algunos ejemplos de funcionalidades transversales son trazas, métricas de rendimiento, seguridad, caches o transacciones. La programación orientada a aspectos es una técnica de programación que permite extraer este código transversal y aplicarlo en aquellos puntos de la aplicación donde sea necesario sin estar mezclado con el código al que se aplica. Un aspecto es el código transversal de utilidad aplicable a varios puntos de la aplicación. Esto facilita la legibilidad del código, su mantenimiento y la separación de conceptos.

La programación orientada a aspectos se usa mucho en las aplicaciones que usan Spring pero hay otras librerías que lo permiten, incluso el propio JDK tiene alguna clase sin necesitar de dependencias adicionales.

La programación define varios términos:

  • Aspect: es una funcionalidad genérica aplicable a múltiples objetos. Cada aspecto trata una sola funcionalidad.
  • Join point: es el punto de ejecución donde se puede aplicar un aspecto como la llamada a un método, su retorno o el acceso a una propiedad.
  • Advice: es la acción que se realiza en un pointcut.
  • Pointcut: es una expresión que busca joint points, tiene un advice asociado que se ejecuta en todos los joint points que concuerdan con la expresión.
  • weaving: proceso que aplica los aspectos a las clases, puede ser en tiempo de compilación o en tiempo de ejecución.

Esta es una clase normal con un método en la que a modo de ejemplo en la llamada al método se le apliquen dos aspectos, uno para añadir una traza cuando se llame al método y su valor de retorno y otro aspecto para medir cuando tiempo tarda en ejecutarse. La clase Foo descnoce los aspectos que se van a aplicar, no hay que hacer ninguna modificación en ella ni para añadirle los aspectos ni para quitarselos.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package io.github.picodotdev.blogbitix.aspects;

import java.util.Random;

public class Foo implements IFoo {

    public void echo() {
        System.out.println("echo");
    }

    public int sum(int a, int b) {
        return a + b;
    }

    public void sleep() {
        try {
            long time = new Random().nextInt(1500);
            Thread.sleep(time);
        } catch(Exception e) {}
    }
}
Foo.java

La interfaz solo es necesaria para un aspecto implementado con la clase Proxy de Java.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package io.github.picodotdev.blogbitix.aspects;

import java.util.Random;

public interface IFoo {

    void echo();
    int sum(int a, int b);
    void sleep();
}
IFoo.java

Se recomienda usra la forma más simple que sea suficiente para las necesidad de la aplicación. Spring AOP es más simple que usar AspectJ y no hay necesidad de aplicar el compilador de AspectJ en el proceso de compilación. Si solo se necesita aplicar advices en la ejecución de métodos de beans de Spring, Spring AOP es suficiente.

Si no se usa spring o se necesitan aplicar aspectos en objetos no gestionados por el contenedor de Spring (como objetos de dominio) o aplicar advices en joint points distintos a las ejecuciones de métodos, por ejemplo para la obtención o asignación de una propiedad entonces la opción a usar es AspectJ.

Programación orientada a aspectos con AspectJ

AspectJ es una librería específica y la que más posibilidades ofrece de las que muestro en el artículo. Hay varias formas de utilizar AspectJ, la de usarla mediante anotaciones es bastante simple.

Una de las ventajas de AspectJ es que no requiere usar Spring para utilizarla pero para ello en el momento de compilación hay que realizar un proceso denominado weaving para añadir la funcionalidad de los aspectos que transformar el bytecode de las clases. Aplicar los aspectos transformando el código permite que los aspectos no penalicen en tiempo de ejecución y ofrezca mejor rendimiento que Spring AOP, aunque el rendimiento no es algo determinante en la mayoría de los proyectos. Por contra es más compleja y requiere aplicar a las clases un proceso de postcompilación.

Las expresiones de los ponintcuts son similares a una definición de la firma del los métodos, ámbitos de visibilidad, tipos de parámetros y tipo de retorno además del paquete. Es posible hacer expresiones boleanas compuestas para hacer más especifica una expresión. Este pointcut se aplica en la ejecución del método sum de la clase Foo que recibe dos parámetros de tipo int y retorna un valor de tipo int.

1
execution(int Foo.sum(int,int))
pointcut.txt

En la clase Aspects se definen los aspectos con una colección de pointcuts con sus código de advice asociado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package io.github.picodotdev.blogbitix.aspects;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Around;

@Aspect
public class Aspects {

    @Before("execution(void Foo.echo())")
    public void echoStart() {
        System.out.println("aspect echo begin");
    }

    @After("execution(void Foo.echo())")
    public void echoEnd() {
        System.out.println("aspect echo end");
    }

    @Around("execution(int Foo.sum(int,int))")
    public Object log(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("aspect sum begin");
        Object o = pjp.proceed();
        System.out.println("aspect sum end: " + o);
        return o;
    }

    @Around("execution(void Foo.sleep())")
    public void time(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object o = pjp.proceed();
        long end = System.currentTimeMillis();
        System.out.println("aspect time: " + (end - start));
    }
}
Aspects.java

Con la herramienta de construcción Gradle hay que incluir un plugin para aplicar el proceso de weaving. El proceso de weaving consiste en aplicar los aspectos a las clases, AspectJ lo realiza en tiempo de compilación modificando el bytecode de las clases en un segundo paso de compilación, con anterioridad el compilador de Java ha transformado el código fuente de las clases en bytecode.

1
2
3
4
5
6
7
plugins {
    id 'java'
    id 'application'
    id 'org.springframework.boot' version '2.2.4.RELEASE'
    id 'io.freefair.aspectj.post-compile-weaving' version '4.1.6'
}
...
build-1.gradle
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package io.github.picodotdev.blogbitix.aspects;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.CommandLineRunner;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@SpringBootApplication
@EnableAspectJAutoProxy
public class Main implements CommandLineRunner {

    ...

    @Override
    public void run(String... args) throws Exception {
        // AspectJ
        System.out.println("");
        System.out.println("AspectJ");
        Foo foo = new Foo();
        foo.echo();
        foo.sum(3, 7);
        foo.sleep();

        ...
    }

    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}
Main-1.java

En la salida del programa para el apartado de AspectJ se observa que el código de los aspectos se ejecuta al llamar a los métodos de la instancia de la clase Foo.

1
2
3
4
5
6
7
AspectJ
aspect echo begin
echo
aspect echo end
aspect sum begin
aspect sum end: 10
aspect time: 546
System.out-1

Programación orientada a aspectos con Spring AOP

Spring incluye su solución para la programación orientada a aspectos, más limitada que AspectJ pero suficiente para la mayoría de los casos tampoco requiere aplicar el proceso weaving de AspectJ en tiempo de compilación. La limitación de Spring AOP es que los joint points solo pueden ser métodos. Utiliza las mismas anotaciones de AspectJ para aplicar los aspects en tiempo de ejecución.

Otra diferenia con AspectJ es que los aspectos se aplican usando proxys que son una clase que envuelve a la instancia a la que se le aplica el aspecto, una vez dentro de la clase objetivo si se llama a otro método de forma interna a ese otro método no se le aplica su aspecto.

Suponiendo una clase que tiene un méodo foo y bar y desde fuera se llama a foo y este llama a bar para que en llamada desde foo a bar se apliquen los aspectos de bar hay que usar este código. Usar este código implica poner en el código una dependencia a Spring, lo cual no es deseable para el código de dominio.

1
2
3
4
5
...
public void foo() {
   ((Foo) AopContext.currentProxy()).bar();
}
...
SpringProxy.java

En el proxy es donde se ejecuta el código del advice.

Llamada a un método normal Llamada a un método con un proxy

Llamada a un método normal y con un proxy

Para que Spring procese las anotaciones require usar la anotación @EnableAspectJAutoProxy y que Spring encuentre la clase de los aspectos, anotándola con @Component o devolviendo una instancia en el contenedor de dependencias como en este caso.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package io.github.picodotdev.blogbitix.aspects;

...

@SpringBootApplication
@EnableAspectJAutoProxy
public class Main implements CommandLineRunner {

    ...

    @Bean
    public Foo foo() {
        return new Foo();
    }

    @Bean
    public Aspects aspects() {
        return new Aspects();
    }

    @Override
    public void run(String... args) throws Exception {
        ...

        // Spring AOP
        System.out.println("");
        System.out.println("Spring AOP (AspectJ anotations)");
        fooBean.echo();
        fooBean.sum(3, 7);
        fooBean.sleep();

        ...
    }
}
Main-2.java

El plugin para realizar el proceso de weaving con AspectJ no es necesario. Spring realiza e proceso de weaving en tiempo de ejecución.

1
2
3
4
5
6
7
plugins {
    id 'java'
    id 'application'
    id 'org.springframework.boot' version '2.2.4.RELEASE'
    //id 'io.freefair.aspectj.post-compile-weaving' version '4.1.6'
}
...
build-2.gradle

El resultado es el mismo que con AspectJ.

1
2
3
4
5
6
7
Spring AOP (AspectJ anotations)
aspect echo begin
echo
aspect echo end
aspect sum begin
aspect sum end: 10
aspect time: 1049
System.out-2

Programación orientada a aspectos con la clase Proxy

Para casos muy sencillos donde no sea posible aplicar una de las opciones anteriores al no poder usar sus librerías por restricciones del proyecto en cuanto a dependencias usables está la alternativa incluida en el JDK. La clase Proxy está incorporada en el propio JDK, permite hacer cosas sencillas sin dependencias adicionales.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package io.github.picodotdev.blogbitix.aspects;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class LogProxy implements InvocationHandler {

    protected Object object;
    protected Proxy proxy;

    public LogProxy(Object object) {
        this.object = object;
        proxy = (Proxy) proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), this);
    }

    public Proxy getProxy() {
        return proxy;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("proxy " + method.getName() + " begin");
        Object o = method.invoke(object, args);
        System.out.print("proxy " + method.getName() + " end");
        if (!method.getReturnType().equals(Void.TYPE)) {
            System.out.print(": " + o);
        }
        System.out.println();
        return o;
    }
}
LogProxy.java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package io.github.picodotdev.blogbitix.aspects;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProfileProxy implements InvocationHandler {

    protected Object object;
    protected Proxy proxy;

    public ProfileProxy(Object object) {
        this.object = object;
        proxy = (Proxy) proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), this);
    }

    public Proxy getProxy() {
        return proxy;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.currentTimeMillis();
        Object o = method.invoke(object, args);
        long end = System.currentTimeMillis();
        if (method.getName().equals("sleep")) {
            System.out.println("proxy time: " + (end - start));
        }
        return o;
    }
}
ProfileProxy.java

En este caso se observa que se ha aplicado el aspecto de AspectJ y además los aspectos de los proxys de este apartado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Java Proxy
proxy echo begin
aspect echo begin
echo
aspect echo end
proxy echo end
proxy sum begin
aspect sum begin
aspect sum end: 10
proxy sum end: 10
proxy sleep begin
aspect time: 323
proxy time: 323
proxy sleep end
System.out-3
Terminal

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos de Blog Bitix alojado en GitHub y probarlo en tu equipo ejecutando siguiente comando:
./gradlew run

Comparte el artículo: