傳統在業務代碼中手動調用 log.info() 既侵入性強又難以統一。本文展示如何通過自定義註解 @Log + Spring AOP 切面,實現對 Controller 層方法的無侵入式日志記錄:操作類型、操作名稱、耗時、請求參數、異常堆棧、請求 IP、操作人,自動入庫數據庫,並支持控制台輸出與本地文件雙通道。
@Log(operationType, operationName) 標記在 Controller 方法上。SystemLogAspect 定義切點、前置/環繞/後置/異常/返回通知。SystemLog 實體映射日志表字段。SystemLogMapper (MyBatis) + SystemLogService 完成入庫。<aop:aspectj-autoproxy proxy-target-class="true"/> 強制 CGLIB 代理。@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
String operationType() default ""; // 如 "add操作:"
String operationName() default ""; // 如 "添加用戶"
}
@Aspect
@Component
public class SystemLogAspect {
@Resource private SystemLogService systemLogService;
private static final Logger logger = LoggerFactory.getLogger(SystemLogAspect.class);
// Controller 層切點
@Pointcut("execution(* com.gcx.controller..*.*(..))")
public void controllerAspect() {}
// 環繞通知:計算耗時
@Around("controllerAspect()")
public void around(JoinPoint jp) {
long start = System.currentTimeMillis();
try { ((ProceedingJoinPoint) jp).proceed(); }
finally {
long cost = System.currentTimeMillis() - start;
logger.info("around " + jp + "\tUse time: " + cost + " ms");
}
}
// 後置通知:記錄正常操作日志(核心業務邏輯)
@After("controllerAspect()")
public void after(JoinPoint jp) {
// 1. 解析註解信息
Method method = resolveMethod(jp);
Log logAnn = method.getAnnotation(Log.class);
String opType = logAnn != null ? logAnn.operationType() : "";
String opName = logAnn != null ? logAnn.operationName() : "";
// 2. 構建日志實體(簡化:模擬用戶/IP)
SystemLog log = new SystemLog();
log.setId(UUID.randomUUID().toString());
log.setDescription(opName);
log.setMethod(jp.getTarget().getClass().getName() + "." + jp.getSignature().getName() + "()" + opType);
log.setLogType(0L); // 正常
log.setRequestIp("127.0.0.1");
log.setCreateBy("張三");
log.setCreateDate(new Date());
// 3. 入庫
systemLogService.insert(log);
}
// 異常通知:記錄異常日志
@AfterThrowing(pointcut = "controllerAspect()", throwing = "e")
public void doAfterThrowing(JoinPoint jp, Throwable e) {
// 解析參數序列化為 JSON
String params = Arrays.stream(jp.getArgs())
.map(JsonUtil::getJsonStr).collect(Collectors.joining(";"));
SystemLog log = new SystemLog();
log.setId(UUID.randomUUID().toString());
log.setExceptioncode(e.getClass().getName());
log.setExceptionDetail(e.getMessage());
log.setParams(params);
log.setLogType(1L); // 異常
// ... 其他字段同 after()
systemLogService.insert(log);
}
// 輔助方法:精確匹配方法簽名(含參數類型)
private Method resolveMethod(JoinPoint jp) throws Exception {
String className = jp.getTarget().getClass().getName();
String methodName = jp.getSignature().getName();
Object[] args = jp.getArgs();
Class> clazz = Class.forName(className);
for (Method m : clazz.getMethods()) {
if (m.getName().equals(methodName) && m.getParameterTypes().length == args.length) {
return m;
}
}
throw new NoSuchMethodException();
}
}
@Controller
@RequestMapping("userController")
public class UserController {
@Autowired private UserService userService;
@RequestMapping("testAOP")
@Log(operationType = "add操作:", operationName = "添加用戶")
public void testAOP(String userName, String password) {
userService.addUser(userName, password);
}
}
<aop:aspectj-autoproxy proxy-target-class="true"/> <bean id="systemLogAspect" class="com.gcx.annotation.SystemLogAspect"/> <context:component-scan base-package="com.gcx"/>

控制台輸出包含:請求方法、方法描述、請求人、請求 IP、耗時、異常堆棧等。數據庫表 system_log 完整保存所有字段,便於後續審計查詢、報表分析。
***)。「註解 + AOP」模式實現了橫切關注點與業務邏輯完全解耦,是 Spring 生態中實現日志審計、權限控制、事務管理、緩存處理的標準化範式。掌握此模式,可輕松應對各類「無感知業務增強」需求。