Java framework-aoplog
ins-framework-aoplog 使用指南
目录
- 一、概述
- 二、架构设计
- 三、原理分析
- 四、项目集成
- 五、使用方法
- 六、项目实践
- 七、最佳实践
- 八、AopLogConfig 使用机制详解
- 九、日志级别区分机制
- 十、常见问题
- 十一、总结
- 十二、AspectJ Pointcut 表达式语法详解
一、概述
ins-framework-aoplog 是一个基于 AOP(面向切面编程)的日志记录框架,通过注解和切面技术自动记录方法执行信息,包括方法参数、返回值、执行时间、异常信息等。该框架提供了统一的日志格式,支持分布式链路追踪,是构建可观测性系统的重要组件。
1.1 核心特性
- 自动日志记录:通过
@LogAnnotation注解自动记录方法执行信息 - 统一日志格式:标准化的日志输出格式,便于日志分析和监控
- 链路追踪:支持业务ID、方法ID、全局追踪ID等,实现完整的调用链追踪
- 参数和返回值记录:自动记录方法参数和返回值(包括复杂对象)
- 异常处理:自动捕获和记录异常信息,包括完整堆栈
- Web 请求追踪:通过 Filter 自动记录 HTTP 请求信息
- 线程安全:使用 ThreadLocal 保证多线程环境下的数据隔离
1.2 技术架构
┌─────────────────────────────────────────────────────────────┐
│ 应用层 (Application) │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Controller │ │ Service │ │ DAO │ │
│ │ @LogAnno │ │ @LogAnno │ │ (自动拦截) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
└─────────┼──────────────────┼──────────────────┼─────────────┘
│ │ │
└──────────────────┼──────────────────┘
│
┌──────────────────▼──────────────────┐
│ AOP 切面层 (Aspect Layer) │
│ ┌──────────────────────────────┐ │
│ │ AopLogConfig (自定义) │ │
│ │ - @Around │ │
│ │ - @Before │ │
│ │ - @After │ │
│ │ - @AfterThrowing │ │
│ └──────────┬───────────────────┘ │
│ │ │
│ ┌──────────▼───────────────────┐ │
│ │ LogAopFormat (框架核心) │ │
│ │ - beforeMethod() │ │
│ │ - afterMethod() │ │
│ │ - afterThrowing() │ │
│ │ - logPrint() │ │
│ └──────────┬───────────────────┘ │
└─────────────┼────────────────────────┘
│
┌─────────────▼─────────────┐
│ Web 过滤器层 (Filter) │
│ ┌─────────────────────┐ │
│ │ RequestFilter │ │
│ │ WebAopFilter │ │
│ └─────────────────────┘ │
└─────────────┬───────────────┘
│
┌─────────────▼─────────────┐
│ 日志输出层 (Logger) │
│ ┌─────────────────────┐ │
│ │ Logback/SLF4J │ │
│ │ monitor-aoplog.log│ │
│ └─────────────────────┘ │
└────────────────────────────┘
二、架构设计
2.1 核心组件
2.1.1 LogAopFormat(日志格式化核心类)
位置:ins.framework.aop.log.LogAopFormat
职责:
- 方法执行前的准备工作(记录开始时间、生成方法ID等)
- 方法执行后的日志格式化输出
- 异常处理
- 线程数据管理
关键代码分析:
public class LogAopFormat {
// 使用静态 Map 存储线程数据,key 为 "线程ID|方法序号"
private static Map<String, String> threadData = new HashMap<>();
private static Map<String, Integer> threadNum = new HashMap<>();
/**
* 方法执行前
* 1. 设置 MDC(Mapped Diagnostic Context)用于日志上下文传递
* 2. 记录开始时间和日期
* 3. 生成方法ID(UUID)
* 4. 管理线程计数器(支持嵌套方法调用)
*/
public void beforeMethod(JoinPoint jointPoint) {
// 设置 IP 和域名到 MDC
if (MDC.get(AopLogConfig.IPFLAG) == null || "".equals(MDC.get(AopLogConfig.IPFLAG))) {
MDC.put(AopLogConfig.IPFLAG, AopLogConfig.getInstance().getIp() + "-" + AopLogConfig.DOMAIN);
}
beforeThreadNum(); // 增加线程计数器
startDate = sdf.format(date);
startTime = System.currentTimeMillis();
}
/**
* 方法执行后
* 1. 计算执行时间
* 2. 格式化并输出日志
* 3. 清理线程数据
*/
public void afterMethod(JoinPoint jointPoint) {
endTime = System.currentTimeMillis();
times = endTime - startTime;
logPrint(jointPoint, " [" + times + "] [" + startDate + "] [" + AopThreadLocal.getClientid() + "]", "info");
afterThreadNum(); // 减少线程计数器
MDC.remove(AopLogConfig.IPFLAG);
}
/**
* 日志输出格式
* [业务ID] [方法ID] [用户] [类名.方法名] [描述] [执行时间] [开始时间] [客户端ID]
*/
private static void logPrint(JoinPoint jointPoint, String message, String lever) {
if ("info".equals(lever)) {
LOG.info("[{}] [{}] [{}] [{}.{}] [}]{}",
getServiceId(), // 业务ID(同一请求链共享)
getMethodId(), // 方法ID(每个方法唯一)
getLoginUser(), // 登录用户
getClassName(jointPoint), // 类名
getMethodName(jointPoint), // 方法名
getDescription(jointPoint), // 描述(来自 @LogAnnotation)
message); // 执行时间等信息
}
}
}
设计要点:
- 线程计数器机制:通过
threadNumMap 管理嵌套方法调用,确保方法ID的正确分配和回收 - 业务ID共享:同一请求链中的所有方法共享同一个业务ID(ServiceID),便于追踪完整调用链
- MDC 使用:利用 SLF4J 的 MDC(Mapped Diagnostic Context)传递上下文信息(如IP、域名)
2.1.2 AopThreadLocal(线程本地存储)
位置:ins.framework.aop.constant.AopThreadLocal
职责:
- 存储线程级别的上下文信息
- 保证多线程环境下的数据隔离
- 支持分布式链路追踪
关键代码分析:
public final class AopThreadLocal {
// 请求模型(用户信息、IP、URI等)
private static ThreadLocal<ReqModel> threadLocalRequest = new ThreadLocal<>();
// 业务ID(同一请求链共享)
private static ThreadLocal<String> aopServiceid = new ThreadLocal<>();
// 客户端ID
private static ThreadLocal<String> clientid = new ThreadLocal<>();
// 分布式追踪ID
private static ThreadLocal<String> globalTraceId = new ThreadLocal<>(); // 全局追踪ID
private static ThreadLocal<String> parentTraceId = new ThreadLocal<>(); // 父追踪ID
private static ThreadLocal<String> localTraceId = new ThreadLocal<>(); // 本地追踪ID
}
设计要点:
- ThreadLocal 隔离:每个线程拥有独立的数据副本,避免线程间数据污染
- 链路追踪支持:通过 GlobalTraceId、ParentTraceId、LocalTraceId 实现分布式系统的调用链追踪
- 请求上下文传递:ReqModel 存储用户信息、IP、URI 等请求相关信息
2.1.3 RequestFilter(请求过滤器)
位置:ins.framework.aop.log.tool.RequestFilter
职责:
- 拦截所有 HTTP 请求
- 提取请求信息(IP、用户、URI等)
- 设置追踪ID
- 初始化线程本地变量
关键代码分析:
public class RequestFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
HttpServletRequest request = (HttpServletRequest) req;
// 1. 设置服务器IP和端口
if (AopLogConfig.getInstance().getPort() == 0) {
AopLogConfig.getInstance().setPort(request.getLocalPort());
}
AopLogConfig.getInstance().setIp(getIpAddr(request));
// 2. 设置分布式追踪ID
setTraceId(request);
// 3. 设置请求上下文到 ThreadLocal
AopThreadLocal.setThreadLocalRequest(new ReqModel(
request.getSession().getAttribute(AopLogConfig.USERNAME) == null ? ""
: request.getSession().getAttribute(AopLogConfig.USERNAME).toString(),
req.getRemoteAddr(),
request.getRequestURI()
));
chain.doFilter(req, res);
}
/**
* 设置追踪ID
* - 如果请求头中有 GlobalTraceId,说明是分布式调用,使用该ID
* - 如果没有,说明是入口请求,生成新的 GlobalTraceId
*/
private void setTraceId(HttpServletRequest request) {
String pGlobalTraceId = request.getHeader(AopLogConfig.GLOBAL_TRACEID);
if (pGlobalTraceId == null || "".equals(pGlobalTraceId.trim())) {
// 入口请求,生成新的追踪ID
pGlobalTraceId = UUID.randomUUID().toString();
AopThreadLocal.setParentTraceId("0");
AopThreadLocal.setLocalTraceId(pGlobalTraceId);
} else {
// 分布式调用,使用传递的追踪ID
AopThreadLocal.setParentTraceId(request.getHeader(AopLogConfig.PARENT_TRACEID));
AopThreadLocal.setLocalTraceId(UUID.randomUUID().toString());
}
AopThreadLocal.setGlobalTraceId(pGlobalTraceId);
}
}
设计要点:
- IP 提取策略:优先从
x-forwarded-for、Proxy-Client-IP等请求头获取真实IP,支持代理和负载均衡场景 - 分布式追踪:通过 HTTP 请求头传递追踪ID,实现跨服务的调用链追踪
- 请求上下文初始化:在请求进入时初始化 ThreadLocal,确保后续 AOP 切面可以获取到请求信息
2.1.4 LogAnnotation(日志注解)
位置:ins.framework.aop.log.util.LogAnnotation
职责:
- 标记需要记录日志的方法
- 提供方法的中文名称和描述信息
关键代码分析:
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
@Target({ ElementType.METHOD }) // 只能用于方法
public @interface LogAnnotation {
/**
* @return 方法中文名
*/
String name() default "";
/**
* @return 方法描述
*/
String description() default "";
}
使用示例:
@LogAnnotation(name = "新增用户", description = "创建新用户,需要提供所有必填字段")
public ApiResponse<Integer> add(@RequestBody SysUserVo userVo) {
// 方法实现
}
2.2 数据流转过程
HTTP 请求
│
▼
RequestFilter (设置 ThreadLocal、追踪ID)
│
▼
Controller 方法 (@LogAnnotation)
│
▼
AopLogConfig.aroundMethod() (自定义切面)
│ ├─ 记录参数
│ ├─ LogAopFormat.beforeMethod() (生成方法ID、记录开始时间)
│ ├─ 执行方法
│ ├─ 记录返回值
│ └─ LogAopFormat.afterMethod() (计算执行时间、输出日志)
│
▼
Service 方法 (@LogAnnotation)
│
▼
AopLogConfig.aroundMethod() (同上)
│
▼
DAO 方法 (自动拦截)
│
▼
AopLogConfig.aroundDaoMethod() (DAO 切面)
│
▼
LogAopFormat.logPrint() → monitor-aoplog.log
三、原理分析
3.1 AOP 切面原理
AOP(Aspect-Oriented Programming)通过代理模式实现横切关注点的分离。ins-framework-aoplog 使用 Spring AOP 和 AspectJ 实现方法拦截。
3.1.1 代理机制
Spring AOP 使用两种代理方式:
- JDK 动态代理:基于接口的代理,要求目标类实现接口
- CGLIB 代理:基于继承的代理,通过生成目标类的子类实现代理
对于 Controller 和 Service,通常使用 JDK 动态代理;对于没有接口的类,使用 CGLIB 代理。
3.1.2 切点表达式
// 拦截所有带有 @LogAnnotation 注解的方法
@Pointcut("@annotation(ins.framework.aop.log.util.LogAnnotation)")
public void logAnnotationPointcut() {}
// 拦截 DAO 层所有方法
@Pointcut("execution(* pdfc.monitor.domain.sys.dao.*.*(..))")
public void daoMethodPointcut() {}
// 拦截应用层所有方法(排除 DAO 层)
@Pointcut("execution(* pdfc.monitor..*.*(..)) && !execution(* pdfc.monitor.domain.sys.dao.*.*(..))")
public void applicationMethodPointcut() {}
切点表达式说明:
@annotation(...):匹配带有指定注解的方法execution(...):匹配方法执行*:匹配任意返回类型pdfc.monitor..*.*(..):匹配pdfc.monitor包及其子包下的所有类的所有方法(..):匹配任意参数
3.1.3 通知类型
@Around("logAnnotationPointcut()") // 环绕通知:可以控制方法执行
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
// 方法执行前
logAopFormat.beforeMethod(joinPoint);
try {
// 执行目标方法
Object result = joinPoint.proceed();
// 方法执行后
logAopFormat.afterMethod(joinPoint);
return result;
} catch (Throwable ex) {
// 异常处理
logAopFormat.afterThrowing(joinPoint, ex);
throw ex;
}
}
3.2 线程数据管理原理
3.2.1 线程计数器机制
为了支持嵌套方法调用,框架使用线程计数器来管理方法ID:
// 方法执行前:增加计数器
private static void beforeThreadNum() {
String threadId = getThreadId(); // 获取线程ID
if (threadNum.get(threadId) == null) {
threadNum.put(threadId, 1);
} else {
threadNum.put(threadId, threadNum.get(threadId) + 1);
}
// 为当前方法生成UUID并存储
threadData.put(threadId + "|" + threadNum.get(threadId), getUuid());
}
// 方法执行后:减少计数器
private static void afterThreadNum() {
String threadId = getThreadId();
threadData.remove(threadId + "|" + threadNum.get(threadId));
Integer count = threadNum.get(threadId);
if (count != null) {
if (count == 1) {
threadNum.remove(threadId);
} else {
threadNum.put(threadId, count - 1);
}
}
}
示例:
线程A执行:
Controller.add() → 计数器=1, 方法ID=uuid1
Service.insert() → 计数器=2, 方法ID=uuid2
DAO.insert() → 计数器=3, 方法ID=uuid3
DAO.insert() 返回 → 计数器=2
Service.insert() 返回 → 计数器=1
Controller.add() 返回 → 计数器=0
3.2.2 业务ID共享机制
同一请求链中的所有方法共享同一个业务ID(ServiceID),便于追踪完整调用链:
private static String getServiceId() {
String serviceID;
if (AopThreadLocal.getAopServiceid() != null) {
// 如果 ThreadLocal 中已有业务ID,复用
serviceID = AopThreadLocal.getAopServiceid();
} else {
// 否则生成新的业务ID并存储
serviceID = getUuid();
AopThreadLocal.setAopServiceid(serviceID);
}
return serviceID;
}
3.3 日志格式设计
3.3.1 标准日志格式
[时间] [线程] [级别] [IP] [业务ID] [方法ID] [用户] [类名.方法名] [描述] [执行时间] [开始时间] [客户端ID]
示例:
[2025-11-24 15:21:52,248] [main] [INFO] [192.168.43.72:null]
[28692ac0-e859-4221-83ed-99b0ab01aa99] [48122a66-a9e1-468d-ae83-e8d79741eb47]
[] [pdfc.monitor.web.SysUserController.add] [}]创建新用户,需要提供所有必填字段
[79] [2025-11-24 15:21:52,215] [127.0.0.1] [1e749a02-720f-4e25-ba09-f1d3944e104b] [0] [1e749a02-720f-4e25-ba09-f1d3944e104b]
字段说明:
- 业务ID:同一请求链中所有方法共享,用于关联同一业务操作的所有日志
- 方法ID:每个方法调用唯一,用于标识具体的某个方法执行
- 用户:当前登录用户(从 Session 中获取)
- 描述:来自
@LogAnnotation的description属性 - 执行时间:方法执行耗时(毫秒)
- 客户端ID:请求的唯一标识
3.3.2 参数和返回值日志格式
[参数] [类名.方法名] [参数名(类型)=参数值, ...]
[返回值] [类名.方法名] 返回类型: 类型, 返回值: 值
示例:
[参数] [pdfc.monitor.web.SysUserController.add]
[userVo(SysUserVo)=SysUserVo(id=null, name=测试用户-新增, work=测试工程师, age=25, ...)]
[返回值] [pdfc.monitor.web.SysUserController.add]
返回类型: ApiResponse, 返回值: ins.framework.web.ApiResponse@3751acd7
四、项目集成
4.1 Maven 依赖
在 pom.xml 中添加依赖:
<dependencies>
<!-- AOP Log Framework -->
<dependency>
<groupId>ins.framework</groupId>
<artifactId>ins-framework-aoplog</artifactId>
<version>6.2.1-SNAPSHOT</version>
</dependency>
<!-- Spring AOP Support -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
4.2 配置文件
4.2.1 logAop.properties
创建 src/main/resources/logAop.properties:
# 登录用户变量名(从 Session 中获取用户信息的 key)
loginName_Variable=userCode
# UUID 开关(是否启用 UUID 功能)
uuid_on_off=false
4.2.2 logback-spring.xml
配置 Logback,设置 AOP 日志输出:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!-- 获取应用IP -->
<conversionRule conversionWord="ip" converterClass="ins.framework.aop.log.util.LogIP" />
<!-- AOP日志 Appender -->
<appender name="AOPLOG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<File>logs/monitor-aoplog.log</File>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/monitor-aoplog-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>500MB</totalSizeCap>
</rollingPolicy>
<encoder>
<Pattern>[%date] [%thread] [%level] [%ip] %msg%n</Pattern>
</encoder>
</appender>
<!-- AOPLOG Logger -->
<logger name="AOPLOG" level="INFO" additivity="false">
<appender-ref ref="AOPLOG" />
</logger>
<!-- 项目包日志重定向到 AOP 日志 -->
<logger name="pdfc.monitor" level="DEBUG" additivity="false">
<appender-ref ref="AOPLOG"/>
<appender-ref ref="ERROR_LOG"/>
</logger>
</configuration>
4.3 AOP 配置类
创建 AopLogConfig.java:
package pdfc.monitor.config;
import ins.framework.aop.log.LogAopFormat;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
@Aspect
@Configuration
public class AopLogConfig {
private static final Logger LOG = LoggerFactory.getLogger("AOPLOG");
private static final Logger ERROR_LOG = LoggerFactory.getLogger("pdfc.monitor");
private final LogAopFormat logAopFormat = new LogAopFormat();
/**
* 切点:拦截所有带有 @LogAnnotation 注解的方法
*/
@Pointcut("@annotation(ins.framework.aop.log.util.LogAnnotation)")
public void logAnnotationPointcut() {}
/**
* 切点:拦截 DAO 层所有方法
*/
@Pointcut("execution(* pdfc.monitor.domain.sys.dao.*.*(..))")
public void daoMethodPointcut() {}
/**
* 环绕通知:记录参数、返回值、异常
*/
@Around("logAnnotationPointcut()")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
// 记录参数
String params = getMethodParams(joinPoint);
LOG.info("[参数] [{}.{}] {}",
joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(),
params);
logAopFormat.beforeMethod(joinPoint);
try {
Object result = joinPoint.proceed();
// 记录返回值
String returnValue = formatReturnValue(result);
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
LOG.info("[返回值] [{}.{}] 返回类型: {}, 返回值: {}",
joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(),
signature.getReturnType().getSimpleName(),
returnValue);
logAopFormat.afterMethod(joinPoint);
return result;
} catch (Throwable ex) {
// 记录异常
ERROR_LOG.error("方法执行异常", ex);
LOG.error("[异常] [{}.{}] 参数: {}, 异常: {}",
joinPoint.getSignature().getDeclaringTypeName(),
joinPoint.getSignature().getName(),
params,
ex.getMessage());
logAopFormat.afterThrowing(joinPoint, ex);
throw ex;
}
}
// DAO 层方法的环绕通知(类似实现)
@Around("daoMethodPointcut()")
public Object aroundDaoMethod(ProceedingJoinPoint joinPoint) throws Throwable {
// 实现类似
}
// 参数格式化方法
private String getMethodParams(JoinPoint joinPoint) {
// 实现参数格式化
}
}
4.4 Web 过滤器配置
WebAopLogFilterConfig 是 AOP 日志系统的入口配置类,负责注册两个 Servlet Filter,在 HTTP 请求的生命周期中初始化和清理日志上下文信息。
4.4.1 配置类说明
创建 WebAopLogFilterConfig.java:
package pdfc.monitor.config;
import ins.framework.aop.log.tool.RequestFilter;
import ins.framework.aop.log.tool.WebAopFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class WebAopLogFilterConfig {
@Bean
public FilterRegistrationBean<RequestFilter> requestFilter() {
RequestFilter filter = new RequestFilter();
FilterRegistrationBean<RequestFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(filter);
registrationBean.addUrlPatterns("/*");
registrationBean.setName("requestFilter");
registrationBean.setOrder(200);
return registrationBean;
}
@Bean
public FilterRegistrationBean<WebAopFilter> webAopFilter() {
WebAopFilter filter = new WebAopFilter();
FilterRegistrationBean<WebAopFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(filter);
registrationBean.addUrlPatterns("/*");
registrationBean.setName("webAopFilter");
registrationBean.setOrder(300);
return registrationBean;
}
}
4.4.2 RequestFilter(请求过滤器)- Order 200
作用:在请求进入时初始化 AOP 日志上下文
主要工作:
-
提取服务器信息:
- 获取服务器 IP 地址(支持代理和负载均衡场景)
- 获取服务器端口号
-
设置分布式追踪 ID:
- 如果请求头中有
GlobalTraceId,说明是分布式调用,使用该 ID - 如果没有,说明是入口请求,生成新的
GlobalTraceId - 设置
ParentTraceId和LocalTraceId
- 如果请求头中有
-
初始化 ThreadLocal 请求上下文:
- 从 Session 中获取用户信息(
userCode) - 提取客户端 IP 地址
- 获取请求 URI
- 将以上信息存入
AopThreadLocal,供后续 AOP 切面使用
- 从 Session 中获取用户信息(
执行时机:在所有 Controller 方法执行之前
关键代码逻辑:
public class RequestFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
HttpServletRequest request = (HttpServletRequest) req;
// 1. 设置服务器IP和端口
AopLogConfig.getInstance().setIp(getIpAddr(request));
AopLogConfig.getInstance().setPort(request.getLocalPort());
// 2. 设置分布式追踪ID
setTraceId(request);
// 3. 设置请求上下文到 ThreadLocal
AopThreadLocal.setThreadLocalRequest(new ReqModel(
request.getSession().getAttribute(AopLogConfig.USERNAME) == null ? ""
: request.getSession().getAttribute(AopLogConfig.USERNAME).toString(),
req.getRemoteAddr(),
request.getRequestURI()
));
chain.doFilter(req, res);
}
}
4.4.3 WebAopFilter(Web AOP 过滤器)- Order 300
作用:在请求结束时清理 ThreadLocal 数据
主要工作:
-
清理 ThreadLocal:
- 清除
AopThreadLocal中的请求上下文 - 清除追踪 ID 相关的 ThreadLocal 数据
- 清除
-
防止内存泄漏:
- 避免线程池复用导致的数据污染
- 确保每个请求的日志上下文独立
执行时机:在请求处理完成后(Response 返回前)
为什么需要清理:
- Spring Boot 使用线程池处理请求,同一个线程可能被多个请求复用
- 如果不清理 ThreadLocal,下一个请求可能会读取到上一个请求的数据
- 这会导致日志中的用户信息、IP、追踪 ID 等数据混乱
4.4.4 执行流程
HTTP 请求进入
│
▼
RequestFilter (Order 200) - 请求前置处理
├─ 提取服务器 IP、端口
├─ 设置分布式追踪 ID
│ ├─ 从请求头获取或生成 GlobalTraceId
│ ├─ 设置 ParentTraceId
│ └─ 设置 LocalTraceId
└─ 初始化 ThreadLocal
├─ 用户信息(从 Session 获取)
├─ 客户端 IP
└─ 请求 URI
│
▼
Controller 方法执行
├─ AopLogConfig 切面拦截
├─ 从 ThreadLocal 获取请求上下文
├─ 记录方法参数、返回值
└─ 日志中包含:用户、IP、追踪 ID 等信息
│
▼
Service 方法执行
├─ AopLogConfig 切面拦截
└─ 使用相同的 ThreadLocal 上下文
│
▼
DAO 方法执行
├─ AopLogConfig 切面拦截
└─ 使用相同的 ThreadLocal 上下文
│
▼
WebAopFilter (Order 300) - 请求后置处理
└─ 清理 ThreadLocal(防止内存泄漏)
│
▼
HTTP 响应返回
4.4.5 关键配置说明
1. URL 拦截模式:
registrationBean.addUrlPatterns("/*");
/*:拦截所有 URL 路径- 也可以配置特定路径,如:
/api/*、/admin/*等 - 支持多个路径:
registrationBean.addUrlPatterns("/api/*", "/admin/*")
2. 执行顺序(Order):
registrationBean.setOrder(200); // RequestFilter 先执行
registrationBean.setOrder(300); // WebAopFilter 后执行
- Order 值越小,执行越早
- RequestFilter 必须在 WebAopFilter 之前执行
- 建议 Order 值间隔 100,方便后续插入其他 Filter
3. Filter 注册:
@Bean
public FilterRegistrationBean<RequestFilter> requestFilter() {
// ...
}
- 使用
@Bean注解注册为 Spring Bean - Spring 容器管理 Filter 的生命周期
- 可以控制 Filter 的启用/禁用
4.4.6 为什么需要两个过滤器?
RequestFilter(前置):
- 在请求开始时初始化上下文
- 确保后续 AOP 切面能获取到请求信息
- 如果没有它,日志中无法获取用户、IP、追踪 ID 等信息
WebAopFilter(后置):
- 在请求结束时清理 ThreadLocal
- 避免线程池复用导致的数据污染
- 防止内存泄漏
两者配合:
- RequestFilter 负责"初始化"
- WebAopFilter 负责"清理"
- 形成完整的请求生命周期管理
4.4.7 实际效果
当有 HTTP 请求进入时:
-
RequestFilter 执行:
- 从请求头提取或生成追踪 ID:
GlobalTraceId=abc-123-def - 将用户信息存入 ThreadLocal:
userCode=admin - 将 IP 存入 ThreadLocal:
clientIp=192.168.1.100 - 将 URI 存入 ThreadLocal:
uri=/api/sys/user/add
- 从请求头提取或生成追踪 ID:
-
Controller/Service/DAO 方法执行:
- AopLogConfig 切面拦截方法
- 从 ThreadLocal 获取请求上下文
- 记录日志时自动包含:
[用户:admin] [IP:192.168.1.100] [追踪ID:abc-123-def]
-
WebAopFilter 执行:
- 清理 ThreadLocal
- 确保下次请求不会使用到旧数据
4.4.8 注意事项
-
必须配置:没有这两个 Filter,AOP 日志无法获取请求上下文信息(用户、IP、追踪 ID 等)
-
执行顺序很重要:RequestFilter 必须在 WebAopFilter 之前执行(Order 值更小)
-
URL 模式配置:
/*表示拦截所有请求- 可以根据需要配置特定路径
- 静态资源(如 CSS、JS、图片)通常不需要日志,可以排除
-
线程安全:
- ThreadLocal 保证每个线程的数据隔离
- 但必须在请求结束时清理,避免内存泄漏
-
分布式追踪:
- 如果系统是微服务架构,需要在服务间传递追踪 ID
- 通过 HTTP 请求头传递:
GlobalTraceId、ParentTraceId
五、使用方法
5.1 基本使用
5.1.1 Controller 层
@RestController
@RequestMapping("/api/sys/user")
public class SysUserController {
@PostMapping("/add")
@LogAnnotation(name = "新增用户", description = "创建新用户,需要提供所有必填字段")
public ApiResponse<Integer> add(@RequestBody SysUserVo userVo) {
// 方法实现
// 日志会自动记录:
// 1. 方法参数(userVo 的完整内容)
// 2. 方法返回值
// 3. 执行时间
// 4. 异常信息(如果有)
}
}
5.1.2 Service 层
@Service
public class SysUserServiceImpl implements SysUserService {
@Override
@LogAnnotation(name = "新增用户", description = "创建新用户")
public int insert(SysUserVo entity) {
// 方法实现
}
}
5.1.3 DAO 层
DAO 层方法会自动拦截,无需添加注解:
@Mapper
public interface SysUserDao extends MybatisBaseDao<SysUser, Integer> {
// 所有方法都会自动记录日志
int insert(SysUser entity);
SysUser selectByPrimaryKey(Integer id);
}
5.2 高级用法
5.2.1 自定义切点
可以定义自定义切点来拦截特定包或类的方法:
@Aspect
@Configuration
public class AopLogConfig {
// 拦截特定包下的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() {}
// 拦截特定注解的方法
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
public void transactionalPointcut() {}
// 组合切点
@Pointcut("servicePointcut() && transactionalPointcut()")
public void serviceTransactionalPointcut() {}
}
5.2.2 条件拦截
可以根据条件决定是否记录日志:
@Around("logAnnotationPointcut()")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
// 只记录特定类的方法
String className = joinPoint.getSignature().getDeclaringTypeName();
if (className.contains(".config.")) {
return joinPoint.proceed(); // 跳过配置类
}
// 记录日志
// ...
}
5.2.3 参数过滤
可以过滤敏感参数,避免记录密码等敏感信息:
private String getMethodParams(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String[] paramNames = signature.getParameterNames();
StringBuilder params = new StringBuilder("[");
for (int i = 0; i < args.length; i++) {
String paramName = paramNames[i];
// 过滤敏感参数
if ("password".equalsIgnoreCase(paramName) ||
"pwd".equalsIgnoreCase(paramName)) {
params.append(paramName).append("=***");
} else {
params.append(paramName).append("=").append(args[i]);
}
}
params.append("]");
return params.toString();
}
5.3 日志查询和分析
5.3.1 根据业务ID查询完整调用链
# 查询特定业务ID的所有日志
grep "28692ac0-e859-4221-83ed-99b0ab01aa99" logs/monitor-aoplog.log
# 输出示例:
# [业务ID] [方法ID1] [Controller.add]
# [业务ID] [方法ID2] [Service.insert]
# [业务ID] [方法ID3] [DAO.insert]
5.3.2 根据方法名查询
# 查询特定方法的所有调用
grep "SysUserController.add" logs/monitor-aoplog.log
5.3.3 查询异常日志
# 查询所有异常
grep "\[异常\]" logs/monitor-aoplog.log
# 查询错误日志文件
cat logs/monitor-error.log
5.3.4 性能分析
# 查询执行时间超过 1000ms 的方法
grep -E "\[[0-9]{4,}\]" logs/monitor-aoplog.log
六、项目实践
6.1 当前项目配置
6.1.1 项目结构
monitor_java/
├── src/main/java/pdfc/monitor/
│ ├── config/
│ │ ├── AopLogConfig.java # AOP 切面配置
│ │ └── WebAopLogFilterConfig.java # Web 过滤器配置
│ ├── web/
│ │ └── SysUserController.java # Controller(使用 @LogAnnotation)
│ ├── service/
│ │ └── impl/
│ │ └── SysUserServiceImpl.java # Service(使用 @LogAnnotation)
│ └── domain/sys/dao/
│ └── SysUserDao.java # DAO(自动拦截)
└── src/main/resources/
├── logback-spring.xml # Logback 配置
└── logAop.properties # AOP 日志配置
6.1.2 日志文件
- monitor-aoplog.log:所有 AOP 日志(方法执行、参数、返回值等)
- monitor-error.log:错误日志(异常堆栈)
- monitor-sql.log:SQL 执行日志
- monitor.log:应用运行日志(已重定向到 AOP 日志)
6.2 实际日志示例
6.2.1 完整调用链日志
# 1. Controller 层
[2025-11-24 15:21:52,204] [main] [INFO] [192.168.43.72:null]
[参数] [pdfc.monitor.web.SysUserController.add]
[userVo(SysUserVo)=SysUserVo(id=null, name=测试用户-新增, ...)]
[2025-11-24 15:21:52,248] [main] [INFO] [192.168.43.72:null]
[28692ac0-e859-4221-83ed-99b0ab01aa99] [48122a66-a9e1-468d-ae83-e8d79741eb47]
[] [pdfc.monitor.web.SysUserController.add] [}]创建新用户,需要提供所有必填字段
[79] [2025-11-24 15:21:52,215] [127.0.0.1] [1e749a02-720f-4e25-ba09-f1d3944e104b] [0] [1e749a02-720f-4e25-ba09-f1d3944e104b]
[2025-11-24 15:21:52,248] [main] [INFO] [192.168.43.72:null]
[返回值] [pdfc.monitor.web.SysUserController.add]
返回类型: ApiResponse, 返回值: ins.framework.web.ApiResponse@3751acd7
# 2. Service 层
[2025-11-24 15:21:52,217] [main] [INFO] [192.168.43.72:null]
[参数] [pdfc.monitor.service.impl.SysUserServiceImpl.insert]
[entity(SysUserVo)=SysUserVo(id=null, name=测试用户-新增, ...)]
[2025-11-24 15:21:52,248] [main] [INFO] [192.168.43.72:null]
[28692ac0-e859-4221-83ed-99b0ab01aa99] [29a1cfe4-2678-4e7a-b553-5a38e6f01797]
[] [pdfc.monitor.service.impl.SysUserServiceImpl.insert] [}]创建新用户
[31] [2025-11-24 15:21:52,217] [127.0.0.1] [1e749a02-720f-4e25-ba09-f1d3944e104b] [0] [1e749a02-720f-4e25-ba09-f1d3944e104b]
[2025-11-24 15:21:52,248] [main] [INFO] [192.168.43.72:null]
[返回值] [pdfc.monitor.service.impl.SysUserServiceImpl.insert]
返回类型: int, 返回值: 1
# 3. DAO 层
[2025-11-24 15:21:52,223] [main] [INFO] [192.168.43.72:null]
[参数] [ins.framework.mybatis.MybatisBaseDao.insert]
[entity(Object)=SysUser(id=null, name=测试用户-新增, work=测试工程师, age=25, ...)]
[2025-11-24 15:21:52,248] [main] [INFO] [192.168.43.72:null]
[28692ac0-e859-4221-83ed-99b0ab01aa99] [a5c4995a-2628-4332-be7c-f82f7e69a9fd]
[] [ins.framework.mybatis.MybatisBaseDao.insert] [}]NONE
[25] [2025-11-24 15:21:52,223] [127.0.0.1] [1e749a02-720f-4e25-ba09-f1d3944e104b] [0] [1e749a02-720f-4e25-ba09-f1d3944e104b]
[2025-11-24 15:21:52,248] [main] [INFO] [192.168.43.72:null]
[返回值] [ins.framework.mybatis.MybatisBaseDao.insert]
返回类型: int, 返回值: 1
分析:
- 所有方法共享同一个业务ID:
28692ac0-e859-4221-83ed-99b0ab01aa99 - 每个方法有唯一的方法ID
- 可以清晰地看到调用链:Controller → Service → DAO
- 记录了完整的参数和返回值
6.2.2 异常日志示例
# AOP 日志中的异常记录
[2025-11-24 15:21:52,XXX] [main] [ERROR] [192.168.43.72:null]
[异常] [pdfc.monitor.web.SysUserController.add]
参数: [userVo(SysUserVo)=...], 异常: NullPointerException - null pointer
# monitor-error.log 中的详细异常堆栈
2025-11-24 15:21:52.XXX [main] ERROR pdfc.monitor - ========== AOP 方法异常 ==========
2025-11-24 15:21:52.XXX [main] ERROR pdfc.monitor - 方法执行异常 - 类: pdfc.monitor.web.SysUserController, 方法: add, 参数: [...], 异常类型: NullPointerException, 异常消息: null pointer
2025-11-24 15:21:52.XXX [main] ERROR pdfc.monitor - 异常堆栈:
java.lang.NullPointerException
at pdfc.monitor.web.SysUserController.add(SysUserController.java:123)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
...
七、最佳实践
7.1 注解使用规范
-
所有 Controller 方法都应该添加
@LogAnnotation@LogAnnotation(name = "新增用户", description = "创建新用户,需要提供所有必填字段") -
Service 层关键方法添加
@LogAnnotation@LogAnnotation(name = "新增用户", description = "创建新用户") -
DAO 层无需添加注解(自动拦截)
7.2 日志级别控制
- INFO:正常业务流程日志
- ERROR:异常和错误日志
- DEBUG:开发调试日志(生产环境可关闭)
7.3 性能优化
- 避免记录过大的对象:对于大对象,可以只记录关键字段
- 异步日志:使用异步 Appender 提高性能
- 日志级别控制:生产环境适当提高日志级别,减少日志量
7.4 安全考虑
- 敏感信息过滤:密码、token 等敏感信息不应记录到日志
- 日志文件权限:确保日志文件有适当的访问权限
- 日志脱敏:对手机号、身份证号等敏感数据进行脱敏处理
八、AopLogConfig 使用机制详解
8.1 Spring 如何发现和加载 AopLogConfig
8.1.1 注解识别
@Aspect // 标识这是一个 AOP 切面类
@Configuration // 标识这是一个 Spring 配置类
public class AopLogConfig {
// ...
}
@Configuration:Spring 在启动时会扫描并加载该类为配置 Bean@Aspect:Spring AOP 会识别该类为切面,并处理其中的切点和通知
8.1.2 自动扫描机制
@SpringBootApplication // 在 MonitorWeb.java 中
public class MonitorWeb {
public static void main(String[] args){
SpringApplication.run(MonitorWeb.class, args);
}
}
@SpringBootApplication 包含 @ComponentScan,默认扫描主类所在包及其子包:
- 扫描范围:
pdfc.monitor及其子包 AopLogConfig位于pdfc.monitor.config,会被自动扫描到
8.1.3 AOP 自动代理
spring-boot-starter-aop 依赖会自动启用 AOP:
- 自动配置
@EnableAspectJAutoProxy - 创建 AOP 代理工厂
- 为匹配切点的 Bean 创建代理对象
8.2 AOP 切面工作流程
8.2.1 切点定义
// 切点1:拦截所有带有 @LogAnnotation 注解的方法
@Pointcut("@annotation(ins.framework.aop.log.util.LogAnnotation)")
public void logAnnotationPointcut() {}
// 切点2:拦截 DAO 层所有方法
@Pointcut("execution(* pdfc.monitor.domain.sys.dao.*.*(..))")
public void daoMethodPointcut() {}
// 切点3:拦截应用层所有方法
@Pointcut("execution(* pdfc.monitor..*.*(..)) && !execution(* pdfc.monitor.domain.sys.dao.*.*(..))")
public void applicationMethodPointcut() {}
8.2.2 通知绑定
@Around("logAnnotationPointcut()") // 绑定切点1
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
// 环绕通知逻辑
}
@Around("daoMethodPointcut()") // 绑定切点2
public Object aroundDaoMethod(ProceedingJoinPoint joinPoint) throws Throwable {
// DAO 层环绕通知逻辑
}
@Around("applicationMethodPointcut()") // 绑定切点3
public Object aroundApplicationMethod(ProceedingJoinPoint joinPoint) throws Throwable {
// 应用层环绕通知逻辑
}
8.3 实际调用流程示例
以 SysUserController.add() 方法为例:
8.3.1 方法定义
@RestController
@RequestMapping("/api/sys/user")
public class SysUserController {
@PostMapping("/add")
@LogAnnotation(name = "新增用户", description = "创建新用户,需要提供所有必填字段")
public ApiResponse<Integer> add(@RequestBody SysUserVo userVo) {
int result = sysUserService.insert(userVo);
return new ApiResponse<Integer>(result, "新增成功", result);
}
}
8.3.2 执行流程
1. HTTP 请求到达
↓
2. Spring MVC DispatcherServlet 接收请求
↓
3. 路由到 SysUserController.add() 方法
↓
4. Spring AOP 拦截(因为方法有 @LogAnnotation 注解)
↓
5. 匹配切点:logAnnotationPointcut()
↓
6. 执行 AopLogConfig.aroundMethod() 环绕通知
├─ 6.1 记录方法参数
│ LOG.info("[参数] [SysUserController.add] [userVo(...)]")
├─ 6.2 调用 LogAopFormat.beforeMethod()
│ ├─ 设置 MDC(IP、域名)
│ ├─ 生成方法ID(UUID)
│ ├─ 记录开始时间
│ └─ 增加线程计数器
├─ 6.3 执行目标方法:joinPoint.proceed()
│ └─ 实际调用 SysUserController.add()
│ └─ 调用 sysUserService.insert()
│ └─ 调用 sysUserDao.insert()(DAO 层也会被拦截)
├─ 6.4 记录返回值
│ LOG.info("[返回值] [SysUserController.add] 返回类型: ApiResponse, 返回值: ...")
└─ 6.5 调用 LogAopFormat.afterMethod()
├─ 计算执行时间
├─ 输出标准日志格式
└─ 清理线程数据
↓
7. 返回结果给客户端
8.3.3 嵌套调用示例
当 Controller.add() 调用 Service.insert() 时:
Controller.add() 被拦截
├─ AopLogConfig.aroundMethod() 执行
│ ├─ 记录参数
│ ├─ LogAopFormat.beforeMethod() → 生成方法ID1
│ └─ joinPoint.proceed()
│ └─ Service.insert() 被拦截
│ ├─ AopLogConfig.aroundMethod() 执行
│ │ ├─ 记录参数
│ │ ├─ LogAopFormat.beforeMethod() → 生成方法ID2
│ │ └─ joinPoint.proceed()
│ │ └─ DAO.insert() 被拦截
│ │ ├─ AopLogConfig.aroundDaoMethod() 执行
│ │ │ ├─ 记录参数
│ │ │ ├─ LogAopFormat.beforeMethod() → 生成方法ID3
│ │ │ └─ joinPoint.proceed()
│ │ │ └─ 执行实际的 SQL
│ │ │ └─ LogAopFormat.afterMethod() → 清理方法ID3
│ │ └─ 记录返回值
│ │ └─ LogAopFormat.afterMethod() → 清理方法ID2
│ └─ 记录返回值
│ └─ LogAopFormat.afterMethod() → 清理方法ID1
└─ 记录返回值
8.4 切点匹配规则
8.4.1 切点1:@annotation 匹配
@Pointcut("@annotation(ins.framework.aop.log.util.LogAnnotation)")
匹配所有带有 @LogAnnotation 注解的方法:
SysUserController.add()有@LogAnnotation→ 匹配SysUserServiceImpl.insert()有@LogAnnotation→ 匹配SysUserDao.selectPageCount()有@LogAnnotation→ 匹配
8.4.2 切点2:execution 匹配(DAO 层)
@Pointcut("execution(* pdfc.monitor.domain.sys.dao.*.*(..))")
匹配 pdfc.monitor.domain.sys.dao 包下所有类的所有方法:
SysUserDao.insert()→ 匹配SysUserDao.selectByPrimaryKey()→ 匹配SysUserAddressDao.insert()→ 匹配
8.4.3 切点3:execution 匹配(应用层)
@Pointcut("execution(* pdfc.monitor..*.*(..)) && !execution(* pdfc.monitor.domain.sys.dao.*.*(..))")
匹配 pdfc.monitor 包及其子包下的所有方法,但排除 DAO 层:
SysUserController.add()→ 匹配(但也会被切点1匹配,优先执行切点1)SysUserServiceImpl.insert()→ 匹配(但也会被切点1匹配,优先执行切点1)SysUserDao.insert()→ 不匹配(被排除)
8.5 代理对象创建
Spring AOP 为匹配切点的 Bean 创建代理:
8.5.1 Controller 代理
// 原始对象
SysUserController controller = new SysUserController();
// Spring 创建的代理对象(简化示例)
SysUserController proxy = new Proxy() {
@Override
public ApiResponse<Integer> add(SysUserVo userVo) {
// 1. 执行 AOP 切面逻辑
AopLogConfig.aroundMethod(joinPoint);
// 2. 调用原始方法
return controller.add(userVo);
}
};
8.5.2 代理类型
- JDK 动态代理:如果目标类实现了接口(如 Controller 实现接口)
- CGLIB 代理:如果目标类没有接口(如直接使用
@RestController的类)
8.6 关键机制总结
| 机制 | 说明 |
|---|---|
| 自动发现 | @SpringBootApplication 自动扫描 @Configuration 和 @Aspect 类 |
| 代理创建 | Spring AOP 为匹配切点的 Bean 创建代理对象 |
| 切点匹配 | 运行时检查方法是否匹配切点表达式 |
| 通知执行 | 匹配的方法调用会先执行 AOP 通知,再执行原方法 |
| 嵌套调用 | 支持多层嵌套,每层都有独立的方法ID |
| 线程隔离 | 使用 ThreadLocal 保证多线程环境下的数据隔离 |
8.7 使用要点
- 无需手动调用:Spring 自动识别并应用切面
- 透明代理:业务代码无需感知 AOP 的存在
- 性能影响:每次方法调用都会经过代理,但开销很小
- 执行顺序:多个切面时,可通过
@Order控制执行顺序
九、日志级别区分机制
9.1 AOP 日志级别区分原理
9.1.1 LogAopFormat 中的级别控制
在 LogAopFormat.java 中,通过 logPrint() 方法的 lever 参数控制日志级别:
private static void logPrint(JoinPoint jointPoint, String message, String lever) {
if ("info".equals(lever)) {
// 使用 INFO 级别记录正常执行
LOG.info("[{}] [{}] [{}] [{}.{}] [}]{}", ...);
} else if ("error".equals(lever)) {
// 使用 ERROR 级别记录异常
LOG.error("[{}] [{}] [{}] [{}.{}] [{}]{}", ...);
}
}
// 正常执行后调用
public void afterMethod(JoinPoint jointPoint) {
logPrint(jointPoint, " [" + times + "] ...", "info"); // 传入 "info"
}
// 异常时调用
public void afterThrowing(JoinPoint jointPoint, Throwable ex) {
logPrint(jointPoint, " [方法运行异常] ...", "error"); // 传入 "error"
}
9.1.2 AopLogConfig 中的级别使用
在自定义的 AopLogConfig.java 中:
// INFO 级别:记录正常信息
LOG.info("[参数] [{}.{}] {}", className, methodName, params);
LOG.info("[返回值] [{}.{}] 返回类型: {}, 返回值: {}", ...);
// ERROR 级别:记录异常信息
LOG.error("[异常] [{}.{}] 参数: {}, 异常: {} - {}", ...);
ERROR_LOG.error("========== AOP 方法异常 ==========");
ERROR_LOG.error("异常堆栈:", ex);
9.2 AOP 与 Logback 的集成
9.2.1 集成架构
┌─────────────────────────────────────────┐
│ ins-framework-aoplog 框架 │
│ ┌──────────────────────────────────┐ │
│ │ LogAopFormat.java │ │
│ │ LoggerFactory.getLogger("AOPLOG")│ │
│ │ LOG.info() / LOG.error() │ │
│ └──────────────┬───────────────────┘ │
└─────────────────┼───────────────────────┘
│
│ SLF4J API
▼
┌─────────────────────────────────────────┐
│ SLF4J 门面层 │
│ (org.slf4j.Logger, LoggerFactory) │
└─────────────────┬───────────────────────┘
│
│ 桥接
▼
┌─────────────────────────────────────────┐
│ Logback 实现层 │
│ (ch.qos.logback.classic.Logger) │
│ ┌──────────────────────────────────┐ │
│ │ logback-spring.xml 配置 │ │
│ │ - AOPLOG Logger │ │
│ │ - AOPLOG Appender │ │
│ │ - 日志格式、滚动策略等 │ │
│ └──────────────┬───────────────────┘ │
└─────────────────┼───────────────────────┘
│
▼
monitor-aoplog.log 文件
9.2.2 依赖关系
在 ins-framework-aoplog/pom.xml 中:
<dependencies>
<!-- SLF4J API -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<!-- Logback 实现(包含 logback-core 和 logback-classic) -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
</dependencies>
logback-classic包含logback-core和slf4j-api到 logback 的桥接- 因此 AOP 框架通过 SLF4J API 使用 logback 作为日志实现
9.2.3 Logback 配置中的级别显示
在 logback-spring.xml 中:
<encoder>
<Pattern>[%date] [%thread] [%level] [%ip] %msg%n</Pattern>
</encoder>
%level:显示日志级别(INFO、DEBUG、ERROR 等)- 实际输出:
[2025-11-24 15:21:52,204] [main] [INFO] [192.168.43.72:null] ...
9.2.4 Logger 级别控制
<logger name="AOPLOG" level="INFO" additivity="false">
<appender-ref ref="AOPLOG" />
</logger>
level="INFO":只记录 INFO 及以上级别(INFO、WARN、ERROR)- DEBUG 级别的日志不会被记录(除非改为
level="DEBUG")
9.3 日志级别在日志文件中的识别
9.3.1 AOP 日志文件(monitor-aoplog.log)
[2025-11-24 15:21:52,204] [main] [INFO] [192.168.43.72:null] [参数] ...
[2025-11-24 15:21:52,231] [main] [DEBUG] [192.168.43.72:null] Fetching JDBC Connection ...
[2025-11-24 15:21:52,248] [main] [INFO] [192.168.43.72:null] [返回值] ...
识别方式:
- 第三个字段
[INFO]、[DEBUG]、[ERROR]表示日志级别 - 可通过
grep过滤特定级别
9.3.2 错误日志文件(monitor-error.log)
2025-11-24 15:21:52.XXX [main] ERROR pdfc.monitor - ========== AOP 方法异常 ==========
2025-11-24 15:21:52.XXX [main] ERROR pdfc.monitor - 方法执行异常 - 类: ...
2025-11-24 15:21:52.XXX [main] ERROR pdfc.monitor - 异常堆栈:
java.lang.NullPointerException
at ...
识别方式:
- 第三个字段
ERROR表示日志级别 - 该文件只包含 ERROR 级别及以上的日志(通过 ThresholdFilter 过滤)
9.4 日志级别查询命令
# 查询所有 INFO 级别的日志
grep "\[INFO\]" logs/monitor-aoplog.log
# 查询所有 ERROR 级别的日志
grep "\[ERROR\]" logs/monitor-aoplog.log
# 查询所有 DEBUG 级别的日志
grep "\[DEBUG\]" logs/monitor-aoplog.log
# 查询异常日志
grep "\[异常\]" logs/monitor-aoplog.log
# 查看错误日志文件(只包含 ERROR 级别)
cat logs/monitor-error.log
9.5 日志级别分类总结
| 日志级别 | 使用场景 | 代码示例 | 日志文件 |
|---|---|---|---|
| INFO | 正常业务流程 | LOG.info("[参数] ...") |
monitor-aoplog.log |
| INFO | 方法执行信息 | logAopFormat.afterMethod() |
monitor-aoplog.log |
| DEBUG | 调试信息(SQL、连接等) | MyBatis SQL 日志 | monitor-aoplog.log |
| ERROR | 异常信息 | LOG.error("[异常] ...") |
monitor-aoplog.log + monitor-error.log |
| ERROR | 详细异常堆栈 | ERROR_LOG.error("异常堆栈:", ex) |
monitor-error.log |
9.6 日志级别层次
TRACE < DEBUG < INFO < WARN < ERROR < FATAL
- TRACE:最详细,通常不使用
- DEBUG:调试信息(SQL、参数等)
- INFO:正常业务流程(方法执行、参数、返回值)
- WARN:警告信息(当前项目未使用)
- ERROR:错误和异常
- FATAL:致命错误(当前项目未使用)
9.7 总结
AOP 日志级别的区分方式:
-
代码层面:
LOG.info()→ INFO 级别LOG.error()→ ERROR 级别LOG.debug()→ DEBUG 级别
-
日志格式:
[%level]在日志中显示级别标记- 格式:
[时间] [线程] [级别] [IP] 消息
-
文件分离:
monitor-aoplog.log:所有级别(INFO、DEBUG、ERROR 等)monitor-error.log:只包含 ERROR 级别(通过 ThresholdFilter 过滤)
-
查询方式:
- 通过
grep命令按级别过滤 - 通过日志格式中的
[级别]字段识别
- 通过
十、常见问题
10.1 日志没有记录
可能原因:
- 方法没有添加
@LogAnnotation注解 - AOP 配置类没有被 Spring 扫描到
- 切点表达式不正确
解决方法:
- 检查方法是否添加了
@LogAnnotation - 确认
AopLogConfig类上有@Configuration和@Aspect注解 - 确认切点表达式是否正确
10.2 参数或返回值没有记录
可能原因:
- 使用的是
@Before和@After,无法获取返回值 - 参数格式化方法有问题
解决方法:
- 使用
@Around通知,可以获取返回值 - 检查参数格式化方法的实现
10.3 线程数据混乱
可能原因:
- ThreadLocal 没有正确清理
- 线程池复用导致 ThreadLocal 数据残留
解决方法:
- 确保在方法执行后清理 ThreadLocal
- 在 Filter 的
finally块中清理 ThreadLocal
十一、总结
ins-framework-aoplog 是一个功能完善的 AOP 日志框架,通过注解和切面技术实现了自动化的日志记录。其主要优势包括:
- 自动化:通过注解自动记录日志,减少代码侵入
- 统一格式:标准化的日志格式,便于日志分析和监控
- 链路追踪:支持完整的调用链追踪,便于问题定位
- 灵活扩展:可以自定义切点和通知,满足不同需求
- Logback 集成:通过 SLF4J API 集成 Logback,提供强大的日志管理能力
- 日志级别区分:支持 INFO、DEBUG、ERROR 等多种日志级别,便于日志分类和查询
在实际项目中,合理使用该框架可以大大提高系统的可观测性和问题排查效率。
11.1 核心要点回顾
- AopLogConfig 自动工作:通过 Spring AOP 机制自动发现和应用,无需手动调用
- 日志级别区分:通过 SLF4J API 的方法(
info()、error())和 Logback 的%level格式体现 - AOP 与 Logback 集成:框架使用 SLF4J API,底层由 Logback 实现,通过
logback-spring.xml配置 - 完整调用链追踪:通过业务ID、方法ID、追踪ID 实现完整的调用链追踪
十二、AspectJ Pointcut 表达式语法详解
在 AopLogConfig 中,我们使用了 AspectJ 的 pointcut 表达式来定义切点。本节详细讲解 AspectJ pointcut 表达式的语法规则和用法。
12.1 基本语法结构
AspectJ pointcut 表达式的基本格式:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)
其中:
?表示可选部分modifiers-pattern:修饰符模式(public、private、protected、static 等)ret-type-pattern:返回类型模式declaring-type-pattern:声明类型模式(类名)name-pattern:方法名模式param-pattern:参数模式throws-pattern:异常类型模式
12.2 关键符号说明
12.2.1 星号 * - 通配符
* 用于匹配任意内容,但只能匹配一个层级。
用法示例:
// 1. 匹配任意返回类型
execution(* pdfc.monitor.service.*.*(..))
// 匹配:pdfc.monitor.service 包下所有类的所有方法
// 不匹配:pdfc.monitor.service.impl 包下的类(因为 * 只匹配一层)
// 2. 匹配任意方法名
execution(* pdfc.monitor.service.UserService.*(..))
// 匹配:UserService 类中的所有方法
// 3. 匹配任意类名
execution(* pdfc.monitor.service.*Service.*(..))
// 匹配:以 Service 结尾的类中的所有方法
// 例如:UserService、OrderService
// 4. 匹配任意包名(单层)
execution(* pdfc.*.service.*.*(..))
// 匹配:pdfc 下任意一级子包中的 service 包下的所有方法
// 例如:pdfc.monitor.service、pdfc.order.service
// 不匹配:pdfc.monitor.web.service(因为 * 只匹配一层)
12.2.2 点 . - 包分隔符
. 用于分隔包名和类名,表示精确的层级关系。
用法示例:
// 精确匹配包路径
execution(* pdfc.monitor.service.*.*(..))
// 匹配:pdfc.monitor.service 包下的所有类的所有方法
// 不匹配:pdfc.monitor.service.impl(因为 . 表示精确的下一层)
12.2.3 双点 .. - 递归匹配
.. 用于匹配任意层级的包或任意数量的参数。
用法示例:
// 1. 匹配任意层级的子包
execution(* pdfc.monitor..*.*(..))
// 匹配:pdfc.monitor 包及其所有子包下的所有类的所有方法
// 例如:
// - pdfc.monitor.service.UserService
// - pdfc.monitor.service.impl.UserServiceImpl
// - pdfc.monitor.web.controller.UserController
// - pdfc.monitor.domain.sys.dao.UserDao
// 2. 匹配任意数量的参数
execution(* pdfc.monitor.service.*.*(..))
// 匹配:任意参数的方法
// 例如:
// - void method()
// - void method(String arg)
// - void method(String arg1, Integer arg2)
// - void method(String... args)
// 3. 组合使用
execution(* pdfc..service..*.*(..))
// 匹配:pdfc 下任意层级中,包含 service 的包路径下的所有方法
// 例如:
// - pdfc.monitor.service.UserService
// - pdfc.order.service.impl.OrderServiceImpl
12.2.4 括号 () - 参数匹配
() 用于匹配方法参数。
用法示例:
// 1. 无参数
execution(* pdfc.monitor.service.*.*())
// 匹配:无参数的方法
// 例如:void init()
// 2. 任意参数(使用 ..)
execution(* pdfc.monitor.service.*.*(..))
// 匹配:任意参数的方法
// 例如:
// - void method()
// - void method(String arg)
// - void method(String arg1, Integer arg2)
// 3. 精确参数类型
execution(* pdfc.monitor.service.*.*(String))
// 匹配:只有一个 String 类型参数的方法
// 例如:void method(String name)
// 4. 多个参数
execution(* pdfc.monitor.service.*.*(String, Integer))
// 匹配:第一个参数是 String,第二个参数是 Integer 的方法
// 例如:void method(String name, Integer age)
// 5. 第一个参数任意,后面还有参数
execution(* pdfc.monitor.service.*.*(.., String))
// 匹配:最后一个参数是 String 的方法
// 例如:
// - void method(String name)
// - void method(Integer id, String name)
// 6. 第一个参数是 String,后面还有任意参数
execution(* pdfc.monitor.service.*.*(String, ..))
// 匹配:第一个参数是 String,后面还有任意参数的方法
// 例如:
// - void method(String name)
// - void method(String name, Integer age)
12.3 项目中的实际例子
12.3.1 示例 1:拦截注解方法
@Pointcut("@annotation(ins.framework.aop.log.util.LogAnnotation)")
public void logAnnotationPointcut() {
}
说明:
@annotation(...):匹配带有指定注解的方法- 匹配所有带有
@LogAnnotation注解的方法,无论类和方法的位置
匹配示例:
// 匹配
@LogAnnotation(name = "查询用户", description = "根据ID查询用户")
public User getUserById(Long id) { ... }
// 不匹配
public User getUserById(Long id) { ... } // 没有注解
12.3.2 示例 2:拦截 DAO 层方法
@Pointcut("execution(* pdfc.monitor.domain.sys.dao.*.*(..))")
public void daoMethodPointcut() {
}
语法分解:
execution:执行切点*:任意返回类型pdfc.monitor.domain.sys.dao:精确的包路径.*:包下的任意类(*匹配类名).*:类中的任意方法(*匹配方法名)(..):任意参数
匹配示例:
// 匹配
package pdfc.monitor.domain.sys.dao;
public interface SysUserDao {
List<SysUser> findAll(); // ✓
SysUser findById(Long id); // ✓
void save(SysUser user); // ✓
}
// 不匹配
package pdfc.monitor.domain.sys.dao.impl; // 不在 dao 包下
public class SysUserDaoImpl { ... } // ✗
package pdfc.monitor.domain.sys.dao;
public class SysUserHelper { ... } // 在 dao 包下,但会被匹配
12.3.3 示例 3:拦截应用层方法(排除 DAO)
@Pointcut("execution(* pdfc.monitor..*.*(..)) && !execution(* pdfc.monitor.domain.sys.dao.*.*(..))")
public void applicationMethodPointcut() {
}
语法分解:
execution(* pdfc.monitor..*.*(..)):pdfc.monitor..:pdfc.monitor包及其所有子包(..表示递归)*.*:任意类的任意方法(..):任意参数
&&:逻辑与(AND)!execution(...):逻辑非(NOT),排除 DAO 层方法
匹配示例:
// 匹配(应用层)
package pdfc.monitor.service;
public class UserService {
public void method() { ... } // ✓
}
package pdfc.monitor.web.controller;
public class UserController {
public void method() { ... } // ✓
}
package pdfc.monitor.config;
public class AopLogConfig {
public void method() { ... } // ✓
}
// 不匹配(被排除)
package pdfc.monitor.domain.sys.dao;
public interface SysUserDao {
public void method() { ... } // ✗(被 !execution 排除)
}
12.4 逻辑运算符
AspectJ 支持逻辑运算符来组合多个切点表达式。
12.4.1 逻辑与 &&(AND)
@Pointcut("execution(* pdfc.monitor.service.*.*(..)) && execution(* *Service.*(..))")
public void servicePointcut() {
}
// 匹配:pdfc.monitor.service 包下,类名以 Service 结尾的类的所有方法
12.4.2 逻辑或 ||(OR)
@Pointcut("execution(* pdfc.monitor.service.*.*(..)) || execution(* pdfc.monitor.web.*.*(..))")
public void serviceOrWebPointcut() {
}
// 匹配:service 包或 web 包下的所有方法
12.4.3 逻辑非 !(NOT)
@Pointcut("execution(* pdfc.monitor..*.*(..)) && !execution(* pdfc.monitor.config.*.*(..))")
public void excludeConfigPointcut() {
}
// 匹配:pdfc.monitor 包下的所有方法,但排除 config 包
12.5 其他常用 Pointcut 表达式
12.5.1 基于注解的切点
// 匹配带有指定注解的方法
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")
public void transactionalPointcut() {
}
// 匹配带有指定注解的类中的所有方法
@Pointcut("@within(org.springframework.stereotype.Service)")
public void serviceClassPointcut() {
}
// 匹配带有指定注解的类及其子类中的所有方法
@Pointcut("@target(org.springframework.stereotype.Controller)")
public void controllerPointcut() {
}
12.5.2 基于类型的切点
// 匹配实现指定接口的所有方法
@Pointcut("execution(* pdfc.monitor.service.*Service+.*(..))")
public void serviceInterfacePointcut() {
}
// + 表示匹配该类型及其子类型
// 匹配指定类的所有方法
@Pointcut("execution(* pdfc.monitor.service.UserService.*(..))")
public void userServicePointcut() {
}
12.5.3 基于包名的切点
// 匹配指定包下的所有方法
@Pointcut("within(pdfc.monitor.service..*)")
public void servicePackagePointcut() {
}
// within 用于匹配包或类,..* 表示包下所有类
// 排除指定包
@Pointcut("within(pdfc.monitor..*) && !within(pdfc.monitor.config..*)")
public void excludeConfigPackagePointcut() {
}
12.5.4 基于方法签名的切点
// 匹配方法名
@Pointcut("execution(* *Service.find*(..))")
public void findMethodPointcut() {
}
// 匹配:类名以 Service 结尾,方法名以 find 开头的方法
// 匹配返回类型
@Pointcut("execution(List pdfc.monitor.service.*.*(..))")
public void listReturnPointcut() {
}
// 匹配:返回类型为 List 的方法
// 匹配参数类型
@Pointcut("execution(* pdfc.monitor.service.*.*(String, ..))")
public void stringFirstParamPointcut() {
}
// 匹配:第一个参数是 String 的方法
12.6 常见用法模式
12.6.1 拦截所有 Controller 方法
@Pointcut("execution(* pdfc.monitor.web.controller..*.*(..))")
public void controllerPointcut() {
}
12.6.2 拦截所有 Service 方法
@Pointcut("execution(* pdfc.monitor.service..*.*(..))")
public void servicePointcut() {
}
12.6.3 拦截所有 DAO 方法
@Pointcut("execution(* pdfc.monitor.domain..dao.*.*(..))")
public void daoPointcut() {
}
// 匹配所有 domain 子包下的 dao 包中的方法
12.6.4 拦截特定方法名模式
// 拦截所有查询方法
@Pointcut("execution(* pdfc.monitor..*.find*(..)) || " +
"execution(* pdfc.monitor..*.get*(..)) || " +
"execution(* pdfc.monitor..*.query*(..))")
public void queryMethodPointcut() {
}
// 拦截所有保存方法
@Pointcut("execution(* pdfc.monitor..*.save*(..)) || " +
"execution(* pdfc.monitor..*.insert*(..)) || " +
"execution(* pdfc.monitor..*.add*(..))")
public void saveMethodPointcut() {
}
12.6.5 排除特定方法
// 拦截所有方法,但排除 toString、equals、hashCode
@Pointcut("execution(* pdfc.monitor..*.*(..)) && " +
"!execution(* *.toString()) && " +
"!execution(* *.equals(..)) && " +
"!execution(* *.hashCode())")
public void excludeObjectMethodsPointcut() {
}
12.7 符号对比总结
| 符号 | 含义 | 匹配范围 | 示例 |
|---|---|---|---|
* |
通配符 | 单个层级或单个名称 | *.service.* 匹配一层包名 |
. |
包分隔符 | 精确的下一层 | pdfc.monitor 精确匹配 |
.. |
递归匹配 | 任意层级或任意数量 | pdfc.monitor.. 匹配所有子包 |
() |
无参数 | 精确匹配无参数方法 | method() |
(..) |
任意参数 | 匹配任意参数的方法 | method(..) |
(String) |
单参数 | 匹配一个 String 参数 | method(String) |
(String, ..) |
可变参数 | 第一个是 String,后面任意 | method(String, ...) |
&& |
逻辑与 | 同时满足 | A && B |
|| |
逻辑或 | 满足其一 | A || B |
! |
逻辑非 | 排除 | !A |
12.8 最佳实践
-
精确匹配优先:尽量使用精确的包路径,避免过度使用
..// 推荐 execution(* pdfc.monitor.service.*.*(..)) // 不推荐(过于宽泛) execution(* pdfc..*.*(..)) -
使用逻辑运算符组合:合理使用
&&、||、!来精确控制切点// 推荐:明确排除不需要的包 execution(* pdfc.monitor..*.*(..)) && !execution(* pdfc.monitor.config.*.*(..)) -
使用注解切点:对于需要特殊处理的方法,使用注解更清晰
// 推荐:使用注解标记需要日志的方法 @Pointcut("@annotation(ins.framework.aop.log.util.LogAnnotation)") -
避免过度拦截:不要拦截框架类、工具类等,避免性能问题
// 推荐:排除框架类 execution(* pdfc.monitor..*.*(..)) && !within(org.springframework..*) -
测试切点表达式:使用 AOP 调试工具或日志验证切点是否正确匹配
12.9 调试技巧
如果切点表达式没有按预期工作,可以通过以下方式调试:
-
添加日志:在通知方法中添加日志,查看是否被调用
@Around("yourPointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { LOG.info("切点匹配: {}", joinPoint.getSignature()); return joinPoint.proceed(); } -
使用 Spring AOP 调试:在
application.properties中启用 AOP 调试logging.level.org.springframework.aop=DEBUG -
验证方法签名:确认目标方法的完整签名(包名、类名、方法名、参数类型)
文档版本:v1.2
最后更新:2025-11-24
作者:Allen
项目:monitor_java
更多推荐
所有评论(0)