Aspects are a way the framework intercepts method calls and possibly alters the execution of methods.
When designing an aspect, you define the following:
- What code you want Spring to execute when you call specific methods. This is named an aspect.
- When the app should execute this logic of the aspect (e.g., before or after the method call, instead of the method call). This is named the advice.
- Which methods the framework needs to intercept and execute the aspect for them. This is named a pointcut
We want some logic (the aspect) to be executed before (the advice) each execution (the joint point) of method publishComment() (the pointcut), which belongs to the CommentService bean (the target object).
When we use aspects, we do not directly call the actual bean's methods. Instead, spring gives us a "proxy" object. When we call the aspected method, it actually goes through this proxy object. The proxy applies the aspect logic and delegates the call to the actual method.
We will make use of a CommentService
, which has a publishComment()
method. The requirement is to add a log before and after each execution of any of the methods in the service(s). We will achieve this using aspects.
First, we need to add the following dependencies to pom.xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.1.1</version>
</dependency>
Now, we will get the classes ready (without aspects). First the model class Comment
:
package models;
public class Comment {
private String author;
private String text;
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
To keep things simple, we will only keep the CommentService
class for the use case. This uses the Java API logging to log a message which includes the class name from which this gets logged.
package services;
import models.Comment;
import org.springframework.stereotype.Service;
import java.util.logging.Logger;
@Service
public class CommentService {
private Logger logger = Logger.getLogger(CommentService.class.getName());
public void publishComment(Comment comment){
logger.info("publishing comment: " + comment.getText() + " by " + comment.getAuthor());
}
}
Let's run the main app, which currently does not use aspects:
package main;
import config.AppConfig;
import models.Comment;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import services.CommentService;
public class App {
public static void main(String[] args) {
var context = new AnnotationConfigApplicationContext(AppConfig.class);
Comment comment = new Comment();
comment.setAuthor("Scooby");
comment.setText("Scooby Dooby Doo!");
CommentService commentService = context.getBean(CommentService.class);
commentService.publishComment(comment);
}
}
OUTPUT:
Dec 22, 2023 11:25:07 PM services.CommentService publishComment
INFO: publishing comment: Scooby Dooby Doo! by Scooby
-
Enable the aspect mechanism in the Spring app by annotating the configuration class with the
@EnableAspectJAutoProxy
annotation.package config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @ComponentScan(basePackages = {"services","aspects"}) @EnableAspectJAutoProxy public class AppConfig { }
-
Create a new class, and annotate it with the
@Aspect
annotation. Using either@Bean
or stereotype annotations, add a bean for this class in the Spring context.package aspects; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect @Component public class LoggingAspect { public void log(){ // To implement later } }
IMPORTANT: Remember, you need to make this object a bean in the Spring context because Spring needs to know about any object it needs to manage.
-
Define a method that will implement the aspect logic and tell Spring when and which methods to intercept using an advice annotation
package aspects; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import java.util.logging.Logger; @Aspect @Component public class LoggingAspect { private Logger logger = Logger.getLogger(LoggingAspect.class.getName()); @Around("execution(* services.*.*(..))") public void log(ProceedingJoinPoint joinPoint) throws Throwable { logger.info("starting execution"); joinPoint.proceed(); logger.info("execution complete"); } }
The pointcut expression
execution(* services.*.*(..))
means:"Apply this advice around the execution of any method with any return type within any class in the services package, regardless of the method name and with any parameters."
Check AspectJ for more info: @AspectJ
-
Let's run the app!
package main; import config.AppConfig; import models.Comment; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import services.CommentService; public class App { public static void main(String[] args) { var context = new AnnotationConfigApplicationContext(AppConfig.class); Comment comment = new Comment(); comment.setAuthor("Scooby"); comment.setText("Scooby Dooby Doo!"); CommentService commentService = context.getBean(CommentService.class); commentService.publishComment(comment); } }
OUTPUT:
Dec 22, 2023 11:43:38 PM aspects.LoggingAspect log INFO: starting execution Dec 22, 2023 11:43:38 PM services.CommentService publishComment INFO: publishing comment: Scooby Dooby Doo! by Scooby Dec 22, 2023 11:43:38 PM aspects.LoggingAspect log INFO: execution complete
Magically, the aspect method that we created intercepts the call and handles the execution. The method
joinPoint.proceed()
actually delegates the call to the real method. If we did not add it, it wouldn't have executed thepublishComment()
method at all!