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();
}