@OperInfo 標註業務方法,支援 SpEL 模板LogAspect 環繞通知攔截,解析註解、構建日誌對象、異步入庫@OperInfo@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface OperInfo {
String description() default ""; // SpEL 模板,如 "新增了名為#{#user.username}的用戶"
}
LogAspectpublic class LogAspect {
@Resource private OperationLogService operService;
public Object doAroundMethodForCtLog(ProceedingJoinPoint pjp) throws Throwable {
Method method = getMethod(pjp);
if (method != null && method.isAnnotationPresent(OperInfo.class)) {
OperInfo annotation = method.getAnnotation(OperInfo.class);
String description = parseSpelTemplate(annotation.description(), pjp);
HttpServletRequest request = HttpServletUtils.getRequest();
Users user = (Users) request.getSession().getAttribute("users");
OperationLog log = new OperationLog();
log.setOptid(UUIDUtils.getUUID());
log.setChangeuserid(user.getUsersid());
log.setChangetime(new Date());
log.setDescription("用戶:" + user.getUsername() + ",日誌信息:【{" + description + "}】");
operService.insertService(log); // 建議異步執行
}
return pjp.proceed(); // 繼續執行原方法
}
/** SpEL 模板解析 */
private String parseSpelTemplate(String template, ProceedingJoinPoint joinPoint) {
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
Method method = getMethod(joinPoint);
String[] paramNames = discoverer.getParameterNames(method);
EvaluationContext context = new StandardEvaluationContext();
ExpressionParser parser = new SpelExpressionParser();
Object[] args = joinPoint.getArgs();
if (args.length == paramNames.length) {
for (int i = 0; i < args.length; i++) {
context.setVariable(paramNames[i], args[i]);
}
}
return parser.parseExpression(template, new TemplateParserContext()).getValue(context, String.class);
}
private Method getMethod(ProceedingJoinPoint joinPoint) {
Object target = joinPoint.getTarget();
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
try {
return target.getClass().getMethod(method.getName(), method.getParameterTypes());
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
<bean id="logAspect" class="com.aop.LogAspect" />
<aop:config expose-proxy="true">
<aop:pointcut expression="execution(* com.service.impl.*Impl.*(..))" id="txPointcut"/>
<aop:aspect ref="logAspect">
<aop:around method="doAroundMethodForCtLog" pointcut-ref="txPointcut" />
</aop:aspect>
</aop:config>
@OperInfo(description = "新增了名為#{#user.username}的用戶")
public int insertUser(Users user) {
// 業務邏輯
return 1;
}
SpEL 上下文自動包含方法參數 user,可直接存取屬性 #user.username、巢狀屬性 #order.items[0].productName、集合投影 #list.?[price>100] 等高級語法。
| 點 | 說明 |
|---|---|
| 參數名發現 | LocalVariableTableParameterNameDiscoverer 需編譯時保留參數名 (-parameters 或 javac -g) |
| SpEL 安全性 | 模板來自註解(開發者可控),避免外部注入風險 |
| 效能考量 | 日誌入庫建議異步 (ThreadPoolTaskExecutor / @Async),避免阻塞主業務 |
| 異常處理 | 切面捕獲異常記錄錯誤日誌,但不阻斷原方法異常拋出 |
| 擴展性 | 可增加 @OperInfo(type="LOGIN") 分類、支援條件執行 condition="#user.id>100" |
@Async("logExecutor")
public void insertService(OperationLog log) {
operationLogDao.insert(log);
}
配置 ThreadPoolTaskExecutor bean 名為 logExecutor,核心池 10、隊列 500、拒絕策略 CallerRunsPolicy,確保高峰期不丟日誌、不阻塞業務。
AOP + 註解 + SpEL 實現零侵入、聲明式、動態模板的操作日誌記錄,核心在於參數名發現與異步持久化。掌握此模式,可快速擴展到審計日誌、安全審計、業務指標埋點等場景。