第十九章:Spring AOP实战:高效日志切面开发

牛子 | 2026-02-01 00:56:53 | 75 | 0 | 0 | 0

AOP 是 面向切面编程(Aspect-Oriented Programming)的缩写,是一种编程范式,旨在将横切关注点(cross-cutting concerns)从主业务逻辑中分离出来,以提高代码的模块化程度。

1. 背景

在传统的面向对象编程(OOP)中,某些功能(如日志记录、事务管理、安全控制、性能监控等)会散布在多个类或方法中,导致代码重复、耦合度高、难以维护。这些功能被称为“横切关注点”,因为它们“横切”了多个模块。

AOP 的目标就是把这些横切关注点封装到独立的模块(称为“切面”,Aspect)中,从而让核心业务逻辑更清晰、简洁。

2. AOP 的核心概念

切面(Aspect):横切关注点的模块化,比如日志切面、事务切面。
连接点(Join Point):程序执行过程中的某个特定点,如方法调用、异常抛出等。在 Spring AOP 中,通常指方法的执行。
通知(Advice):在连接点上执行的动作。常见的类型有:
前置通知(Before):在方法执行前运行。
后置通知(After):在方法执行后运行(无论是否异常)。
返回通知(After Returning):方法成功返回后执行。
异常通知(After Throwing):方法抛出异常时执行。
环绕通知(Around):包围方法调用,可控制是否执行原方法。
切入点(Pointcut):定义哪些连接点会被通知拦截,通常通过表达式(如 execution(* com.example.service..(..)))来匹配方法。
织入(Weaving):将切面应用到目标对象并创建代理对象的过程。可在编译期、类加载期或运行期完成。
以上就是关于aop的知识点,咱们废话少说直接上代码:

3.aop切面类

一个 基于 Spring AOP 的 API 调用日志记录切面(Aspect),用于在 Spring Boot 应用中自动记录带有特定注解(@LogApi)的控制器方法的调用详情(如请求参数、响应结果、耗时、IP 等),并将日志异步保存到数据库。

package org.example.aop;
 
 
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.example.entity.ApiCallLog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
 
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
 
@Aspect
@Component
public class ApiLogAspect {
 
    private static final Logger log = LoggerFactory.getLogger(ApiLogAspect.class);
 
    @Autowired
    private ObjectMapper objectMapper; // 用于 JSON 序列化
 
    @Autowired
    private ApiCallLogRepository apiCallLogRepository;
 
    // 异步保存日志(关键!避免阻塞主流程)
    @Async
    public void saveLog(ApiCallLog logEntry) {
        try {
            apiCallLogRepository.save(logEntry);
        } catch (Exception e) {
            log.error("Failed to save API call log", e);
        }
    }
 
    @Around("@annotation(org.example.annotation.LogApi)")
    public Object logApiCall(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        HttpServletRequest request = getCurrentRequest();
        ApiCallLog logEntry = new ApiCallLog();
 
        try {
            // 1. 基础信息
            logEntry.setApiPath(request.getRequestURI());
            logEntry.setMethod(request.getMethod());
            logEntry.setClientIp(getClientIp(request));
            logEntry.setUserAgent(request.getHeader("User-Agent"));
 
            // 2. 渠道编码(假设从 Header 获取)
            logEntry.setChannelCode(request.getHeader("X-Channel-Code"));
 
            // 3. 请求参数(合并 Query + Body)
            Map<String, Object> allParams = new HashMap<>();
            // Query 参数
            request.getParameterMap().forEach((key, value) -> 
                allParams.put(key, value.length == 1 ? value[0] : value)
            );
            // Body 参数(如果是 POST/PUT)
            Object[] args = joinPoint.getArgs();
            if (args.length > 0 && args[0] != null) {
                allParams.put("body", args[0]);
            }
            logEntry.setRequestParams(serialize(allParams));
 
            // 4. 执行目标方法
            Object result = joinPoint.proceed();
 
            // 5. 响应结果 & 状态码(简化:成功默认 200)
            logEntry.setResponseResult(serialize(result));
            logEntry.setStatusCode(200);
 
            return result;
 
        } catch (Exception e) {
            // 记录异常响应
            logEntry.setResponseResult(serialize(Map.of("error", e.getMessage())));
            logEntry.setStatusCode(500);
            throw e; // 重新抛出,不影响原有异常流程
        } finally {
            long duration = System.currentTimeMillis() - startTime;
            logEntry.setDurationMs(duration);
            logEntry.setCreateTime(LocalDateTime.now());
 
            // 异步保存
            saveLog(logEntry);
        }
    }
 
    private HttpServletRequest getCurrentRequest() {
        ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
        return attrs.getRequest();
    }
 
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
 
    private String serialize(Object obj) {
        try {
            return objectMapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            log.warn("Failed to serialize object for logging", e);
            return obj != null ? obj.toString() : "null";
        }
    }
}

4. Spring Data JPA 的 Repository 接口

package org.example.aop;
 
 
import org.example.entity.ApiCallLog;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
 
@Repository
public interface ApiCallLogRepository extends JpaRepository<ApiCallLog, Long> {
 
}

5.自定义java注解

作用是**标记哪些方法需要被记录 API 调用日志**,通常与 AOP(面向切面编程)配合使用。

package org.example.annotation;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogApi {
 
}

6.JPA 实体类

package org.example.entity;
 
import jakarta.persistence.*;
import lombok.Data;
 
import java.time.LocalDateTime;
 
@Entity
@Table(name = "api_call_log")
@Data
public class ApiCallLog {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
 
    private String traceId;
    private String channelCode;
    private String apiPath;
    private String method;
    @Lob
    private String requestParams;
    @Lob
    private String responseResult;
    private Integer statusCode;
    private String clientIp;
    private String userAgent;
    private Long durationMs;
 
    @Column(name = "create_time")
    private LocalDateTime createTime = LocalDateTime.now();
}

7.加入相关依赖

<!-- JPA + 数据库驱动(以 MySQL 为例)-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <scope>runtime</scope>
    </dependency>
 
    <!-- AOP -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

8.启动类添加注解

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
 
@SpringBootApplication
@EnableAsync               // 启用 @Async 异步支持
@EnableJpaAuditing         // (可选)如果你后续想用 @CreatedDate
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

9.注解使用

 // ✅ 添加 @LogApi 注解 → 这个接口会被自动记录日志
    @PostMapping("/hello")
    @LogApi
    public String sayHello(@RequestBody HelloRequest request) {
        return "Hello, " + request.getName();
    }

以上就是提供的aop自定义的注解,希望可以帮助到大家,如有用,请拿出宝贵的双手点个赞!