<< Back to Spring Boot Tutorial
<< Back to Spring Boot Tutorial
In this tutorial, we will learn how to use Spring AOP in the Spring boot application. We will implement an Aspect for logging the execution of service, controller, and repository Spring components using Spring AOP.
Important: This tutorial is upgraded to Spring Boot 3 and Java 17.
Before using Spring AOP in the spring boot application, you should familiar with Spring AOP terminology in Spring AOP Tutorial with Example.
Spring AOP provides a way to dynamically add the cross-cutting concern before, after, or around the actual logic using simple pluggable configurations. It makes it easy to maintain code in the present and future as well.
Examples of cross-cutting concerns:
- Logging
- Security
- Transaction management
- Auditing,
- Caching
- Internationalization
- Error detection and correction
- Memory management
- Performance monitoring
- Synchronization
In this tutorial, we will implement an Aspect for logging execution for service, controller, and repository Spring components.
Quick Start
So before starting a complete step-by-step example, here is a snippet of LoggingAspect source code for quick reference:
- springBeanPointcut() - Pointcut that matches all repositories, services, and Web REST endpoints.
- applicationPackagePointcut() - Pointcut that matches all Spring beans in the application's main packages.
- logAfterThrowing() - Advice that logs methods throw exceptions.
- logAround() - Advice that logs when a method is entered and exited.
package net.guides.springboot2.springboot2jpacrudexample.aspect;
import java.util.Arrays;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Aspect for logging execution of service and repository Spring components.
* @author Ramesh Fadatare
*
*/
@Aspect
@Component
public class LoggingAspect {
private final Logger log = LoggerFactory.getLogger(this.getClass());
/**
* Pointcut that matches all repositories, services and Web REST endpoints.
*/
@Pointcut("within(@org.springframework.stereotype.Repository *)" +
" || within(@org.springframework.stereotype.Service *)" +
" || within(@org.springframework.web.bind.annotation.RestController *)")
public void springBeanPointcut() {
// Method is empty as this is just a Pointcut, the implementations are in the advices.
}
/**
* Pointcut that matches all Spring beans in the application's main packages.
*/
@Pointcut("within(net.guides.springboot2.springboot2jpacrudexample..*)" +
" || within(net.guides.springboot2.springboot2jpacrudexample.service..*)" +
" || within(net.guides.springboot2.springboot2jpacrudexample.controller..*)")
public void applicationPackagePointcut() {
// Method is empty as this is just a Pointcut, the implementations are in the advices.
}
/**
* Advice that logs methods throwing exceptions.
*
* @param joinPoint join point for advice
* @param e exception
*/
@AfterThrowing(pointcut = "applicationPackagePointcut() && springBeanPointcut()", throwing = "e")
public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
log.error("Exception in {}.{}() with cause = {}", joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(), e.getCause() != null ? e.getCause() : "NULL");
}
/**
* Advice that logs when a method is entered and exited.
*
* @param joinPoint join point for advice
* @return result
* @throws Throwable throws IllegalArgumentException
*/
@Around("applicationPackagePointcut() && springBeanPointcut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
if (log.isDebugEnabled()) {
log.debug("Enter: {}.{}() with argument[s] = {}", joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(), Arrays.toString(joinPoint.getArgs()));
}
try {
Object result = joinPoint.proceed();
if (log.isDebugEnabled()) {
log.debug("Exit: {}.{}() with result = {}", joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(), result);
}
return result;
} catch (IllegalArgumentException e) {
log.error("Illegal argument: {} in {}.{}()", Arrays.toString(joinPoint.getArgs()),
joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
throw e;
}
}
}
Let's create a complete step-by-step CRUD spring boot application with Spring AOP.
Tools and Technologies Used
- Spring Boot - 3+
- Spring Framework - 6+
- Hibernate - 6+
- Thymeleaf - 3+
- Maven - 3.2+
- IDE - Eclipse or Spring Tool Suite (STS)
- H2 Database
Development Steps
- Creating a Spring Boot Application
- Project Structure
- Maven Dependencies - Pom.xml
- Domain Layer
- The Controller Layer
- The Service Layer
- The Repository Layer
- The Logging Aspect
- Exception Handling
- Running the Application
- Spring Rest Client
- Conclusion
1. Creating a Spring Boot Application
There are many ways to create a Spring Boot application. You can refer to the below articles to create a Spring Boot application.
>> Create Spring Boot Project With Spring Initializer
>> Create Spring Boot Project in Spring Tool Suite [STS]
>> Create Spring Boot Project in Spring Tool Suite [STS]
Refer next step to create the project packaging structure.
3. Maven Dependencies - Pom.xml
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>net.guides.springboot2</groupId>
<artifactId>springboot2-springaop-example</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>springboot2-springaop-example</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.4</version>
<relativePath />
<!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
4. Domain Layer
Let's create a simple Employee JPA entity class with the following code in it:
import jakarta.persistence.*;
@Entity
@Table(name = "employees")
public class Employee {
private long id;
private String firstName;
private String lastName;
private String emailId;
public Employee() {
}
public Employee(String firstName, String lastName, String emailId) {
this.firstName = firstName;
this.lastName = lastName;
this.emailId = emailId;
}
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
@Column(name = "first_name", nullable = false)
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
@Column(name = "last_name", nullable = false)
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
@Column(name = "email_address", nullable = false)
public String getEmailId() {
return emailId;
}
public void setEmailId(String emailId) {
this.emailId = emailId;
}
@Override
public String toString() {
return "Employee [id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + ", emailId=" + emailId +
"]";
}
}
5. The Controller Layer
Let's first create a Spring rest controller class and then we will add the logging aspect to it:
import java.util.List;
import java.util.Map;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@GetMapping("/employees")
public List < Employee > getAllEmployees() {
return employeeService.getAllEmployees();
}
@GetMapping("/employees/{id}")
public ResponseEntity < Employee > getEmployeeById(@PathVariable(value = "id") Long employeeId)
throws ResourceNotFoundException {
Employee employee = employeeService.getEmployeeById(employeeId)
.orElseThrow(() - > new ResourceNotFoundException("Employee not found for this id :: " + employeeId));
return ResponseEntity.ok().body(employee);
}
@PostMapping("/employees")
public Employee createEmployee(@Valid @RequestBody Employee employee) {
return employeeService.createEmployee(employee);
}
@PutMapping("/employees/{id}")
public ResponseEntity < Employee > updateEmployee(@PathVariable(value = "id") Long employeeId,
@Valid @RequestBody Employee employeeDetails) throws ResourceNotFoundException {
Employee updatedEmployee = employeeService.updateEmployee(employeeId, employeeDetails);
return ResponseEntity.ok(updatedEmployee);
}
@DeleteMapping("/employees/{id}")
public Map < String, Boolean > deleteEmployee(@PathVariable(value = "id") Long employeeId)
throws ResourceNotFoundException {
return employeeService.deleteEmployee(employeeId);
}
}
6. The Service Layer
Let's first create EmployeeService class and then we will add the logging aspect to it:
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import net.guides.springboot2.springboot2jpacrudexample.exception.ResourceNotFoundException;
import net.guides.springboot2.springboot2jpacrudexample.model.Employee;
import net.guides.springboot2.springboot2jpacrudexample.repository.EmployeeRepository;
/**
* Employee Service
* @author Ramesh
*
*/
@Service
public class EmployeeService {
@Autowired
private EmployeeRepository employeeRepository;
public List < Employee > getAllEmployees() {
return employeeRepository.findAll();
}
public Optional < Employee > getEmployeeById(Long employeeId)
throws ResourceNotFoundException {
return employeeRepository.findById(employeeId);
}
public Employee createEmployee(Employee employee) {
return employeeRepository.save(employee);
}
public Employee updateEmployee(Long employeeId,
Employee employeeDetails) throws ResourceNotFoundException {
Employee employee = employeeRepository.findById(employeeId)
.orElseThrow(() - > new ResourceNotFoundException("Employee not found for this id :: " + employeeId));
employee.setEmailId(employeeDetails.getEmailId());
employee.setLastName(employeeDetails.getLastName());
employee.setFirstName(employeeDetails.getFirstName());
final Employee updatedEmployee = employeeRepository.save(employee);
return updatedEmployee;
}
public Map < String, Boolean > deleteEmployee(Long employeeId)
throws ResourceNotFoundException {
Employee employee = employeeRepository.findById(employeeId)
.orElseThrow(() - > new ResourceNotFoundException("Employee not found for this id :: " + employeeId));
employeeRepository.delete(employee);
Map < String, Boolean > response = new HashMap < > ();
response.put("deleted", Boolean.TRUE);
return response;
}
}
7. The Repository Layer
Let's first create EmployeeRepository and then we will add the logging aspect to it:
package net.guides.springboot2.springboot2jpacrudexample.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface EmployeeRepository extends JpaRepository < Employee, Long > {
}
8. The Logging Aspect
Now, let's create an Aspect for logging execution for service and repository Spring components. We will create 4 methods and here are the details:
- springBeanPointcut() - Pointcut that matches all repositories, services, and Web REST endpoints.
- applicationPackagePointcut() - Pointcut that matches all Spring beans in the application's main packages.
- logAfterThrowing() - Advice that logs methods throw exceptions.
- logAround() - Advice that logs when a method is entered and exited.
import java.util.Arrays;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
/**
* Aspect for logging execution of service and repository Spring components.
* @author Ramesh Fadatare
*
*/
@Aspect
@Component
public class LoggingAspect {
private final Logger log = LoggerFactory.getLogger(this.getClass());
/**
* Pointcut that matches all repositories, services and Web REST endpoints.
*/
@Pointcut("within(@org.springframework.stereotype.Repository *)" +
" || within(@org.springframework.stereotype.Service *)" +
" || within(@org.springframework.web.bind.annotation.RestController *)")
public void springBeanPointcut() {
// Method is empty as this is just a Pointcut, the implementations are in the advices.
}
/**
* Pointcut that matches all Spring beans in the application's main packages.
*/
@Pointcut("within(net.guides.springboot2.springboot2jpacrudexample..*)" +
" || within(net.guides.springboot2.springboot2jpacrudexample.service..*)" +
" || within(net.guides.springboot2.springboot2jpacrudexample.controller..*)")
public void applicationPackagePointcut() {
// Method is empty as this is just a Pointcut, the implementations are in the advices.
}
/**
* Advice that logs methods throwing exceptions.
*
* @param joinPoint join point for advice
* @param e exception
*/
@AfterThrowing(pointcut = "applicationPackagePointcut() && springBeanPointcut()", throwing = "e")
public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
log.error("Exception in {}.{}() with cause = {}", joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(), e.getCause() != null ? e.getCause() : "NULL");
}
/**
* Advice that logs when a method is entered and exited.
*
* @param joinPoint join point for advice
* @return result
* @throws Throwable throws IllegalArgumentException
*/
@Around("applicationPackagePointcut() && springBeanPointcut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
if (log.isDebugEnabled()) {
log.debug("Enter: {}.{}() with argument[s] = {}", joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(), Arrays.toString(joinPoint.getArgs()));
}
try {
Object result = joinPoint.proceed();
if (log.isDebugEnabled()) {
log.debug("Exit: {}.{}() with result = {}", joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(), result);
}
return result;
} catch (IllegalArgumentException e) {
log.error("Illegal argument: {} in {}.{}()", Arrays.toString(joinPoint.getArgs()),
joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
throw e;
}
}
}
Database and Logging Configuration
We are using the H2 database for a quick start-up and for logging to work, please add the following logging statements in the application.properties file:
logging.level.org.springframework.web=INFO
logging.level.org.hibernate=ERROR
logging.level.net.guides=DEBUG
9. Exception Handling
Let's create a ResourceNotFoundException.java class:
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends Exception{
private static final long serialVersionUID = 1L;
public ResourceNotFoundException(String message){
super(message);
}
}
Let’s define a simple error response bean:
import java.util.Date;
public class ErrorDetails {
private Date timestamp;
private String message;
private String details;
public ErrorDetails(Date timestamp, String message, String details) {
super();
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
public Date getTimestamp() {
return timestamp;
}
public String getMessage() {
return message;
}
public String getDetails() {
return details;
}
}
To use ErrorDetails to return the error response, let’s create a GlobalExceptionHandler class annotated with @ControllerAdvice annotation. This class handles exception-specific and global exceptions in a single place.
import java.util.Date;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<?> resourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<?> globleExcpetionHandler(Exception ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
10. Running the Application
Finally, let’s define the application’s entry point. Like most Spring Boot applications, we can do this with a plain old main() method:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class Application {
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) {
return builder.build();
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
11. Spring Rest Client
Once the above spring application is up and running, let's test all Spring rest APIs and logging results with the below Spring rest client:
package net.guides.springboot2.springboot2jpacrudexample;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;
import net.guides.springboot2.springboot2jpacrudexample.model.Employee;
public class SpringRestClient {
private static final String GET_EMPLOYEES_ENDPOINT_URL = "http://localhost:8080/api/v1/employees";
private static final String GET_EMPLOYEE_ENDPOINT_URL = "http://localhost:8080/api/v1/employees/{id}";
private static final String CREATE_EMPLOYEE_ENDPOINT_URL = "http://localhost:8080/api/v1/employees";
private static final String UPDATE_EMPLOYEE_ENDPOINT_URL = "http://localhost:8080/api/v1/employees/{id}";
private static final String DELETE_EMPLOYEE_ENDPOINT_URL = "http://localhost:8080/api/v1/employees/{id}";
private static RestTemplate restTemplate = new RestTemplate();
public static void main(String[] args) {
SpringRestClient springRestClient = new SpringRestClient();
// Step1: first create a new employee
springRestClient.createEmployee();
// Step 2: get new created employee from step1
springRestClient.getEmployeeById();
// Step3: get all employees
springRestClient.getEmployees();
// Step4: Update employee with id = 1
springRestClient.updateEmployee();
// Step5: Delete employee with id = 1
springRestClient.deleteEmployee();
}
private void getEmployees() {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
HttpEntity < String > entity = new HttpEntity < String > ("parameters", headers);
ResponseEntity < String > result = restTemplate.exchange(GET_EMPLOYEES_ENDPOINT_URL, HttpMethod.GET, entity,
String.class);
System.out.println(result);
}
private void getEmployeeById() {
Map < String, String > params = new HashMap < String, String > ();
params.put("id", "1");
RestTemplate restTemplate = new RestTemplate();
Employee result = restTemplate.getForObject(GET_EMPLOYEE_ENDPOINT_URL, Employee.class, params);
System.out.println(result);
}
private void createEmployee() {
Employee newEmployee = new Employee("admin", "admin", "admin@gmail.com");
RestTemplate restTemplate = new RestTemplate();
Employee result = restTemplate.postForObject(CREATE_EMPLOYEE_ENDPOINT_URL, newEmployee, Employee.class);
System.out.println(result);
}
private void updateEmployee() {
Map < String, String > params = new HashMap < String, String > ();
params.put("id", "1");
Employee updatedEmployee = new Employee("admin123", "admin123", "admin123@gmail.com");
RestTemplate restTemplate = new RestTemplate();
restTemplate.put(UPDATE_EMPLOYEE_ENDPOINT_URL, updatedEmployee, params);
}
private void deleteEmployee() {
Map < String, String > params = new HashMap < String, String > ();
params.put("id", "1");
RestTemplate restTemplate = new RestTemplate();
restTemplate.delete(DELETE_EMPLOYEE_ENDPOINT_URL, params);
}
}
Output:
The logging statements are printed in the console, the above screenshot is for your reference.
12. Conclusion
We have learned how to use Spring AOP in the Spring boot application. We also looked into the implementation of an Aspect for logging execution for service, controller, and repository Spring components using Spring AOP.
The source code of this tutorial is hosted on my GitHub repository.
Nice Explanation Sir. Sir i need to log all the data in one Log file using AOP. If you dont mind please make an article on that. Why because in production we need to generate log files externally to stop increasing catalina.out file size in Tomcat server.
ReplyDeleteNice write up, only the repositories are NOT logged. Care to fix it?
ReplyDeleteExcelent
ReplyDeleteExcellent work by content provider. Really awesome and need to many appreciation.
ReplyDeleteI dont see any calls to repository being logged in your screenshot and it is not working on my side either.
ReplyDelete