Pointcuts Explained: Spring AOP vs. AspectJ in Handling Cross-Cutting Concerns - Part 2

Pointcuts Explained: Spring AOP vs. AspectJ in Handling Cross-Cutting Concerns - Part 2

Written by Danubius IT Solutions

Part 2 – Pointcuts in Practice

 

In the first part of this series, we examined the conceptual differences between Spring AOP’s proxy-based model and AspectJ’s compiler-based weaving approach in managing cross-cutting concerns. Pointcuts are essential for defining where exactly these concerns should take effect in the application flow. In this second part of the series, we will explore Spring AOP and AspectJ pointcuts in practice. Whether you are integrating AspectJ or Spring AOP into your Spring application, these code examples will help solidify your understanding.

 

Pointcut definitions explained

 

If we aim at utilizing AOP in our application, much depends on designing good pointcuts. For instance, we would like to add metrics, measuring how much time a database query, the execution of a method with some complicated business logic or a REST endpoint’s response takes. Let us examine some of such common use cases – and potential pitfalls – with the help of a very simple Java 8 Spring Boot demo app. Its REST API consists of a single endpoint, /books, which returns a list of BookDto objects sorted by author and title. There is a usual service and repository layer, and though sorting could be done at the database level, to explore pointcuts, now we will have a Java interface for sorting.

First, let us add the logic for the metrics, which we would like to run in our aspects.

abstract class AbstractMetricsAspect {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractMetricsAspect.class);
    
    public Object performLogging(ProceedingJoinPoint joinPoint, String aspectName) throws Throwable {
        LOGGER.debug("{}'s source location: {}; type: {}", aspectName, joinPoint.getSourceLocation(), joinPoint.getKind());
        
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        
        try {
            return joinPoint.proceed();
        } finally {
            stopWatch.stop();
            LOGGER.info("ASPECT: {}; METRICS LOGGING: {}", aspectName, stopWatch.getLastTaskTimeMillis());
        }
    }
}

We are using ProceedingJoinPoint here. It is an extension of JoinPoint, exposing the additional proceed() method. When invoked, the code execution jumps to the next advice or to the target method. While JoinPoint is compatible with @Before, @After, @AfterThrowing and@AfterReturning advice, ProceedingJoinPoint can only be used with @Around. Metrics or caching are some typical usages. The info-level logging will log the measured time; we will concentrate, however, on the debug log, used for demonstration purposes.

The next step is to extend AbstractMetricsAspect. We call performLogging() in the @Around advice as well as define a pointcut – for example to advise method executions in controller classes.

@Aspect
public class ControllerMetricsAspect extends AbstractMetricsAspect {
    
    @Pointcut("within(@org.springframework.web.bind.annotation.RestController *) && execution(* *(..))")
    public void pointcutForControllerMetricsLogging() {
        // intentionally left blank
    }
    
    @Around("pointcutForControllerMetricsLogging()")
    public Object logMetrics(ProceedingJoinPoint joinPoint) throws Throwable {
        return performLogging(joinPoint, "ControllerMetricsAspect");
    }
}

Let us have a closer look at this pointcut definition. It takes any class (first *) having the RestController annotation and within that class runs the aspect at any execution join point – the part * *(..) identifies any method with any type and number of arguments. This is our BookController the advice should take effect on:

@RestController
@RequestMapping("/books")
public class BookController {
    
    private final BookService bookService;
    
    public BookController(BookService bookService) {
        this.bookService = bookService;
    }
    
    @GetMapping()
    public List<BookDto> getBooks() {
        return bookService.getSortedBooks();
    }
}

When we call the /books endpoint, we expect the getBooks() method to be advised, and this is indeed what we see in the logs:

DEBUG: ControllerMetricsAspect's source location: BookController.java:22; type: method-execution

If we had more controllers but for some reason wanted to apply the aspect on BookController only, we could have a pointcut like the one below:

@Pointcut("execution(* hu.danubius.aspectdemo.controller.BookController.*(..))")

Here the first asterisk means any return type and the wildcard at the end with the dots in parentheses refers to any method with any arguments again. We would have exactly the same log with this pointcut definition.

 

AspectJ pointcuts

 

Pre-initialization and initialization

AspectJ has many more pointcut designators other than just execution. What happens if we do not specify the type of join points at all? Let us try the following pointcut:

@Pointcut("within(@org.springframework.web.bind.annotation.RestController *)")

We see warnings during compilation: initialization and pre-initialization are not suitable for use in an "around" advice.

[WARNING] around on initialization not supported (compiler limitation)

[WARNING] around on pre-initialization not supported (compiler limitation)

These two join points are connected to object creation: pre-initialization encompasses the time between the start of the constructor call and the start of the parent class’s constructor call, that is, when the evaluation of the arguments of this() and super() happens. The initialization pointcut matches the join point when the object initialization code for a particular class runs, encompassing the return from the super constructor call to the return of the first-called constructor. So the reason behind the warnings is that unlike method execution, they do not represent a call that can be intercepted and resumed. Object construction cannot be modified, replaced or skipped in an "around" advice.

 

Staticinitialization

Still with the same pointcut expression, we can see the following debug logs on application startup:

DEBUG: ControllerMetricsAspect's source location: BookController.java:0; type: staticinitialization

DEBUG: ControllerMetricsAspect's source location: BookController.java:17; type: constructor-execution

DEBUG: ControllerMetricsAspect's source location: BookController.java:18; type: field-set

Our metrics aspect is run three times as the class is loaded and initialized. The staticinitialization pointcut designator is related to class initialization; it runs once, matching the execution of a static initializer block. This might be surprising as our controller does not contain anything static; however, if we have a look at the woven class, created at compile time, we can see a static initializer block generated by AspectJ. The ajc$preClinit() method captures any initialization AspectJ wants to ensure happens in a class.

static {
    ajc$preClinit();
    JoinPoint var0 = Factory.makeJP(ajc$tjp_5, (Object)null, (Object)null);
    ControllerMetricsAspect var10000 = ControllerMetricsAspect.aspectOf();
    Object[] var1 = new Object[]{var0};
    var10000.logMetrics((new BookController$AjcClosure11(var1)).linkClosureAndJoinPoint(65536));
}

The field-set join point does not need extensive explanation: BookService, the controller’s dependency is injected via the constructor.

 

Execution vs. call

Let us now call the /books endpoint. We can see the following debug logs:

DEBUG: ControllerMetricsAspect's source location: BookController.java:22; type: method-execution

DEBUG: ControllerMetricsAspect's source location: BookController.java:23; type: field-get

DEBUG: ControllerMetricsAspect's source location: BookController.java:23; type: method-call

They speak for themselves: the method getBooks() is executed; in its body the bookService field is referenced and its getSortedBooks() method is called. One might notice something interesting, though: why is there no method-call log for the controller method itself?

The explanation lies in the differences of execution and call. In the case of execution, the pointcut matches the execution time exactly, that is, the call site is the method itself. Weaving is done at only one place: the executed method. In contrast, the call site of call is the enclosing code, and all the callers of the advised method will get woven. This means that using call on a method that is invoked from 10 different classes will result in all those 10 classes being woven, while using execution will result in only the class containing the executing method being woven. We should also note that call cannot handle polymorphism, nor capture super calls. What are some typical use cases for each? When we would like to apply different advice based on the caller, or track calls to methods with particular signatures, call can come in handy. However, if we need to advise all executions of a method, regardless of where it is called from, or when we modify the behavior of the method itself, we should use execution.

Now it is easy to tell why there is no log for the controller method call. The incoming request is processed by Spring’s DispatcherServlet and delegated to BookController, our advised class. In other words, the call site lies outside of our source code – deep within the Spring framework –, so AspectJ cannot access and weave it at compile time.

 

Target vs. this

Another interesting comparison can be made between the pointcut designators target and this:

@Pointcut("this(hu.danubius.aspectdemo.controller.BookController)")

@Pointcut("target(hu.danubius.aspectdemo.controller.BookController)")

Both will result in constructor-execution and field-set logs on application startup; and on calling the endpoint, both will also log method-execution and field-get. The only difference is that this logs the method-call on the getSortedBooks() method, too, but target doesn’t. The reason for this behaviour is that this matches join points inside the given object — the BookController bean —, whereas target matches join points at which this object is being invoked / referenced. We already know that in the case of execution-type join points the object containing the join point is the same as the one invoking it, which explains why both this and target logs constructor-execution, method-execution, field-set and field-get. The call on getSortedBooks() shows the difference, since the object making the call (BookController) is not the same as the one on which the method is being called (BookService).

 

A note on Java versions

Final fields can be assigned only upon declaration or within the constructor of the class. Any attempt to modify a final field outside these contexts results in error. In Java 8, the JVM is more lenient in enforcing these rules at runtime, which allows tools like AspectJ to weave code that modifies final fields outside their initializers without runtime errors. However, in later Java versions (there is a bug report documenting it for Java 11), the JVM enforces stricter runtime checks on final field assignments. As a result, modifying a final field outside its initializer method triggers an IllegalAccessError. So in Java 21, for example, our ControllerMetricsAspect pointcuts that do not specify "execution" or "call" would fail: "Update to non-static final field hu.danubius.aspectdemo.controller.BookController.bookService attempted from a different method (bookService_aroundBody0) than the initializer method <init>". We would have to explicitly exclude the final field assignment from them:

@Pointcut("within(@org.springframework.web.bind.annotation.RestController *) &&" +"

!execution(hu.danubius.aspectdemo.controller.BookController.new(..)) && !set(private final * *)")

 

AspectJ and interfaces

 

We defined an interface for sorting in our demo application to examine pointcuts on interfaces, and it has a simple implementation, used by BookService:

public interface Sorter<T> {
    List<T> sort(List<T> items);
}
@Service
public class BookSorter implements Sorter<BookDto> {
    
    @Override
    public List<BookDto> sort(List<BookDto> items) {
        return items
            .stream()
            .sorted(Comparator.comparing(BookDto::getTitleWithAuthor))
            .collect(Collectors.toList());
    }
}

Let us add SorterMetricsAspect, at first with an execution-type pointcut on the actual sorter implementation.

@Aspect
public class SorterMetricsAspect extends AbstractMetricsAspect {
    
    @Pointcut("execution(* hu.danubius.aspectdemo.service.interfaces.BookSorter.*(..))")
    public void pointcutForSorterMetricsLogging() {
        // intentionally left blank
    }
    
    @Around("pointcutForSorterMetricsLogging()")
    public Object logMetrics(ProceedingJoinPoint joinPoint) throws Throwable {
        return performLogging(joinPoint, "SorterMetricsAspect");
    }
}

The log we see on calling the endpoint is just what we expect:

DEBUG: SorterMetricsAspect's source location: BookSorter.java:14; type: method-execution

Let us now experiment a little with pointcuts. The above execution-type pointcut could use not only the implementation but directly the Sorter interface itself. The following variations will result in the same debug log as above:

@Pointcut("execution(* hu.danubius.aspectdemo.service.interfaces.Sorter.*(..))")

@Pointcut("within(hu.danubius.aspectdemo.service.interfaces.Sorter+) && execution(* *(..))")

They would log sorting, of course, in any existing implementation of Sorter, not only BookSorter. The + sign is worth a note: it designates a SubTypePattern, so the second pointcut can be interpreted as "execution of any method within any implementation of Sorter". What happens if we leave it out, as below?

@Pointcut("within(hu.danubius.aspectdemo.service.interfaces.Sorter) && execution(* *(..))")

There will be no SorterMetricsAspect log – our aspect is not woven at all, there is no matching join point. If we even leave out the "execution" part, our code will not even compile:

ERROR: around on staticinitialization of interface 'hu.danubius.aspectdemo.service.interfaces.Sorter' not supported (compiler limitation)

Interfaces are special in that they form a contract between the implementing class and the outside world, enforced at build time by the compiler, but they do not contain executable code, only method signatures. AspectJ needs the actual implementation to intercept and apply the aspect on.

We can use call with interfaces if we are interested in where the given method is called from, either applying the aspect on all implementations or on a specific one:

@Pointcut("call(* hu.danubius.aspectdemo.service.interfaces.Sorter.*(..))")

@Pointcut("call(* hu.danubius.aspectdemo.service.interfaces.BookSorter.*(..))")

The log we see contains BookService, which is woven as the call site of the sorting.

DEBUG: SorterMetricsAspect's source location: BookService.java:36; type: method-call

 

AspectJ and repositories

 

In the previous section, we had a look at how we can define execution- or call-type pointcuts for our own interfaces. Applying metrics on database queries is a common requirement in projects, so let us write an aspect for the @Repository interface. We already know that we must avoid the "staticinitialization" compile-time error, so we could come up with a solution like below:

@Aspect
public class AspectJRepositoryMetricsAspect extends AbstractMetricsAspect {
    
    @Pointcut("within(org.springframework.data.repository.Repository+) && execution(* *(..))")
    public void pointcutForRepositoryMetricsLogging() {
        // intentionally left blank
    }
    
    @Around("pointcutForRepositoryMetricsLogging()")
    public Object logMetrics(ProceedingJoinPoint joinPoint) throws Throwable {
        return performLogging(joinPoint, "AspectJRepositoryMetricsAspect");
    }
}

Unfortunately, it does not work, BookRepository is not woven, we see no logs for this aspect. Unlike our own interface, there is no accessible implementation: Spring Data repositories are proxy implementations at runtime, provided by Spring, so there is no repository implementation available for AspectJ to weave at compile-time. What we can do is to rewrite our pointcut, using call, to weave the classes in our source code that invoke methods on the repository.

@Pointcut("call(* hu.danubius.aspectdemo.repository.BookRepository.*(..))")

This will log the findAll() method call made from BookService:

DEBUG: AspectJRepositoryMetricsAspect's source location: BookService.java:27; type: method-call

AspectJ, while versatile and applicable in most use cases, has limitations when Spring’s internal working is concerned, as it lacks runtime weaving.

 

Spring AOP pointcuts

 

Spring AOP supports only method execution join points in Spring-managed beans – whether they belong to our source code or to the Spring framework. The underlying mechanism involves runtime weaving via proxy objects.

Let us solve our repository problem with Spring AOP, creating a new aspect:

@Aspect
@Component
public class AOPRepositoryMetricsAspect extends AbstractMetricsAspect {
    
    @Pointcut("within(org.springframework.data.repository.Repository+)")
    public void pointcutForRepositoryMetricsLogging() {
        // intentionally left blank
    }
    
    @Around("pointcutForRepositoryMetricsLogging()")
    public Object logMetrics(ProceedingJoinPoint joinPoint) throws Throwable {
        return performLogging(joinPoint, "AOPRepositoryMetricsAspect");
    }
}

The pointcut matches execution join points in any repository implementation; the + SubTypePattern designator is still necessary for the suitable implementations to be proxied. We have to add @Component in order to let Spring load and instantiate the class; also, since the project is already configured for using AspectJ, we need to modify the aspectj-maven-plugin configuration in our pom.xml and exclude this bean from being handled by AspectJ. On calling the endpoint, we can see a debug log like the one below:

DEBUG: AOPRepositoryMetricsAspect's source location: org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint$SourceLocationImpl@c048d85; type: method-execution

The source location is the proxy class created by runtime weaving. Running the debugger shows that the intercepted method is JpaRepository’s findAll(), invoked through a proxy:

 

Target vs. this

We have seen that in AspectJ the pointcut designators this and target behave differently when it comes to method calls. Although Spring AOP handles only method executions, there is a slight difference between them, too, and the reason is related to proxying. The Spring documentation makes it quite clear: this matches join points "where the bean reference (Spring AOP proxy) is an instance of the given type", whereas target matches join points "where the target object (application object being proxied) is an instance of the given type". So this refers to the proxy object, while target to the proxied object. When do we have to pay attention to this distinction? Whenever CGLIB proxying is enabled, both pointcut types will work the same way. However, if our Spring Boot application allows only JDK dynamic proxies (spring.aop.proxy-target-class=false), and we have an interface MyService, we should keep in mind that target(MyServiceImpl) will match because target refers to the actual object, but this(MyServiceImpl) will not, because this refers to the proxy object, which implements only MyService (not MyServiceImpl).

 

Conclusion

 

In this two-part blog article series, we aimed to provide insight into some key differences of AspectJ and Spring AOP, with a focus on pointcuts. As we have seen, both AOP frameworks offer powerful ways to apply cross-cutting concerns, but they differ in scope and implementation. AspectJ provides a more comprehensive approach with compile-time and load-time weaving, enabling pointcuts that go beyond method execution to include field access, object construction, and method calls. Spring AOP, being proxy-based, is limited to method-level interception on Spring-managed beans, making it more lightweight and optimal for Spring-specific solutions. The effectiveness of both AOP implementations heavily depends on well-crafted pointcuts, as they determine where and how advice is applied. They should balance precision and maintainability – too broad pointcuts may lead to unintended advice execution, while overly specific ones can reduce reusability. By understanding their differences, developers can find the most effective aspect-oriented solutions tailored to their application's needs.

 

Source code

https://github.com/danubiusinfo/aspect-demo (Java 8 on master branch, Java 21 on separate branch)

 

Resources

 

The article was written by Zsuzsanna Benkő, software developer at Danubius IT Solutions.

Interested in IT solutions tailored to your business? Contact us for a free consultation, where we'll collaboratively explore your needs and our methodologies.

Read our tech blog

Pointcuts Explained: Spring AOP vs. AspectJ in Handling Cross-Cutting Concerns - Part 1

Pointcuts Explained: Spring AOP vs. AspectJ in Handling Cross-Cutting Concerns - Part 1

As software systems grow in complexity, efficiently managing cross-cutting concerns becomes increasingly challenging. Pointcuts, a central feature in Aspect-Oriented Programming, are essential for defining where exactly these concerns should take effect in the application flow.

7 minutes
AI for C-level managers: hype or not?

AI for C-level managers: hype or not?

C-level managers play a crucial role in identifying and allocating resources to the most valuable opportunities, regardless of whether they involve AI. It’s not about the hype, it’s about the revenue. Some things never change – the fundamental principles of finance and economics remain the same: companies must generate returns above their cost of capital. Where is the AI in it?

5 minutes
Prompt Engineering – Is it Fake or a Vital Skill for the AI-Powered Future?

Prompt Engineering – Is it Fake or a Vital Skill for the AI-Powered Future?

In our continued journey to innovate within the AI-driven customer support landscape, we would like to draw attention to a term that's often misunderstood outside tech circles: prompt engineering. Prompt engineering emerges as a cornerstone of AI engineering, vital for refining the capabilities of large language...

5 minutes

Listen to our podcast below

Scrum Mastery Unveiled: Part 1

Scrum Mastery Unveiled: Part 1

Scrum Masters are more than just facilitators; they are often a key part of a development team, ensuring efficiency, timely delivery, and much more. We discussed their challenges with two of our experts, Zsofia Samodai and...

55 minutes
Hello, Danubius!

Hello, Danubius!

The two founders of Danubius IT Solutions, Peter Balogh and Peter Halasz, sat down to discuss the company's past, present, and future. Our discussions covered the creation and development of an organizational culture, as well as scaling the company from two coworkers to 100.

31 minutes