摘要:想象一栋写字楼,如果每个房间都自己配锁、拉监控,既费钱又容易漏;更聪明的做法是装一套统一的门禁和中控,访客一刷卡,全楼的安全和记录都被接管。aop 就是这套“中央管家”——把日志与权限从每个接口中抽离,统一织入,既轻量又可追溯。
1. 场景与概念对照
- 痛点:每个接口都写日志与权限,像每个房间各自装锁和摄像头,重复又易漏。
- aop 角色类比:切面=管家,通知=动作(餐前消毒/餐后清洁),连接点=每次上菜瞬间,切入点=哪些桌子需要清洁。
- 概念与代码速查表:
- 切面 → @aspect 类
- 通知 → @around/@before/@afterreturning
- 连接点 → 目标方法调用
- 切入点 → @pointcut 表达式
2. 环境准备与版本差异
spring-boot-starter-aop 与 spring-boot-starter-web 示例使用 spring boot 2.7.15,代码同时兼容 1.5.x 和 3.x(3.x 需把 javax.servlet 换成 jakarta.servlet 包名即可,boot 1.5.x 仍使用 javax.servlet,aop 注解与用法保持一致):
<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-aop</artifactid> <version>2.7.15</version> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid> <version>2.7.15</version> </dependency>
如果你在维护老项目(如 spring boot 1.5.x),只需把上面的 <version> 改成 1.5.x 系列(如 1.5.22.release),然后确保 jdk8 与 spring aop 版本匹配即可,切面与注解写法可直接复用;如果是新项目(spring boot 3.x+),将依赖版本升级到 3.x,同时把示例中的 javax.servlet.http.httpservletrequest 改为 jakarta.servlet.http.httpservletrequest 即可。
包结构建议:com.demo.aop.annotation / aspect / common / controller。
3. 注解定义(支持类与方法)
package com.demo.aop.annotation;
import java.lang.annotation.*;
// 简单日志级别枚举,用于控制切面日志输出级别
public enum loglevel { trace, debug, info, warn, error }
// 日志注解:可标在类或方法上,控制是否记录日志以及日志细节
@target({elementtype.type, elementtype.method})
@retention(retentionpolicy.runtime)
public @interface apilog {
// 业务含义描述,例如“查询订单”“用户登录”
string value() default "";
// 是否忽略当前方法(即使类上加了 apilog)
boolean ignore() default false;
// 日志级别,默认为 info
loglevel level() default loglevel.info;
// 是否隐藏响应体(例如大对象或隐私数据)
boolean hideresp() default false;
}
// 权限注解:可标在类或方法上,声明允许访问的角色
@target({elementtype.type, elementtype.method})
@retention(retentionpolicy.runtime)
public @interface requirerole {
// 支持多个角色,只要命中其中一个即可访问
string[] value();
}
类级别生效逻辑:切面中若方法未标注,回退检查类上的注解。
4. 通用返回体与异常
package com.demo.aop.common;
public class apiresponse<t> {
// 业务状态码,0 表示成功,其他表示失败
private int code;
// 业务提示信息
private string message;
// 真实数据载体
private t data;
// 快捷构造成功返回
public static <t> apiresponse<t> ok(t d){ return of(0,"ok",d); }
// 快捷构造失败返回
public static <t> apiresponse<t> fail(string msg){ return of(-1,msg,null); }
private static <t> apiresponse<t> of(int c,string m,t d){
apiresponse<t> r=new apiresponse<>(); r.code=c; r.message=m; r.data=d; return r;
}
}
// 自定义权限异常,统一由全局异常处理器拦截
public class permissiondeniedexception extends runtimeexception {
public permissiondeniedexception(string msg){ super(msg); }
}
全局异常处理(json 返回):
@restcontrolleradvice
public class globalexceptionhandler {
// 捕获权限异常,统一转成 apiresponse json 返回
@exceptionhandler(permissiondeniedexception.class)
public apiresponse<void> handlepermission(permissiondeniedexception e){
return apiresponse.fail(e.getmessage());
}
}
5. 日志切面(含 npe 防护、脱敏、traceid)
package com.demo.aop.aspect;
import com.demo.aop.annotation.apilog;
import com.demo.aop.annotation.loglevel;
import com.demo.aop.common.apiresponse;
import com.fasterxml.jackson.core.jsonprocessingexception;
import com.fasterxml.jackson.databind.objectmapper;
import org.aspectj.lang.proceedingjoinpoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.order;
import org.springframework.stereotype.component;
import org.springframework.web.context.request.requestcontextholder;
import org.springframework.web.context.request.servletrequestattributes;
import javax.servlet.http.httpservletrequest;
import java.util.*;
@aspect
@component
@order(2) // 权限优先,日志在后
public class apilogaspect {
// slf4j 日志器
private static final org.slf4j.logger log = org.slf4j.loggerfactory.getlogger(apilogaspect.class);
// 统一 json 序列化
private final objectmapper mapper = new objectmapper();
// 切入点:匹配标了 @apilog 的方法或类
@pointcut("@annotation(com.demo.aop.annotation.apilog) || @within(com.demo.aop.annotation.apilog)")
public void apilogpointcut() {}
// 环绕通知:在目标方法前后插入日志逻辑
@around("apilogpointcut()")
public object recordlog(proceedingjoinpoint pjp) throws throwable {
// 兼容方法级 / 类级注解
apilog ann = findannotation(pjp);
if (ann == null || ann.ignore()) { return pjp.proceed(); }
// 记录开始时间,用于计算耗时
long start = system.currenttimemillis();
// 从请求上下文中获取 httpservletrequest,非 web 环境直接放行
servletrequestattributes attrs = (servletrequestattributes) requestcontextholder.getrequestattributes();
if (attrs == null) { // 非 web 环境兜底
return pjp.proceed();
}
httpservletrequest req = attrs.getrequest();
// traceid:从 header 取,没有则生成一个新的
string traceid = optional.ofnullable(req.getheader("x-trace-id"))
.orelse(uuid.randomuuid().tostring());
// 基本请求信息
string url = req.getrequesturi();
string method = req.getmethod();
string user = optional.ofnullable(req.getheader("x-user")).orelse("anonymous");
// ip:优先使用 x-forwarded-for 头(兼容网关 / 负载)
string ip = optional.ofnullable(req.getheader("x-forwarded-for"))
.map(s -> s.split(",")[0].trim()).orelse(req.getremoteaddr());
// 客户端标识
string ua = optional.ofnullable(req.getheader("user-agent")).orelse("unknown");
// 请求参数 map
map<string, string[]> params = req.getparametermap();
// 将参数转 json 并脱敏
string args = mask(tojsonsafe(params));
object result = null; throwable ex = null;
try {
// 继续执行真实业务方法
result = pjp.proceed();
return result;
} catch (throwable t) {
// 记录异常并向上抛出
ex = t; throw t;
} finally {
long cost = system.currenttimemillis() - start;
// 根据注解配置决定是否输出响应体
string resp = ann.hideresp() ? "<hidden>" : tojsonsafe(result);
// 按指定日志级别输出
logwithlevel(ann.level(), "[api-log] trace={} {} {} user={} ip={} ua={} cost={}ms args={} resp={} err={}",
traceid, method, url, user, ip, ua, cost, args, resp, ex == null ? "none" : ex.getmessage());
}
}
// 查找方法 / 类上的 apilog 注解
private apilog findannotation(proceedingjoinpoint pjp) {
apilog methodann = org.springframework.core.annotation.annotationutils
.findannotation(((org.aspectj.lang.reflect.methodsignature) pjp.getsignature()).getmethod(), apilog.class);
apilog typeann = org.springframework.core.annotation.annotationutils
.findannotation(pjp.gettarget().getclass(), apilog.class);
return methodann != null ? methodann : typeann;
}
// 安全 json 序列化,失败时给出占位字符串
private string tojsonsafe(object obj){
try { return mapper.writevalueasstring(obj); }
catch (jsonprocessingexception e){
log.warn("json serialize fail", e);
return "<json-error>";
}
}
// 简单脱敏:隐藏密码与手机号中间四位
private string mask(string json){
if (json == null) return null;
// password/pwd 字段统一替换为 ****
return json.replaceall("(?i)(password|pwd)\":\"[^\"]+\"", "$1\":\"****\"")
// phone 字段保留前 3 位和后 4 位,中间打 ***
.replaceall("(\"phone\"\\s*:\\s*\")\\d{3}\\d{4}(\\d{4}\")", "$1***$2");
}
// 根据注解上的日志级别动态选择输出方法
private void logwithlevel(loglevel level, string msg, object... args){
switch (level){
case trace: log.trace(msg, args); break;
case debug: log.debug(msg, args); break;
case warn: log.warn(msg, args); break;
case error: log.error(msg, args); break;
default: log.info(msg, args);
}
}
}
要点:判空防 npe;json 序列化异常兜底;脱敏密码/手机号;traceid/ip/user-agent 记录;支持 hideresp 与日志级别;类级别注解兼容。
6. 权限切面(多角色 + 自定义异常)
package com.demo.aop.aspect;
import com.demo.aop.annotation.requirerole;
import com.demo.aop.common.permissiondeniedexception;
import org.aspectj.lang.proceedingjoinpoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.order;
import org.springframework.stereotype.component;
import org.springframework.web.context.request.requestcontextholder;
import org.springframework.web.context.request.servletrequestattributes;
import javax.servlet.http.httpservletrequest;
import java.util.arrays;
@aspect
@component
@order(1) // 先校验权限,再记录日志
public class authaspect {
@pointcut("@annotation(com.demo.aop.annotation.requirerole) || @within(com.demo.aop.annotation.requirerole)")
public void authpointcut() {}
@around("authpointcut() && @annotation(requirerole)")
public object checkrole(proceedingjoinpoint pjp, requirerole requirerole) throws throwable {
// 获取当前请求上下文,非 web 环境直接略过权限校验
servletrequestattributes attrs = (servletrequestattributes) requestcontextholder.getrequestattributes();
if (attrs == null) { return pjp.proceed(); } // 非 web 环境直接放行
httpservletrequest req = attrs.getrequest();
// 1) 从 header 中读取角色(演示用,真实场景多为 jwt / session)
string role = req.getheader("x-role");
// 2) jwt 极简示例(伪代码)
// string role = jwtutil.parserole(req.getheader("authorization"));
// 3) db 极简示例(伪代码)
// list<string> roles = rolerepo.findrolesbyuser(req.getheader("x-user"));
// 只要当前角色命中注解声明的任一角色,就视为通过
boolean ok = role != null && arrays.stream(requirerole.value())
.anymatch(r -> r.equalsignorecase(role));
// 未通过则抛出权限异常,由全局异常处理器统一返回 json
if (!ok) { throw new permissiondeniedexception("无访问权限,需角色: " + string.join("/", requirerole.value())); }
return pjp.proceed();
}
}
7. controller 示例
package com.demo.aop.controller;
import com.demo.aop.annotation.apilog;
import com.demo.aop.annotation.loglevel;
import com.demo.aop.annotation.requirerole;
import com.demo.aop.common.apiresponse;
import org.springframework.web.bind.annotation.*;
// 类级别加上 apilog:该 controller 所有接口默认记录日志
@apilog(value="类级日志", level=loglevel.debug)
@restcontroller
@requestmapping("/demo")
public class democontroller {
// 查询订单接口:单独指定日志描述,并要求 admin/ops 角色
@apilog(value="查询订单", hideresp=false)
@requirerole({"admin","ops"})
@getmapping("/order")
public apiresponse<string> getorder(@requestparam string id) {
// 真实场景可查询数据库,这里仅返回一个拼接字符串
return apiresponse.ok("order-" + id);
}
// 健康检查接口:可被监控系统频繁调用,隐藏响应体减少日志噪音
@apilog(value="健康检查", ignore=false, hideresp=true)
@getmapping("/ping")
public apiresponse<string> ping() {
return apiresponse.ok("pong");
}
}
8. 优势、扩展与排查
- 优势:业务零侵入、格式统一、可配置(级别/忽略/隐藏响应),更易审计与追踪。
- 扩展:日志异步落盘/发 mq;权限列表支持;与 spring security 配合(注解转 securitymetadatasource);接入 elk 关联 traceid。
- 常见问题排查:
- 切面不生效:类未被 spring 管理或方法是
private/final;调整为public并交给容器。 - 注解写在接口而实现类无代理:确保代理对象被调用,或把注解写到实现类。
- 未开启 aop:确认引入 starter,未手动禁用
spring.aop.auto=true。
- 切面不生效:类未被 spring 管理或方法是
9. 小结
把日志与权限交给 aop 这位“统一管家”,再加上空指针兜底、json 异常防护、脱敏与多角色校验,就能在生产环境稳稳落地。后续可平滑接入 spring security 或 elk,持续进化。
到此这篇关于springboot集成aop实现日志记录与接口权限校验的文章就介绍到这了,更多相关springboot 日志记录与接口权限内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论