在这个教程中,我们将一步一步的教大家使用Spring AOP实现一个记录service、controller、repository日志的Aspect。
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 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.7.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.whu</groupId>
<artifactId>aop</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>aop</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<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-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</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>
Domain层
创建一个简单的Employee实体类:
package com.whu.aop.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
(name = "employees")
public class Employee {
private long id;
private String firstName;
private String lastName;
private String emailId;
public Employee() {
}
public Employee(long id, String firstName, String lastName, String emailId) {
super();
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.emailId = emailId;
}
(strategy = GenerationType.AUTO)
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
(name = "first_name", nullable = false)
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
(name = "last_name", nullable = false)
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
(name = "email_address", nullable = false)
public String getEmailId() {
return emailId;
}
public void setEmailId(String emailId) {
this.emailId = emailId;
}
public String toString() {
return "Employee [id=" + id + ", firstName=" + firstName + ", lastName=" + lastName + ", emailId=" + emailId +
"]";
}
}
Repository层
package com.whu.aop.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.whu.aop.model.Employee;
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
}
Service层
package com.whu.aop.service;
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 com.whu.aop.exception.ResourceNotFoundException;
import com.whu.aop.model.Employee;
import com.whu.aop.repository.EmployeeRepository;
public class EmployeeService {
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;
}
}
Controller层
package com.whu.aop.controller;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
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;
import com.whu.aop.exception.ResourceNotFoundException;
import com.whu.aop.model.Employee;
import com.whu.aop.service.EmployeeService;
("/api/v1")
public class EmployeeController {
private EmployeeService employeeService;
("/employees")
public List<Employee> getAllEmployees() {
return employeeService.getAllEmployees();
}
("/employees/{id}")
public ResponseEntity<Employee> getEmployeeById( (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);
}
("/employees")
public Employee createEmployee( Employee employee) {
return employeeService.createEmployee(employee);
}
("/employees/{id}")
public ResponseEntity < Employee > updateEmployee( (value = "id") Long employeeId,
Employee employeeDetails) throws ResourceNotFoundException {
Employee updatedEmployee = employeeService.updateEmployee(employeeId, employeeDetails);
return ResponseEntity.ok(updatedEmployee);
}
("/employees/{id}")
public Map<String, Boolean > deleteEmployee( (value = "id") Long employeeId)
throws ResourceNotFoundException {
return employeeService.deleteEmployee(employeeId);
}
}
至此,一个简单的web应该已经建好,可以通过http://localhost:8080//api/v1/employees 访问第一个接口,请求获取所有employees。
创建Logging Aspect
现在,让我们创建一个Aspect来记录service和repository组件的执行情况。我们将创建4个方法,以下是详细内容:
- springBeanPointcut()--匹配所有repository、service和Web REST端点的pointcut。
- applicationPackagePointcut()--用于匹配应用程序主包中的所有Spring Bean的pointcut。
- logAfterThrowing()--记录抛出异常的方法的advice。
- logAround()--记录方法进入和退出时的advice。
package com.whu.aop.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;
public class LoggingAspect {
private final Logger log = LoggerFactory.getLogger(this.getClass());
("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.
*/
(value = "within(com.whu.aop..*)" +
" || within(com.whu.aop.service..*)" +
" || within(com.whu.aop.controller..*)")
public void applicationPackagePointcut() {
}
/**
* Advice that logs method throwing exceptions.
* @param joinpoint
* @param e
*/
(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");
}
("applicationPackagePointcut() && springBeanPointcut()")
public Object logAround(ProceedingJoinPoint joinpoint) throws Throwable {
if(log.isDebugEnabled()) {
log.debug("Enter: {}.{}() with arguments[s] = {}",
joinpoint.getSignature().getDeclaringType(),
joinpoint.getSignature().getName(),
Arrays.toString(joinpoint.getArgs()));
}
try {
Object result = joinpoint.proceed();
if(log.isDebugEnabled()) {
log.debug("Exit: {}.{}() with result = {}",
joinpoint.getSignature().getDeclaringType(),
joinpoint.getSignature().getName(),
result);
}
return result;
}catch (IllegalArgumentException e) {
log.error("Illegal argument: {} in {}.{}()",
Arrays.toString(joinpoint.getArgs()),
joinpoint.getSignature().getDeclaringType(),
joinpoint.getSignature().getName());
throw e;
}
}
}
application.properties
logging.level.org.springframework.web=INFO
logging.level.org.hibernate=ERROR
logging.level.com.whu=DEBUG
Exception Handling
我们可以用@ResponseStatus注解来指定特定异常的响应状态,以及异常的定义。
package com.whu.aop.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends Exception {
private static final long serialVersionUID = 1L;
public ResourceNotFoundException(String message){
super(message);
}
}
自定义Error Response结构
Spring Boot提供的默认错误响应,通常包含所有需要的detail。但是,你可以创建一个独立于框架的响应结构。在这种情况下,你可以定义一个特定的错误响应结构。让我们来定义一个简单的错误响应Bean。
package com.whu.aop.exception;
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;
}
}
package com.whu.aop.exception;
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;
public class GlobalExceptionHandler {
(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);
}
(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);
}
}
日志输出
访问http://localhost:8080/api/v1/employees/。
2022-06-04 20:35:22.702 DEBUG 34484 --- [nio-8080-exec-4] com.whu.aop.aspect.LoggingAspect : Enter: class com.whu.aop.controller.EmployeeController.getAllEmployees() with arguments[s] = []
2022-06-04 20:35:22.702 DEBUG 34484 --- [nio-8080-exec-4] com.whu.aop.aspect.LoggingAspect : Enter: class com.whu.aop.service.EmployeeService.getAllEmployees() with arguments[s] = []
2022-06-04 20:35:22.704 DEBUG 34484 --- [nio-8080-exec-4] com.whu.aop.aspect.LoggingAspect : Exit: class com.whu.aop.service.EmployeeService.getAllEmployees() with result = []
2022-06-04 20:35:22.704 DEBUG 34484 --- [nio-8080-exec-4] com.whu.aop.aspect.LoggingAspect : Exit: class com.whu.aop.controller.EmployeeController.getAllEmployees() with result = []
访问http://localhost:8080/api/v1/employees/1。
2022-06-04 20:36:19.902 DEBUG 34484 --- [nio-8080-exec-6] com.whu.aop.aspect.LoggingAspect : Enter: class com.whu.aop.controller.EmployeeController.getEmployeeById() with arguments[s] = [1]
2022-06-04 20:36:19.903 DEBUG 34484 --- [nio-8080-exec-6] com.whu.aop.aspect.LoggingAspect : Enter: class com.whu.aop.service.EmployeeService.getEmployeeById() with arguments[s] = [1]
2022-06-04 20:36:19.907 DEBUG 34484 --- [nio-8080-exec-6] com.whu.aop.aspect.LoggingAspect : Exit: class com.whu.aop.service.EmployeeService.getEmployeeById() with result = Optional.empty
2022-06-04 20:36:19.910 ERROR 34484 --- [nio-8080-exec-6] com.whu.aop.aspect.LoggingAspect : Exception in com.whu.aop.controller.EmployeeController.getEmployeeById() with cause = NULL
2022-06-04 20:36:19.918 WARN 34484 --- [nio-8080-exec-6] .m.m.a.ExceptionHandlerExceptionResolver : Resolved [com.whu.aop.exception.ResourceNotFoundException: Employee not found for this id :: 1]