最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

SpringBoot集成AOP實現(xiàn)日志記錄與接口權(quán)限校驗

 更新時間:2026年02月08日 10:03:28   作者:帥氣的你  
本文主要介紹了SpringBoot集成AOP實現(xiàn)日志記錄與接口權(quán)限校驗,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

摘要:想象一棟寫字樓,如果每個房間都自己配鎖、拉監(jiān)控,既費錢又容易漏;更聰明的做法是裝一套統(tǒng)一的門禁和中控,訪客一刷卡,全樓的安全和記錄都被接管。AOP 就是這套“中央管家”——把日志與權(quán)限從每個接口中抽離,統(tǒng)一織入,既輕量又可追溯。

1. 場景與概念對照

  • 痛點:每個接口都寫日志與權(quán)限,像每個房間各自裝鎖和攝像頭,重復(fù)又易漏。
  • AOP 角色類比:切面=管家,通知=動作(餐前消毒/餐后清潔),連接點=每次上菜瞬間,切入點=哪些桌子需要清潔。
  • 概念與代碼速查表:
    • 切面 → @Aspect 類
    • 通知 → @Around/@Before/@AfterReturning
    • 連接點 → 目標(biāo)方法調(diào)用
    • 切入點 → @Pointcut 表達(dá)式

2. 環(huán)境準(zhǔn)備與版本差異

spring-boot-starter-aopspring-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>

如果你在維護(hù)老項目(如 Spring Boot 1.5.x),只需把上面的 <version> 改成 1.5.x 系列(如 1.5.22.RELEASE),然后確保 JDK8 與 Spring AOP 版本匹配即可,切面與注解寫法可直接復(fù)用;如果是新項目(Spring Boot 3.x+),將依賴版本升級到 3.x,同時把示例中的 javax.servlet.http.HttpServletRequest 改為 jakarta.servlet.http.HttpServletRequest 即可。

包結(jié)構(gòu)建議: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 }

// 日志注解:可標(biāo)在類或方法上,控制是否記錄日志以及日志細(xì)節(jié)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiLog {
  // 業(yè)務(wù)含義描述,例如“查詢訂單”“用戶登錄”
  String value() default "";
  // 是否忽略當(dāng)前方法(即使類上加了 ApiLog)
  boolean ignore() default false;
  // 日志級別,默認(rèn)為 INFO
  LogLevel level() default LogLevel.INFO;
  // 是否隱藏響應(yīng)體(例如大對象或隱私數(shù)據(jù))
  boolean hideResp() default false;
}

// 權(quán)限注解:可標(biāo)在類或方法上,聲明允許訪問的角色
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
  // 支持多個角色,只要命中其中一個即可訪問
  String[] value();
}

類級別生效邏輯:切面中若方法未標(biāo)注,回退檢查類上的注解。

4. 通用返回體與異常

package com.demo.aop.common;

public class ApiResponse<T> {
  // 業(yè)務(wù)狀態(tài)碼,0 表示成功,其他表示失敗
  private int code;
  // 業(yè)務(wù)提示信息
  private String message;
  // 真實數(shù)據(jù)載體
  private T data;
  // 快捷構(gòu)造成功返回
  public static <T> ApiResponse<T> ok(T d){ return of(0,"OK",d); }
  // 快捷構(gòu)造失敗返回
  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;
  }
}

// 自定義權(quán)限異常,統(tǒng)一由全局異常處理器攔截
public class PermissionDeniedException extends RuntimeException {
  public PermissionDeniedException(String msg){ super(msg); }
}

全局異常處理(JSON 返回):

@RestControllerAdvice
public class GlobalExceptionHandler {
  // 捕獲權(quán)限異常,統(tǒng)一轉(zhuǎn)成 ApiResponse JSON 返回
  @ExceptionHandler(PermissionDeniedException.class)
  public ApiResponse<Void> handlePermission(PermissionDeniedException e){
    return ApiResponse.fail(e.getMessage());
  }
}

5. 日志切面(含 NPE 防護(hù)、脫敏、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) // 權(quán)限優(yōu)先,日志在后
public class ApiLogAspect {

  // Slf4j 日志器
  private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ApiLogAspect.class);
  // 統(tǒng)一 JSON 序列化
  private final ObjectMapper mapper = new ObjectMapper();

  // 切入點:匹配標(biāo)了 @ApiLog 的方法或類
  @Pointcut("@annotation(com.demo.aop.annotation.ApiLog) || @within(com.demo.aop.annotation.ApiLog)")
  public void apiLogPointcut() {}

  // 環(huán)繞通知:在目標(biāo)方法前后插入日志邏輯
  @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 環(huán)境直接放行
    ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    if (attrs == null) { // 非 Web 環(huán)境兜底
      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:優(yōu)先使用 X-Forwarded-For 頭(兼容網(wǎng)關(guān) / 負(fù)載)
    String ip = Optional.ofNullable(req.getHeader("X-Forwarded-For"))
            .map(s -> s.split(",")[0].trim()).orElse(req.getRemoteAddr());
    // 客戶端標(biāo)識
    String ua = Optional.ofNullable(req.getHeader("User-Agent")).orElse("unknown");
    // 請求參數(shù) Map
    Map<String, String[]> params = req.getParameterMap();
    // 將參數(shù)轉(zhuǎn) JSON 并脫敏
    String args = mask(toJsonSafe(params));

    Object result = null; Throwable ex = null;
    try {
      // 繼續(xù)執(zhí)行真實業(yè)務(wù)方法
      result = pjp.proceed();
      return result;
    } catch (Throwable t) {
      // 記錄異常并向上拋出
      ex = t; throw t;
    } finally {
      long cost = System.currentTimeMillis() - start;
      // 根據(jù)注解配置決定是否輸出響應(yīng)體
      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>";
    }
  }

  // 簡單脫敏:隱藏密碼與手機(jī)號中間四位
  private String mask(String json){
    if (json == null) return null;
    // password/pwd 字段統(tǒng)一替換為 ****
    return json.replaceAll("(?i)(password|pwd)\":\"[^\"]+\"", "$1\":\"****\"")
            // phone 字段保留前 3 位和后 4 位,中間打 ***
            .replaceAll("(\"phone\"\\s*:\\s*\")\\d{3}\\d{4}(\\d{4}\")", "$1***$2");
  }

  // 根據(jù)注解上的日志級別動態(tài)選擇輸出方法
  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 序列化異常兜底;脫敏密碼/手機(jī)號;TraceId/IP/User-Agent 記錄;支持 hideResp 與日志級別;類級別注解兼容。

6. 權(quán)限切面(多角色 + 自定義異常)

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) // 先校驗權(quán)限,再記錄日志
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 {
    // 獲取當(dāng)前請求上下文,非 Web 環(huán)境直接略過權(quán)限校驗
    ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    if (attrs == null) { return pjp.proceed(); } // 非 Web 環(huán)境直接放行
    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"));

    // 只要當(dāng)前角色命中注解聲明的任一角色,就視為通過
    boolean ok = role != null && Arrays.stream(requireRole.value())
            .anyMatch(r -> r.equalsIgnoreCase(role));
    // 未通過則拋出權(quán)限異常,由全局異常處理器統(tǒng)一返回 JSON
    if (!ok) { throw new PermissionDeniedException("無訪問權(quán)限,需角色: " + 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 所有接口默認(rèn)記錄日志
@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) {
        // 真實場景可查詢數(shù)據(jù)庫,這里僅返回一個拼接字符串
        return ApiResponse.ok("order-" + id);
    }

    // 健康檢查接口:可被監(jiān)控系統(tǒng)頻繁調(diào)用,隱藏響應(yīng)體減少日志噪音
    @ApiLog(value="健康檢查", ignore=false, hideResp=true)
    @GetMapping("/ping")
    public ApiResponse<String> ping() {
        return ApiResponse.ok("pong");
    }
}

8. 優(yōu)勢、擴(kuò)展與排查

  • 優(yōu)勢:業(yè)務(wù)零侵入、格式統(tǒng)一、可配置(級別/忽略/隱藏響應(yīng)),更易審計與追蹤。
  • 擴(kuò)展:日志異步落盤/發(fā) MQ;權(quán)限列表支持;與 Spring Security 配合(注解轉(zhuǎn) SecurityMetadataSource);接入 ELK 關(guān)聯(lián) traceId。
  • 常見問題排查:
    • 切面不生效:類未被 Spring 管理或方法是 private/final;調(diào)整為 public 并交給容器。
    • 注解寫在接口而實現(xiàn)類無代理:確保代理對象被調(diào)用,或把注解寫到實現(xiàn)類。
    • 未開啟 AOP:確認(rèn)引入 starter,未手動禁用 spring.aop.auto=true。

9. 小結(jié)

把日志與權(quán)限交給 AOP 這位“統(tǒng)一管家”,再加上空指針兜底、JSON 異常防護(hù)、脫敏與多角色校驗,就能在生產(chǎn)環(huán)境穩(wěn)穩(wěn)落地。后續(xù)可平滑接入 Spring Security 或 ELK,持續(xù)進(jìn)化。

到此這篇關(guān)于SpringBoot集成AOP實現(xiàn)日志記錄與接口權(quán)限校驗的文章就介紹到這了,更多相關(guān)SpringBoot 日志記錄與接口權(quán)限內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • 淺析Java 并發(fā)編程中的synchronized

    淺析Java 并發(fā)編程中的synchronized

    這篇文章主要介紹了Java 并發(fā)編程中的synchronized的相關(guān)資料,幫助大家更好的理解和學(xué)習(xí)Java并發(fā)編程,感興趣的朋友可以了解下
    2020-12-12
  • java 較大數(shù)據(jù)量取差集,list.removeAll性能優(yōu)化詳解

    java 較大數(shù)據(jù)量取差集,list.removeAll性能優(yōu)化詳解

    這篇文章主要介紹了java 較大數(shù)據(jù)量取差集,list.removeAll性能優(yōu)化詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2020-09-09
  • java中return語句的幾種用法舉例

    java中return語句的幾種用法舉例

    這篇文章主要介紹了Java中return語句的使用,包括有返回值類型的方法、提前返回、返回對象以及方法返回類型為void的情況,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2025-01-01
  • java分割文本字符串的方法

    java分割文本字符串的方法

    這篇文章主要為大家詳細(xì)介紹了java分割文本字符串的方法,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2017-06-06
  • Mybatis如何根據(jù)List批量查詢List結(jié)果

    Mybatis如何根據(jù)List批量查詢List結(jié)果

    這篇文章主要介紹了Mybatis如何根據(jù)List批量查詢List結(jié)果,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-03-03
  • java中循環(huán)刪除list中元素的方法總結(jié)

    java中循環(huán)刪除list中元素的方法總結(jié)

    下面小編就為大家?guī)硪黄猨ava中循環(huán)刪除list中元素的方法總結(jié)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2016-12-12
  • Java 用Prometheus搭建實時監(jiān)控系統(tǒng)過程詳解

    Java 用Prometheus搭建實時監(jiān)控系統(tǒng)過程詳解

    這篇文章主要介紹了用Prometheus搭建實時監(jiān)控系統(tǒng)過程詳解之上帝之火,普羅米修斯的崛起,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-07-07
  • 解決SpringBoot 測試類無法自動注入@Autowired的問題

    解決SpringBoot 測試類無法自動注入@Autowired的問題

    這篇文章主要介紹了解決SpringBoot 測試類無法自動注入@Autowired的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-03-03
  • Java多線程下載的實現(xiàn)方法

    Java多線程下載的實現(xiàn)方法

    復(fù)習(xí)多線程的時候,練習(xí)了下,順便記錄一下:
    2013-03-03
  • Java內(nèi)存模型JMM詳解

    Java內(nèi)存模型JMM詳解

    這篇文章主要介紹了Java內(nèi)存模型JMM詳解,涉及volatile和監(jiān)視器鎖,final字段,內(nèi)存屏障等相關(guān)內(nèi)容,具有一定參考價值,需要的朋友可以了解下。
    2017-11-11

最新評論

日照市| 台湾省| 镇雄县| 阿拉尔市| 桐庐县| 三明市| 县级市| 吉安市| 徐汇区| 宜兰县| 色达县| 枝江市| 富阳市| 城步| 涿州市| 甘肃省| 浠水县| 云和县| 芜湖县| 碌曲县| 封丘县| 红桥区| 新津县| 延寿县| 阿尔山市| 白银市| 德保县| 西华县| 唐海县| 宁都县| 方山县| 武城县| 安多县| 育儿| 石狮市| 榕江县| 河北区| 桦川县| 广德县| 洞口县| 从化市|