Spring框架之Spring AOP Logging教程

語言: CN / TW / HK

在這個教程中,我們將一步一步的教大家使用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 http://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;
@Entity
@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;
    }
    @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 +
            "]";
    }
}

Repository層

package com.whu.aop.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.whu.aop.model.Employee;
@Repository
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;
@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;
    }
}

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;
@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(@Validated @RequestBody Employee employee) {
        return employeeService.createEmployee(employee);
    }
    @PutMapping("/employees/{id}")
    public ResponseEntity < Employee > updateEmployee(@PathVariable(value = "id") Long employeeId,
        @Validated @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);
    }
}

至此,一個簡單的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;
@Aspect
@Component
public class LoggingAspect {  
    private final Logger log = LoggerFactory.getLogger(this.getClass());
    @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(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
     */
    @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");
    }
    
    @Around("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;
@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;
@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);
    }
}

日誌輸出

訪問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]