Aspect Oriented Programming in Java with AspectJ

04 January, 2024 |  Vladimir Djurovic 
img/aop-aspectj.jpg

Table Of Contents

Aspect Oriented Programming (AOP) is a programming paradigm that can increase modularity and separation of concerns in code. In this blog post, we’ll cover the basics of AOP in Java and get our hands dirty with working code samples using AspectJ, an open source implementation of AOP for Java.

Sample code for this post can be found in Github repository.

Introduction to Aspect Oriented Programming (AOP)

As mentioned above, Aspect-Oriented Programming is a programming paradigm that provides a way to modularize and manage cross-cutting concerns. In software development, a concern is a specific aspect of a program’s behavior or functionality. Examples of concerns include logging, security, error handling, and performance monitoring.

Cross-cutting concerns are concerns that affect multiple parts of an application and cannot be cleanly separated from the core business logic. For instance, logging and security may need to be added to various methods throughout the codebase. In traditional programming, cross-cutting concerns are often scattered throughout the codebase. This can lead to code duplication, reduced maintainability, and difficulty in understanding the overall structure of the program.

In AOP, cross-cutting concerns are isolated into separate blocks and can be applied to multiple code paths without cluttering business logic. To visualize this, take a look at the image bellow:

Diagram showing cross cutting concerns in AOP

Cross cutting concerns in AOP

Here, logging and security are cross cutting concerns. They are applied to all the layers in the application, but instead of explicitely adding logging and security code to application logic, they are extracted into separate modules. AOP allows for these concerns to be applied at specific points in the code.

Using AOP, you can greatly improve modularity and maintainability of the code. Changes to the concerns need only to be done in one place rather then being scattered across the codebase. In addition, this helps keep the business logic clean and focused on it’s primary functionality.

Key concepts in Aspect Oriented Programming

In this section, let’s look into basic concepts related to Aspect Oriented Programming.

Advice is the additional code that you want to apply to your existing code. Looking at the diagram of cross-cutting concerns above, the logging or security code that needs to run in all layers is the advice. Advice allows developers to insert code at specific points in the program’s execution without modifying the core logic. Types of advice include before, after, and around.

Advice type names refer to when the advice is being executed. It can be before the target method, after it or around (before AND after).

Pointcut is the point of execution in application where cross-cutting concern needs to be executed. It defines where in the codebase the advice should be applied. This is usually an expression which selects all matching points where advice is to be applied.

Aspect is a module or class encapsulating cross-cutting concerns which defines how and where the cross-cutting logic should be applied. Aspects help modularize cross-cutting concerns, promoting separation of concerns in the codebase. You can think of an aspect as the combination of pointcut and advice.

Join point is a well-defined point in the execution of a program, such as a method call, an exception being thrown, or the handling of an exception. Join points are where the execution of the program can be altered by applying aspects. Aspects specify these join points using pointcuts.

Weaving is the process of integrating aspects into the main application code at various points in the software development life cycle—compile-time, load-time, or runtime. Weaving combines the aspects with the application, ensuring that the additional behavior specified in aspects is applied to the appropriate join points. We’ll look at weaving in more details in the next section.

Weaving implementations in Java

Aspect-Oriented Programming (AOP) in Java can be implemented using various weaving strategies, each of which introduces aspects into the codebase at different stages of the software development life cycle. There are 3 common approaches: compile-time weaving, load-time weaving and runtime weaving. Let’s take a look at each one of them in more detail.

Compile time weaving involves modifying the Java source code before it is compiled. A separate tool, typically the AspectJ compiler, is used to weave aspects into the code. This approach leads to most performant code, since the resulting code is optimized during compilation. In addition, it is possible to catch errors early, during compilation step.

On the other hand, this approach complicates build process, since it involves additional compilation step. It is also less dynamic, since changes to aspectes require recompilation of the entire codebase.

Load time weaving involves applying aspects when classes are loaded into the Java Virtual Machine (JVM). It uses a Java agent to dynamically weave aspects at runtime. This approach allows for aspects to be developed and applied independently, which allows for better separation of concerns.

As a bad side, there is an overhead during class loading , which can potentially impact application loading time. There is also an additional complexity involved, since you need to configure Java agent.

Runtime weaving involves applying aspects during the execution of the program. This can be achieved through a runtime container or framework that dynamically applies aspects. This is the most flexible approach, since aspects can be added and modified without restarting the application. Aspects can also adapt to runtime conditions and change behaviour accordingly.

The con of this approach is the overhead during execution as aspects are applied dynamically. There is also a potential for errors which can only be discovered at runtime.

Choosing the best approach depends on development and deployment requirements, as well as the performance considerations.

Java libraries for Aspect Oriented Programming

Java ecosystem has several libraries for AOP implementation. Here, we’ll list some of the most prominent:

  • AspectJ - AspectJ is a widely-used AOP framework for Java. It allows developers to use aspects to modularize cross-cutting concerns and apply them declaratively. AspectJ supports both compile-time and load-time weaving.
  • Spring AOP - Spring AOP is a part of the Spring Framework and provides support for AOP in Java. It allows developers to use aspects to add behavior to existing code without modifying the code itself. Spring AOP supports both proxy-based and AspectJ-style AOP.
  • Javassist - Javassist is a bytecode manipulation library that can be used for AOP in Java. It allows developers to modify the bytecode of classes at runtime, enabling the use of aspects to add behavior to existing code.

In this post, we’ll focus on AspectJ for implementation of AOP.

Getting started with AspectJ

Now it’s time to get our hands dirty with some code! We’ll create a simple Maven project which uses AspectJ and it’s Maven plugin to apply aspects. We’ll use compile-time weaving for best performance of our code. You can find the complete source code in Github repository.

The first order of business is to configure AspectJ Maven plugin to perform waeving at compile time. This code snippet from Maven POM performs configuration:

 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
<properties>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  <maven.compiler.source>21</maven.compiler.source>
  <maven.compiler.target>21</maven.compiler.target>
  <aspectj.version>1.9.21</aspectj.version>
</properties>
...
<dependencies>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>${aspectj.version}</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>${aspectj.version}</version>
    </dependency>
</dependencies>

...
<build>
<plugins>
    <plugin>
    <groupId>dev.aspectj</groupId>
    <artifactId>aspectj-maven-plugin</artifactId>
    <version>1.13.1</version>
    <dependencies>
        <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjtools</artifactId>
        <version>${aspectj.version}</version>
        </dependency>
    </dependencies>
    <configuration>
        <complianceLevel>21</complianceLevel>
        <showWeaveInfo>true</showWeaveInfo>
        <verbose>true</verbose>
        <encoding>UTF-8</encoding>
    </configuration>
    <executions>
        <execution>
        <goals>
            <goal>compile</goal>
        </goals>
        </execution>
    </executions>
    </plugin>
</plugins>
</build>

In lines 1-6, we set some common properties. In this case, we want to use Java 21 and AspectJ version 1.9.21. Next, in lines 8-19 we add dependencies required for developing aspects.

Lines 22-50 configre AspectJ Maven plugin. here, we set bytecode compliance level to be Java 21 and set more verbose output for AspectJ compiler. Execution phase is set to compile, so this bytecode weaving will be performed at compile time.

For our example, we will create a simple class called Greeter, which has a single method greet(). Source code for this class:

1
2
3
4
5
6
public class Greeter {

    public void greet(String name) {
        System.out.println("Hello, " + name);
    }
}

Ths method will simply output a greeting for a given name. This class will be called from main class called App:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class App {
    public static void main( String[] args ) {
        var greeter = new Greeter();
        String name = null;
        if(args.length > 0) {
            name = args[0];
        }
        greeter.greet(name);
    }
}

You can specify name as parameter on command line. To run this program, simply run the following command:

java -cp target/classes co.bitshifted.sample.App
Hello, null

Output will be Hello, null, since we did not set any name at command line.

Now that we have the basic setup working, let’s create an aspect that will be called when our methods execute.

Annotation-based aspects

AspectJ defines it’s own programming language for development of aspects, but it is also possible to use plain Java. With this approach, you use specific annotations to define Java classes as aspects. Let’s create a simple logging aspect:

1
2
3
4
5
6
7
8
9
@Aspect
public class LoggingAspect {

    @Around("execution(* *(..))")
    public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("LoggingAspect called..!");
        return joinPoint.proceed();
    }
}

Annotation @Aspect declares annotated class as aspect. Method log is annotated with @Around annotation, which indicates that this advice will run before and after the target method. Implementation of this method will simply print a log message and continue.

Take a note of execution(* *(..)) part. This is pointcut definition. It specifies that this advice will match execution of any method in the program. It is, of course, possible to be more specific here and reduce the scope of matches, but for demonstration purposes, we want to match any method. If you now compile the project, you will notice that AspectJ compiler is being run:

[INFO] --- aspectj:1.13.1:compile (default) @ aspectj-aop ---
[INFO] Showing AJC message detail for messages of types: [error, warning, fail]
CLASSPATH component /usr/local/programs/idea-IC-232.10072.27/plugins/maven/lib/maven3/boot/plexus-classworlds.license: java.util.zip.ZipException: zip END header not found
[INFO] Join point 'method-execution(void co.bitshifted.sample.App.main(java.lang.String[]))' in Type 'co.bitshifted.sample.App' (App.java:8) advised by around advice from 'co.bitshifted.sample.aspect.LoggingAspect' (LoggingAspect.java:11)
[INFO] Join point 'method-execution(void co.bitshifted.sample.Greeter.greet(java.lang.String))' in Type 'co.bitshifted.sample.Greeter' (Greeter.java:5) advised by around advice from 'co.bitshifted.sample.aspect.LoggingAspect' (LoggingAspect.java:11)
[INFO] 

Compiler output shows that two methods match the pointcut and corresponding join pointes are being weaved with arvice. If you now run the application, you will get the following output:

LoggingAspect called..!
LoggingAspect called..!
Hello, null

Both matched methods have been advised with our logging aspect.

Conclusion

In this post, we’ve seen how Aspect Oriented Programming can be used to enrich applications developed with Object Oriented Programming paradigm. Our example with AspectJ is simple, but ilustrates use case for AOP in practice.

Many people have differing opinions about AOP. Some hate it, some think it’s best thing since the sliced bread. My opinion is that it can very useful in specific situations, but as with any tool, abusing it can lead to spectacular failures.

What is your opinion about this blog post and AOP in general? Feel free to comment using the form bellow.