Inauguramos el nuevo nombre y aspecto del blog, que pasa de “Posteando mientras…” a “El Recetario”, con una nueva receta de Spring. En este caso vamos a cocinar algo con Spring AOP.
Qué cocinamos
Spring AOP es la parte de Spring relacionada con la porgramación orientada a aspectos. La programación orientada a aspectos es un paradigma informático que se fundamenta en programar en función de un aspecto, funcionalidad o característica concreta, en lugar de hacerlo centrado en un servicio o método específico. Es decir, que se puede codificar una funcionalidad o actividad no en función de un método o servicio específico, si no para un conjunto de ellos que representan una característica o aspecto representativo de la aplicación.
Por ejemplo, se puede programar la escritura de logs, la intercepción de excepciones, la generación de comentarios o auditorías en función de la ejecución o resultados de varios métodos que implementan una única funcionalidad o característica de la aplicación final. Así, podemos crear una entrada en un log cada vez que se invoque un método de la capa de servicios, o cada vez que se llame a un método de un paquete concreto.
Las acciones a realizar o implementación del aspecto es el “Advice”. Un aspecto puede tener varios “advices”, y cada “advice” estara asociado a un punto de interés o “pointcut”. El punto de interés que se usa para indicar cuando ha de ejecutarse el “advice”, es decir, los métodos que se quiere controlar.
AspectJ facilita la creación de los aspectos usando anotaciones.
Ingredientes
- Spring Boot
- Spring MVC
- Spring AOP
- AspectJ
- Log4j
- Maven
Paso a paso
1. Pom.xml
Configuramos el pom.xml de Spring Boot para que resuelva las dependencias necesarias para crear una aplicación con Spring AOP. La aplicación va a ser una simple API REST que recibirá una petición para contar cuantas veces aparece una determinada letra en una palabra, de ahi que necesitemos Spring MVC para la parte web. Se va a capturar la invocación y resultados de la ejecución de esta API usando aspectos y se escribirá en un log cuando esto ocurrá usando Log4j.
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>SpringAOP</groupId> <artifactId>SpringAOP</artifactId> <version>0.0.1-SNAPSHOT</version> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.3.0.BUILD-SNAPSHOT</version> </parent> <dependencies> <!-- Import dependency management from Spring Boot --> <!-- <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>1.3.0.BUILD-SNAPSHOT</version> <type>pom</type> <scope>import</scope> </dependency> --> <!-- Add typical dependencies for a web application --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Use Log4j instead of default logging --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-logging</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j</artifactId> </dependency> <!-- Spring AOP --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> </dependencies> <build> <sourceDirectory>src</sourceDirectory> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> <!-- Package as an executable jar --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> <!-- Add Spring repositories --> <!-- (you don't need this if you are using a .RELEASE version) --> <repositories> <repository> <id>spring-snapshots</id> <url>http://repo.spring.io/snapshot</url> <snapshots><enabled>true</enabled></snapshots> </repository> <repository> <id>spring-milestones</id> <url>http://repo.spring.io/milestone</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>spring-snapshots</id> <url>http://repo.spring.io/snapshot</url> </pluginRepository> <pluginRepository> <id>spring-milestones</id> <url>http://repo.spring.io/milestone</url> </pluginRepository> </pluginRepositories> </project> |
2. Inicializar Spring Boot
Se puede obtener más información acerca de Spring Boot en este otro post. Basta con incluir el siguiente código para inicializar la aplicación como Spring Boot. Esto hará que se ejecute en el Tomcat embebido que se carga automaticamente al incluir las dependencias de Spring MVC.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/** * Main class. * @author lagarcia * */ @SpringBootApplication public class SpringAOP { /** * Main method. * @param args */ public static void main(String[] args) { SpringApplication.run(SpringAOP.class, args); } } |
3. Crear el servicio que servirá de pointcut para los advices del aspecto
Como ejemplo práctico disponemos de un servicio que recibe una palabra y una letra. Éste cuenta cuantas veces aparece la letra en dicha palabra y devuelve la cuenta.
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 |
/** * @author lagarcia * */ @Service public class SpringAOPService { /** * Count the number of 'c' appearances in the passed word. * * @param word * - The word * @param c * - The char to count * @return The number of times the c char is in the word */ public Integer countCharsService(String word, char c) { int count = 0; char[] chars = word.toCharArray(); for (char ch : chars) { if (ch == c) { count++; } } return new Integer(count); } } |
4. Controlador REST
Como se ha mencionado antes, se va ha implementar un servicio web tipo REST que se invocará desde un navegador web. Este servicio recibe en su URL una palabra y una letra, que seran los datos que se le pasarán al servicio antes creado.
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 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
/** * Example controller class. * * @author lagarcia * */ @RestController public class ExampleController { /** A logger reference */ private static Logger logger = Logger.getLogger(ExampleController.class); /** A Spring service */ @Autowired private SpringAOPService aService; /** * Go to home. * * @return */ @RequestMapping("/home") String home() { logger.info("Entering in HOME"); return "Hola Spring AOP"; } /** * Perform a calculation. * * @return */ @RequestMapping("/calculate/{word}/{c}") String calculate(@PathVariable String word, @PathVariable String c) { logger.info("Entering in CALCULATE"); int count = 0; try { count = this.getaService().countCharsService(word, c.charAt(0)); } catch (Exception ex) { ex.printStackTrace(); } logger.info("Finishing CALCULATE"); return "¿Cuántas " + c + "es hay en la palabra " + word + " ? Hay " + count; } /** * @return the aService */ public SpringAOPService getaService() { return aService; } /** * @param aServiceArg * the aService to set */ public void setaService(SpringAOPService aServiceArg) { aService = aServiceArg; } } |
5. Aspecto
Por último lo interesante de esta receta. La creación del aspecto usando las anotaciones de AspectJ. El aspecto puede tener varios “advices”, a saber:
- @Before – Para antes de la ejecución del pointcut
- @After – Para después de la ejecución del pointcut
- @AfterReturning – Para después de la ejecución y recibiendo lo que se retorna
- @AfterThrowing – Para después de que se lanza una excepción
- @Around – Para antes y después del pointcut
Declaramos la clase como Aspect y como Component para que Spring la instancie automaticamente y la configure como aspecto:
1 2 3 4 5 6 7 8 9 |
/** * @author lagarcia * */ @Aspect @Component public class LogAspect { ... } |
Implementamos a modo de ejemplo los sigueintes advices:
- logBefore, para que escriba un mensaje en el log antes de ejecutar el servicio anterior:
1 2 3 4 5 6 7 8 9 10 |
/** * Log before method is executed. * * @param joinPoint */ @Before("execution(* net.luisalbertogh.springaop..*Service*(..))") public void logBefore(JoinPoint joinPoint) { logger.info("logBefore"); logger.info("Log before in: " + joinPoint.getSignature().getName()); } |
Usamos la anotación @Before y el pointcut será la ejecución de cualquier método que tenga el texto Service en su nombre dentro del paquete net.luisalbertogh.springaop, independientemente de lo que reciban como argumento.
- logAfterReturning, para que escriba en el log después de la ejecución del servicio, e imprima el resultado obtenido:
1 2 3 4 5 6 7 8 9 10 11 12 |
/** * Log after method is executed and get returned value. * * @param joinPoint * @param result */ @AfterReturning(pointcut = "execution(* net.luisalbertogh.springaop..*Service*(..))", returning = "result") public void logAfterReturning(JoinPoint joinPoint, Object result) { logger.info("logAfterReturning"); logger.info("Log after in: " + joinPoint.getSignature().getName()); logger.info("- And value returned is: " + result); } |
Al igual que en el caso anterior, se usa la anotación y se define el pointcut correspondiente, pero en este caso además se añade el resultado (result).
- logAfterThrowing, para que escriba después de lanzar una excepción e indique el mensaje de error en el log:
1 2 3 4 5 6 7 8 9 10 11 12 |
/** * Log after exception is thrown. * * @param joinPoint * @param exception */ @AfterThrowing(pointcut = "execution(* net.luisalbertogh.springaop..*Service*(..))", throwing = "exception") public void logAfterThrowing(JoinPoint joinPoint, Throwable exception) { logger.info("logAfterThrowing"); logger.info("Log in: " + joinPoint.getSignature().getName()); logger.info("- And thrown exception is: " + exception.getMessage()); } |
- logAround, para que escriba en el log antes y después de la ejecución del servicio. En este caso, lo que ocurre es que se pasa la ejecución del pointcut al advice. Entonces para que continue la ejecución del servicio se usa joinPoint.proceed().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
/** * Log around the method invocation. * * @param joinPoint * @throws Throwable */ @Around("execution(* net.luisalbertogh.springaop..*Service*(..))") public void logAround(ProceedingJoinPoint joinPoint) throws Throwable { logger.info("logAround"); logger.info("Log before in: " + joinPoint.getSignature().getName()); /* Back to invoked method */ joinPoint.proceed(); logger.info("Log after in: " + joinPoint.getSignature().getName()); } |
El resultado
En el navegador se vería esto:
Y en el log o en la consola:
INFO : net.luisalbertogh.springaop.controllers.ExampleController – Entering in CALCULATE
INFO : net.luisalbertogh.springaop.aspect.LogAspect – logBefore
INFO : net.luisalbertogh.springaop.aspect.LogAspect – Log before in: countCharsService
INFO : net.luisalbertogh.springaop.aspect.LogAspect – logAfterReturning
INFO : net.luisalbertogh.springaop.aspect.LogAspect – Log after in: countCharsService
INFO : net.luisalbertogh.springaop.aspect.LogAspect – - And value returned is: 4
INFO : net.luisalbertogh.springaop.controllers.ExampleController – Finishing CALCULATE