Spring AOP AspectJ Tutorial
In aspect-oriented programming (AOP) the main focus, as the name suggests, is aspects. Whereas, Object-Oriented Programming (OOP) focus of modularity is the object or class. In AOP, the base unit of modularity is the Aspect. It allows the developer to build more modular code without extensive modification of existing code. Aspects enable the implementation of cross-cutting concerns which would normally be scattered across the entire application and would result in code duplication. These concerns such as logging, auditing, caching, transaction management, security, performance monitoring are usually referred to as secondary concerns as primary concern would be to address some specific business need.
AOP Concepts
Let’s start with defining some of the central tenets of Aspect Oriented Programming (AOP):
Aspect — A module which contains both a pointcut and an advice and provides a modularization of a concern that may cuts across multiple classes. A logging framework decribed in this tutorial is a good example of a crosscutting concern that is a perfect candidate for AOP aspects. In Spring AOP, aspects are implemented using regular classes annotated with the @Aspect annotation.
Advice — The action taken by an aspect at a particular join point. Advice types include “around”, “before”, and “after”. Spring AOP model advise as an interceptor, maintaining a chain of interceptors around the join point. In Spring AOP, advice types annotated with the @Before, @After, @AfterReturning, @AfterThrowing, @Around annotations.
Pointcut — an expression that matches join point. Advice is associated with pointcut expresson and runs at any join point matched by the pointcut (use of execution expression that matches all classes of a package containing get* in the method name).
Join Point — a point during the execution of a program, such as the execution of a method or the handling of an exception. In Spring AOP, a join point always represents a method execution (this is the only type supported by Spring AOP). Spring aspects can not be applied final or static methods as these can not be overridden.
Cross Cutting Concerns (Use Cases)
- Logging
- Performance Monitoring
- Security
- Error Detection and Correction
- Monitoring
- Data Validation
- Persistence
- Synchronization
- Transaction Handling
Spring AOP Advice Types
- Before Advice: Advice that runs before a join point but that does not have the ability to prevent execution flow proceeding to the join point (unless it throws an exception).
- After Advice: Advice to be executed irrespective of how the join point exits (normal or with exception return)
- After Returning Advice: Advice that runs after returning (NORMALLY) from a join point, that is, without throwing an exception
- After Throwing Advice: Advice that runs after (EXCEPTION) a join point method exits with an exception.
- Around Advice: Advice that surrounds a join point such as a method invocation. This is the most powerful kind of advice. Around advice performs custom behavior before and after the join point. It is also responsible for choosing whether to proceed to the join point or to shortcut the advised method execution by returning its own return value or throwing an exception.
Project Structure
POM.xml
Add the Spring AOP AspectJ dependencies in Maven’s pom.xml file as highlighted below:
<?xml version="1.0" encoding="UTF-8"?> <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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.7.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.avaldes</groupId> <artifactId>aop</artifactId> <version>0.0.1-SNAPSHOT</version> <name>aop</name> <description>AOP Performance Logging in Spring Boot</description> <properties> <maven-jar-plugin.version>3.1.1</maven-jar-plugin.version> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-web</artifactId> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</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-aop</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Performance Logging Aspect class
Let’s begin by defining the aspect class that will also contain the pointcut definitions. For now, the basic definition of the class will be as follows:
@Aspect @Component public class PerformanceLoggingAspect { ... }
In all of my Spring AOP examples in this tutorial I will be using Java annotations and not the Spring XML configurations to define advise. In this example, we will create a simple spring boot application class, we will add our logging around the aspect and invoke the aspect methods based on pointcut defined in the @Around annotation.
AspectJ @Around annotation
@Around advise is used to surround a join point such as method invocation. This type of advise is the most powerful as it allows you to add custom functionality before and after method invocation.
package com.avaldes.aop.aspect; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import org.springframework.util.StopWatch; @Aspect @Component public class PerformanceLoggingAspect { final static Logger logger = LogManager.getLogger(PerformanceLoggingAspect.class); @Around("execution(* com.avaldes.aop.dao.InvoiceRepository.get*(..))") public Object logAroundGetMethods(ProceedingJoinPoint joinPoint) throws Throwable { logger.info("***** LoggingAspect on get Methods: " + joinPoint.getSignature().getName() + ": Before Method Execution"); StopWatch watch = new StopWatch(); watch.start(); Object result = joinPoint.proceed(); watch.stop(); logger.info("***** {} => Elapsed Time {}ms", joinPoint.getSignature().getName(), watch.getLastTaskTimeMillis()); return result; } @Around("@annotation(com.avaldes.aop.aspect.PerfLogger)") public Object performanceLogger(ProceedingJoinPoint joinPoint) throws Throwable { logger.info("***** performanceLogger on get Methods: " + joinPoint.getSignature().getName() + ": Before Method Execution"); StopWatch watch = new StopWatch(); watch.start(); Object result = joinPoint.proceed(); watch.stop(); logger.info("***** {} => Elapsed Time {}ms", joinPoint.getSignature().getName(), watch.getLastTaskTimeMillis()); return result; } }
Pointcut Expressions
A Pointcut is an expression in Spring AOP that allows you to match the target methods to apply the advise to.
In this example, all GETTER methods in the InvoiceRepository class are the targets for the AspectJ advise.
@Around("execution(* com.avaldes.aop.dao.InvoiceRepository.get*(..))")
Additionally, we can create custom annotations and then use these annotations to mark which methods we want to target for AOP advise.
@Around("@annotation(com.avaldes.aop.aspect.PerfLogger)")
Custom Annotation
Creating custom annotations is used by using the public @interface, followed by the annotation name as shown in the below example.
package com.avaldes.aop.aspect; public @interface PerfLogger { }
Enabling @AspectJ Support
In order to enable AOP in spring we will need to add the @EnableAspectJAutoProxy annotation in the configuration class. In my example, we create an AopConfig class and enable aspectJ via the config class.
package com.avaldes.aop.config; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @EnableAspectJAutoProxy public class AopConfig { }
SpringBoot Application
After using Spring Initializr it creates a simple application class for you. However, for our purpose this class is too simple. We modify the application class and add CommandLineRunner so we can test out various Log4J log levels.
package com.avaldes.aop; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.EnableAspectJAutoProxy; @SpringBootApplication public class AopApplication implements CommandLineRunner { final static Logger logger = LogManager.getLogger(AopApplication.class); public static void main(String[] args) { SpringApplication.run(AopApplication.class, args); } @Override public void run(String... args) throws Exception { // Testing Log4J2 logging logger.trace("Logging trace level here..."); logger.debug("Logging debug level here..."); logger.info("AopApplication run method Started !!"); logger.warn("Logging warn level here..."); logger.error("Oops something bad happened here !!!"); } }
Rest Controller
Now we create a rest controller class with the proper @RestController annotation meaning it is ready for use for Spring MVC to handle requests.
package com.avaldes.aop.controller; import java.util.List; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.avaldes.aop.dao.AccountRepository; import com.avaldes.aop.dao.InvoiceRepository; import com.avaldes.aop.model.Account; import com.avaldes.aop.model.EmptyJsonBody; import com.avaldes.aop.model.Invoice; import com.avaldes.aop.aspect.PerfLogger; @RestController @RequestMapping("/rest") public class AopRestController { final static Logger logger = LogManager.getLogger(AopRestController.class); @Autowired private AccountRepository accountRepository; @Autowired private InvoiceRepository invoiceRepository; public AopRestController() { logger.info("Inside AopRestController() Constructor..."); } @PerfLogger @GetMapping("/accounts/getAll") public ResponseEntity<List<Account>> getAllAccounts() { List<Account> accounts = accountRepository.getAllAccounts(); return new ResponseEntity<>(accounts, HttpStatus.OK); } @GetMapping("/accounts/get/{id}") public ResponseEntity<?> getById(@PathVariable("id") String id) { HttpHeaders headers = new HttpHeaders(); headers.add("Content-Type", "application/json"); Account account = accountRepository.getAccountById(id); if (account != null) { return new ResponseEntity<>(account, headers, HttpStatus.OK); } else { return new ResponseEntity<>(new EmptyJsonBody(), headers, HttpStatus.NOT_FOUND); } } @GetMapping("/invoices/getAll") public ResponseEntity<List<Invoice>> getAllInvoices() { List<Invoice> invoices = invoiceRepository.getAllInvoices(); return new ResponseEntity<>(invoices, HttpStatus.OK); } @GetMapping("/invoices/get/{id}") public ResponseEntity<?> getInvoiceById(@PathVariable("id") String id) { HttpHeaders headers = new HttpHeaders(); headers.add("Content-Type", "application/json"); Invoice invoice= invoiceRepository.getInvoivceById(id); if (invoice != null) { return new ResponseEntity<>(invoice, headers, HttpStatus.OK); } else { return new ResponseEntity<>(new EmptyJsonBody(), headers, HttpStatus.NOT_FOUND); } } @GetMapping("/invoices/accounts/{num}") public ResponseEntity<?> getInvoiceByAccountId(@PathVariable("num") String num) { HttpHeaders headers = new HttpHeaders(); headers.add("Content-Type", "application/json"); List<Invoice> invoices = invoiceRepository.getInvoiceByAccountId(num); return new ResponseEntity<>(invoices, HttpStatus.OK); } }
Account Repository
I created a repository class to store and retrieve account details for our example. For the sake of simplicity I created an accountMap and used a static variable to store a list of dummy accounts for this tutorial. We will then use a few methods to retrieve all accounts or get a specific account by id. I have commented out the repeating performance logging statements in both methods as these will be handled by the Spring AOP advise.
package com.avaldes.aop.dao; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Repository; import org.springframework.util.StopWatch; import com.avaldes.aop.aspect.PerfLogger; import com.avaldes.aop.model.Account; import com.avaldes.aop.util.Utility; @Repository public class AccountRepository { final static Logger logger = LogManager.getLogger(AccountRepository.class); private static Map<String, Account> accountMap = new HashMap<>(); static { logger.info("Inside AccountRepository() static data loader..."); accountMap.put("111", new Account("111", "Lockheed Martin Corp", "defense contractor", "Lockheed Martin Corp", "100 Main Street", "", "Austin", "TX", "73310", "USA", "sales@lockheedmartin.com")); accountMap.put("201", new Account("201", "General Dynamics Corp", "defense contractor", "General Dynamics Corp", "1 Dynamics Way", "", "Atlanta", "GA", "56221", "USA", "sales@generaldynamics.com")); accountMap.put("222", new Account("222", "Raytheon Co", "defense contractor", "Raytheon Co", "1099 Missles Away", "", "Boston", "MA", "10929", "USA", "sales@ratheon.com")); accountMap.put("300", new Account("300", "Northrop Grumman Corp", "defense contractor", "Northrop Grumman Corp", "3000 Grumman Way", "", "Long Island", "NY", "56221", "USA", "sales@northropgrumman.com")); } public AccountRepository() { logger.info("Inside AccountRepository() Constructor..."); } public List<Account> getAllAccounts() { logger.info("Inside getAllAccounts()..."); //StopWatch watch = new StopWatch(); //watch.start(); List<Account> allAccounts = new ArrayList<Account>(accountMap.values()); Utility.slowDown(); //watch.stop(); //logger.info("getAllAccounts() => Elapsed Time {}ms, All Accounts => {}", watch.getLastTaskTimeMillis(), allAccounts); return allAccounts; } public Account getAccountById(String id) { logger.info("Inside getAccountById() with ID: {}", id); if (id != null) { if (accountMap.containsKey(id)) { //StopWatch watch = new StopWatch(); //watch.start(); Utility.slowDown(); Account myAccount = accountMap.get(id); //watch.stop(); //logger.info("getAccountById() => Elapsed Time {}ms, myAccount ID: {} => {}", watch.getLastTaskTimeMillis(), id, myAccount); return myAccount; } } logger.info("myAccount => NOT FOUND !!!"); return null; } }
Invoice Repository
I created a second repository class to store and retrieve invoice details for our example. In this class, I did not remove the logging statements just to compare and contrast logging using the repetitive coding strategies employed by the average programmer.
package com.avaldes.aop.dao; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.springframework.stereotype.Repository; import org.springframework.util.StopWatch; import com.avaldes.aop.model.Invoice; import com.avaldes.aop.util.Utility; @Repository public class InvoiceRepository { final static Logger logger = LogManager.getLogger(InvoiceRepository.class); private static Map<String, Invoice> invoiceMap = new HashMap<>(); private static SimpleDateFormat format = new SimpleDateFormat("MM/dd/yyyy"); static { logger.info("Inside InvoiceRepository() static data loader..."); // public Invoice(String invoiceNum, String accountNum, Date invoiceDate, String invoiceDescription, double invoiceAmount) try { invoiceMap.put("899", new Invoice("899", "222", format.parse("02/11/2017"), "Invoice for seats", 23500.75)); } catch (ParseException e) { e.printStackTrace(); } try { invoiceMap.put("900", new Invoice("900", "222", format.parse("09/4/2017"), "Invoice for engine heads", 74292.98)); } catch (ParseException e) { e.printStackTrace(); } try { invoiceMap.put("901", new Invoice("901", "201", format.parse("9/15/2016"), "Invoice for wing parts", 187290.55)); } catch (ParseException e) { e.printStackTrace(); } try { invoiceMap.put("902", new Invoice("902", "300", format.parse("10/12/2016"), "Invoice for paint", 18729.39)); } catch (ParseException e) { e.printStackTrace(); } } public InvoiceRepository() { logger.info("Inside InvoiceRepository() Constructor..."); } public List<Invoice> getAllInvoices() { logger.info("Inside getAllInvoices()..."); StopWatch watch = new StopWatch(); watch.start(); List<Invoice> allInvoices = new ArrayList<Invoice>(invoiceMap.values()); //Utility.slowDown(); watch.stop(); logger.info("getAllInvoices() => Elapsed Time {}ms, All Invoices => {}", watch.getLastTaskTimeMillis(), allInvoices); return allInvoices; } public Invoice getInvoivceById(String id) { logger.info("Inside getInvoiceById() with ID: {}", id); if (id != null) { if (invoiceMap.containsKey(id)) { StopWatch watch = new StopWatch(); watch.start(); //Utility.slowDown(); Invoice myInvoice = invoiceMap.get(id); watch.stop(); logger.info("getInvoiceById() => Elapsed Time {}ms, myInvoice ID: {} => {}", watch.getLastTaskTimeMillis(), id, myInvoice); return myInvoice; } } logger.info("myInvoice => NOT FOUND !!!"); return null; } public List<Invoice> getInvoiceByAccountId(String AccountNum) { logger.info("Inside getInvoiceByAccountId()..."); List<Invoice> myInvoices = new ArrayList<Invoice>(); if (AccountNum == null) { return null; } StopWatch watch = new StopWatch(); watch.start(); List<Invoice> allInvoices = new ArrayList<Invoice>(invoiceMap.values()); for (Invoice inv: allInvoices) { if (inv.getAccountNum().equalsIgnoreCase(AccountNum)) { myInvoices.add(inv); } } watch.stop(); logger.info("getInvoiceByAccountId() => Elapsed Time {}ms, All Invoices => {}", watch.getLastTaskTimeMillis(), myInvoices); return myInvoices; } }
Account Model Class
package com.avaldes.aop.model; import java.io.Serializable; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.avaldes.aop.AopApplication; public class Account implements Serializable { private static final long serialVersionUID = -4948518387646170164L; final static Logger logger = LogManager.getLogger(AopApplication.class); private String accountNum; private String displayName; private String accountType; private String businessName; private String businessAddress1; private String businessAddress2; private String city; private String state; private String zipCode; private String country; private String emailAddress; public Account() { logger.info("Inside Account() Constructor..."); } public Account(String accountNum, String displayName, String type, String businessName, String businessAddress1, String businessAddress2, String city, String state, String zipCode, String country, String emailAddress) { super(); logger.info("Inside Account(...) Constructor..."); this.accountNum = accountNum; this.displayName = displayName; this.accountType = type; this.businessName = businessName; this.businessAddress1 = businessAddress1; this.businessAddress2 = businessAddress2; this.city = city; this.state = state; this.zipCode = zipCode; this.country = country; this.emailAddress = emailAddress; } public String getAccountNum() { return accountNum; } public void setAccountNum(String accountNum) { this.accountNum = accountNum; } public String getDisplayName() { return displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; } public String getAccountType() { return accountType; } public void setAccountType(String accountType) { this.accountType = accountType; } public String getBusinessName() { return businessName; } public void setBusinessName(String businessName) { this.businessName = businessName; } public String getBusinessAddress1() { return businessAddress1; } public void setBusinessAddress1(String businessAddress1) { this.businessAddress1 = businessAddress1; } public String getBusinessAddress2() { return businessAddress2; } public void setBusinessAddress2(String businessAddress2) { this.businessAddress2 = businessAddress2; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getState() { return state; } public void setState(String state) { this.state = state; } public String getZipCode() { return zipCode; } public void setZipCode(String zipCode) { this.zipCode = zipCode; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } public String getEmailAddress() { return emailAddress; } public void setEmailAddress(String emailAddress) { this.emailAddress = emailAddress; } public static Logger getLogger() { return logger; } @Override public String toString() { return "Account [accountNum=" + accountNum + ", displayName=" + displayName + ", accountType=" + accountType + ", businessName=" + businessName + ", businessAddress1=" + businessAddress1 + ", businessAddress2=" + businessAddress2 + ", city=" + city + ", state=" + state + ", zipCode=" + zipCode + ", country=" + country + ", emailAddress=" + emailAddress + "]"; } }
Invoice Model Class
package com.avaldes.aop.model; import java.io.Serializable; import java.util.Date; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class Invoice implements Serializable { private static final long serialVersionUID = 9136601541557443191L; final static Logger logger = LogManager.getLogger(Invoice.class); private String invoiceNum; private String accountNum; private Date invoiceDate; private String invoiceDescription; private double invoiceAmount; public Invoice() { logger.info("Inside Invoice() Constructor..."); } public Invoice(String invoiceNum, String accountNum, Date invoiceDate, String invoiceDescription, double invoiceAmount) { super(); logger.info("Inside Invoice(...) Constructor..."); this.invoiceNum = invoiceNum; this.accountNum = accountNum; this.invoiceDate = invoiceDate; this.invoiceDescription = invoiceDescription; this.invoiceAmount = invoiceAmount; } public String getInvoiceNum() { return invoiceNum; } public void setInvoiceNum(String invoiceNum) { this.invoiceNum = invoiceNum; } public String getAccountNum() { return accountNum; } public void setAccountNum(String accountNum) { this.accountNum = accountNum; } public Date getInvoiceDate() { return invoiceDate; } public void setInvoiceDate(Date invoiceDate) { this.invoiceDate = invoiceDate; } public String getInvoiceDescription() { return invoiceDescription; } public void setInvoiceDescription(String invoiceDescription) { this.invoiceDescription = invoiceDescription; } public double getInvoiceAmount() { return invoiceAmount; } public void setInvoiceAmount(double invoiceAmount) { this.invoiceAmount = invoiceAmount; } public static Logger getLogger() { return logger; } @Override public String toString() { return "Invoice [invoiceNum=" + invoiceNum + ", accountNum=" + accountNum + ", invoiceDate=" + invoiceDate + ", invoiceDescription=" + invoiceDescription + ", invoiceAmount=" + invoiceAmount + "]"; } }
Utility Class
This sole purpose of this Utility class is the slowDown method which is used to add some random number generator between 1 and 120 milliseconds. I needed to slow down the calls as they were returning too quickly as data repos are only processing data from in-memory HashMaps.
package com.avaldes.aop.util; public class Utility { // Needed to simulate a longer running query in the database // by adding some randomness and sleep for a little while. public static void slowDown() { int max = 120; int min = 1; int range = max - min + 1; int r = (int)(Math.random() * range) + min; try { Thread.sleep(r); } catch (InterruptedException e) { e.printStackTrace(); } } }
Sample Output
Using the following REST API call using Postman.
localhost:8080/rest/accounts/getAll
2021-01-09 14:32:57:309 [http-nio-8080-exec-2] INFO com.avaldes.aop.dao.AccountRepository - Inside getAccountById() with ID: 300 2021-01-09 14:33:35:874 [http-nio-8080-exec-3] INFO com.avaldes.aop.aspect.PerformanceLoggingAspect - ***** performanceLogger on get Methods: getAllAccounts: Before Method Execution 2021-01-09 14:33:35:875 [http-nio-8080-exec-3] INFO com.avaldes.aop.dao.AccountRepository - Inside getAllAccounts()... 2021-01-09 14:33:35:983 [http-nio-8080-exec-3] INFO com.avaldes.aop.aspect.PerformanceLoggingAspect - ***** getAllAccounts => Elapsed Time 108ms
Please Share Us on Social Media






Leave a Reply