SpringBoot(六)--- AOP、ThreadLocal

目錄

前言

一、AOP基礎

1.入門程序

2. AOP核心概念

3. 底層原理

二、AOP進階

1.通知類型

抽取切入點

2. 切入點表達式

2.1 execution

2.2 @annoation

2.3 連接點詳解

三、ThreadLocal


前言

AOP(面向切面編程),面向切面編程實際就是面向特定方法編程

假如有一個項目,現在想知道這個項目中每個業務功能執行的市場,以便對執行時長比較長的功能進行優化。

首先想到的應該就是在每個方法執行前加一個System.currentTimeMillis()方法來記錄時間,執行完后,再次記錄時間。

但是如果業務功能很多,這樣操作未免太過于冗余。利用AOP,只需要單獨定義一段代碼,就可以計算出所有業務功能所執行的時間。

所以,AOP的優勢主要體現在以下四個方面:

  • 減少重復代碼:不需要在業務方法中定義大量的重復性的代碼,只需要將重復性的代碼抽取到AOP程序中即可。

  • 代碼無侵入:在基于AOP實現這些業務功能時,對原有的業務代碼是沒有任何侵入的,不需要修改任何的業務代碼。

  • 提高開發效率

  • 維護方便

一、AOP基礎

1.入門程序

@Component
@Aspect //當前類為切面類
@Slf4j
public class RecordTimeAspect {// Around注解中的屬性表示會攔截com.itheima.service.impl這個包下DeptServiceImpl類中的所有方法@Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {// 記錄方法執行開始時間long begin = System.currentTimeMillis();// 執行原始方法// 由于Around表示方法執行前后都會執行,所以被攔截的方法執行結束后,就會執行下面的代碼。Object result = pjp.proceed();//記錄方法執行結束時間long end = System.currentTimeMillis();//計算方法執行耗時log .info("方法執行耗時: {}毫秒",end-begin);return result;}
}

通過AOP入門程序完成了業務方法執行耗時的統計,那其實AOP的功能遠不止于此,常見的應用場景如下:

  • 記錄系統的操作日志

  • 權限控制

  • 事務管理:我們前面所講解的Spring事務管理,底層其實也是通過AOP來實現的,只要添加@Transactional注解之后,AOP程序自動會在原始方法運行前先來開啟事務,在原始方法運行完畢之后提交或回滾事務。

2. AOP核心概念

連接點:JoinPoint,可以被AOP控制的方法(不僅僅指當前被AOP控制的方法,也包含當前沒有被AOP控制,但是可以被控制的方法)。

通知:Advice,指那些重復邏輯,也就是共性功能(最終體現為一個方法),例如計算每個業務執行的時間。

切入點:PointCut,匹配連接點的條件,通知僅會在切入點方法執行時被應用,可以簡單理解為被AOP控制的方法。

切面:Aspect,描述通知與切入點的對應關系(通知+切入點)。被@Aspect直接所修飾的類被稱為切面類。

目標對象:Target,通知所應用的對象,目標對象指的就是通知所應用的對象,稱之為目標對象。

3. 底層原理

Spring的AOP底層是基于動態代理技術來實現的,也就是說在程序運行的時候,會自動的基于動態代理技術為目標對象生成一個對應的代理對象。在代理對象當中就會對目標對象當中的原始方法進行功能的增強。

如下圖所示,切面類在執行原方法的時候,并不會直接去調用目標對象中的方法。而是創建一個代理對象DeptServiceProxy繼承DeptService,在代理對象中執行目標對象的方法。在前端發起請求的時候,Spring通過IOC容器注入的也是這個代理對象。

就像把目標方法看作“核心業務”(制作手機),動態代理就是自動套在業務外的“流水線外殼”(代理商),在制作手機前自動貼標簽(前置日志),制作后自動打包(后置日志),而手機工廠無需修改任何代碼。如下圖,在執行方法之前,計算時間;執行結束之后,計算時間,而Controller層執行代理商的代碼即可。

二、AOP進階

1.通知類型

AOP的通知類型有以下幾種:

現有如下代碼,通過代碼來直觀地感受不同通知類型的執行順序:

@Slf4j
@Component
@Aspect
public class MyAspect1 {//前置通知@Before("execution(* com.itheima.service.*.*(..))")public void before(JoinPoint joinPoint){log.info("before ...");}//環繞通知@Around("execution(* com.itheima.service.*.*(..))")public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {log.info("around before ...");//調用目標對象的原始方法執行Object result = proceedingJoinPoint.proceed();//原始方法如果執行時有異常,環繞通知中的后置代碼不會在執行了log.info("around after ...");return result;}//后置通知@After("execution(* com.itheima.service.*.*(..))")public void after(JoinPoint joinPoint){log.info("after ...");}//返回后通知(程序在正常執行的情況下,會執行的后置通知)@AfterReturning("execution(* com.itheima.service.*.*(..))")public void afterReturning(JoinPoint joinPoint){log.info("afterReturning ...");}//異常通知(程序在出現異常的情況下,執行的后置通知)@AfterThrowing("execution(* com.itheima.service.*.*(..))")public void afterThrowing(JoinPoint joinPoint){log.info("afterThrowing ...");}
}

隨便執行一個業務程序,在程序沒有發生異常的情況下,@AfterThrowing標識的通知方法不會執行。

可以發現Around先執行,然后Before執行,因為這兩個都是在原始方法執行之前執行的。原始方法執行完畢之后,AfterReturning執行,接著是After,最后是Around。因為@Around是環繞通知,在原始方法執行前后都會執行。

如果程序出現異常,結果如下,@Around環繞通知中原始方法調用時有異常,通知中的環繞后的代碼邏輯也不會在執行了 (因為原始方法調用已經出異常了):

抽取切入點

在上面的代碼中,可以發現每一個通知都有相同的切入點表達式。假如此時切入點表達式需要變動,就需要將所有的切入點表達式一個一個的來改動,就變得非常繁瑣了。因此我們需要將相同的切入點表達式抽取出來。

Spring提供了@PointCut注解,該注解的作用是將公共的切入點表達式抽取出來,需要用到時引用該切入點表達式即可。

將切入點表達式抽取出來,在通知中就直接引入切入點函數名即可。

@Slf4j
@Component
@Aspect
public class MyAspect1 {//切入點方法(公共的切入點表達式)@Pointcut("execution(* com.itheima.service.*.*(..))")private void pt(){}//前置通知(引用切入點)@Before("pt()")public void before(JoinPoint joinPoint){log.info("before ...");}//環繞通知@Around("pt()")public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {log.info("around before ...");//調用目標對象的原始方法執行Object result = proceedingJoinPoint.proceed();//原始方法在執行時:發生異常//后續代碼不在執行log.info("around after ...");return result;}//后置通知@After("pt()")public void after(JoinPoint joinPoint){log.info("after ...");}//返回后通知(程序在正常執行的情況下,會執行的后置通知)@AfterReturning("pt()")public void afterReturning(JoinPoint joinPoint){log.info("afterReturning ...");}//異常通知(程序在出現異常的情況下,執行的后置通知)@AfterThrowing("pt()")public void afterThrowing(JoinPoint joinPoint){log.info("afterThrowing ...");}
}

如果外部其他切面類也想使用這個切入點,就需要把private改為public,而在引用的時候,具體的語法為:

@Slf4j
@Component
@Aspect
public class MyAspect2 {//引用MyAspect1切面類中的切入點表達式//必須是切入點的全類名@Before("com.itheima.aspect.MyAspect1.pt()")public void before(){log.info("MyAspect2 -> before ...");}
}

2. 切入點表達式

切入點表達式分為兩種:execution(……):根據方法的簽名來匹配;@annotation(……) :根據注解匹配。

2.1 execution

execution主要根據方法的返回值、包名、類名、方法名、方法參數等信息來匹配,語法為:

execution(訪問修飾符?  返回值  包名.類名.?方法名(方法參數) throws 異常?)

其中帶?號的部分是可以省略的。

主要理解下面這個表達式即可:

第一個*號指的是任意的訪問修飾符,com.itheima.service.impl指的是包名,DeptServiceImpl指的是這個包中的一個類,類名是DeptServiceImpl,delete是這個類中的一個方法,后面的*號指的是delete方法中的所有參數。

execution(* com.itheima.service.impl.DeptServiceImpl.delete(*))

如果想要com.itheima.service.impl包下的所有類,可以改為如下代碼:

execution(* com.itheima.service.impl.DeptServiceImpl.*(..))

注意事項:

  • 根據業務需要,可以使用 且(&&)、或(||)、非(!) 來組合比較復雜的切入點表達式。

execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..))

2.2 @annoation

使用execution,當方法過多的時候,代碼就會很繁雜。此時就可以借助另一種切入點表達式 @annotation 來描述這一類的切入點,從而來簡化切入點表達式的書寫。

實現步驟:

  1. 編寫自定義注解

  2. 在業務類要做為連接點的方法上添加自定義注解

自定義注解LogOperation

// Target注解指定注解的作用目標 
// Retention指定注解的生命周期(在運行時有效)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation{
}

業務類DeptServiceImpl

@Slf4j
@Service
public class DeptServiceImpl implements DeptService {@Autowiredprivate DeptMapper deptMapper;@Override@LogOperation //自定義注解(表示:當前方法屬于目標方法)public List<Dept> list() {List<Dept> deptList = deptMapper.list();//模擬異常//int num = 10/0;return deptList;}@Override@LogOperation //自定義注解(表示:當前方法屬于目標方法)public void delete(Integer id) {//1. 刪除部門deptMapper.delete(id);}@Override   // 沒有自定義注解,表示當前方法不屬于目標方法public void save(Dept dept) {dept.setCreateTime(LocalDateTime.now());dept.setUpdateTime(LocalDateTime.now());deptMapper.save(dept);}}

切面類

@Slf4j
@Component
@Aspect
public class MyAspect6 {//針對list方法、delete方法進行前置通知和后置通知//前置通知//括號里是自定義注解的全類名@Before("@annotation(com.itheima.anno.LogOperation)")public void before(){log.info("MyAspect6 -> before ...");}//后置通知@After("@annotation(com.itheima.anno.LogOperation)")public void after(){log.info("MyAspect6 -> after ...");}
}

上述就是@annoation的基本用法。

總結:

  • execution切入點表達式

    • 根據我們所指定的方法的描述信息來匹配切入點方法,這種方式也是最為常用的一種方式

    • 如果我們要匹配的切入點方法的方法名不規則,或者有一些比較特殊的需求,通過execution切入點表達式描述比較繁瑣

  • annotation 切入點表達式

    • 基于注解的方式來匹配切入點方法。這種方式雖然多一步操作,我們需要自定義一個注解,但是相對來比較靈活。我們需要匹配哪個方法,就在方法上加上對應的注解就可以了

2.3 連接點詳解

在Spring中用JoinPoint抽象了連接點,用它可以獲得方法執行時的相關信息,如目標類名、方法名、方法參數等。

  • 對于@Around通知,獲取連接點信息只能使用ProceedingJoinPoint類型

  • 對于其他四種通知,獲取連接點信息只能使用JoinPoint,它是ProceedingJoinPoint的父類型

三、ThreadLocal

當在做業務代碼的時候,存在以下問題:

  • 員工登錄成功后,哪里存儲的有當前登錄員工的信息? 給客戶端瀏覽器下發的jwt令牌中

  • 如何從JWT令牌中獲取當前登錄用戶的信息呢? 獲取請求頭中傳遞的jwt令牌,并解析

  • TokenFilter 中已經解析了令牌的信息,如何傳遞給AOP程序、Controller、Service呢?ThreadLocal

ThreadLocal 是 Java 中用于實現線程局部變量的核心類,它通過為每個線程創建獨立的變量副本,解決多線程環境下共享變量的并發問題。ThreadLocal 并不是一個Thread,而是Thread的局部變量,為每個線程提供一份單獨的存儲空間,具有線程隔離的效果,不同的線程之間不會相互干擾。

那么如何操作ThreadLocal呢?

首先定義一個ThreadLocal操作的工具類,用于操作當前登錄員工ID。

package com.itheima.utils;public class CurrentHolder {private static final ThreadLocal<Integer> CURRENT_LOCAL = new ThreadLocal<>();public static void setCurrentId(Integer employeeId) {CURRENT_LOCAL.set(employeeId);}public static Integer getCurrentId() {return CURRENT_LOCAL.get();}public static void remove() {CURRENT_LOCAL.remove();}
}

然后我們在解析JWT令牌的時候,將登入員工的id存入ThreadLocal中,

        //5. 如果token不為空, 調用JWtUtils工具類的方法解析token, 如果解析失敗, 響應401狀態碼try {Claims claims = JwtUtils.parseJWT(token);Integer empId = Integer.valueOf(claims.get("id").toString());// 在這里,將用戶ID存入線程空間中CurrentHolder.setCurrentId(empId);log.info("token解析成功, 放行");} catch (Exception e) {log.info("token解析失敗, 響應401狀態碼");response.setStatus(401);return;}

然后我們就可以在任何地方取出這次線程執行的員工ID

    // 示例方法,獲取當前用戶IDprivate int getCurrentUserId() {return CurrentHolder.getCurrentId();}

在同一個線程/同一個請求中,進行數據共享就可以使用 ThreadLocal

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/83407.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/83407.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/83407.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【深度學習】 19. 生成模型:Diffusion Models

Diffusion Models Diffusion Models 簡介 Diffusion 模型是一類通過逐步添加噪聲并再逆向還原的方式進行圖像生成的深度生成模型。其基本流程包括&#xff1a; 前向過程&#xff08;Forward Process&#xff09;&#xff1a;將真實圖像逐步加噪&#xff0c;最終變為高斯噪聲…

Y1——鏈式前向星

知識點 模版——鏈表的前插法 head表示頭結點的下標 ver[i]表示結點i 的值 tot存儲當前已經用到了哪個 add用于將x插到頭結點 int head1; intt ver[N],Next[N]; int ttot-1; void add(int x){ver[tot]x;Next[tot]head;headtot; } 常見的鏈式前向星三種實現形式&#xff…

如何排查Redis單個Key命中率驟降?

問題現象 Redis整體命中率98%&#xff0c;但監控發現特定Key&#xff08;如user:1000:profile&#xff09;的命中率從99%驟降至40%&#xff0c;引發服務延遲上升。 排查步驟 1. 確認現象與定位Key // 通過Redis監控工具獲取Key指標 public void monitorKey(String key) {Je…

自定義Shell命令行解釋器

目錄 1、目標 2、顯示命令提示符 2.1 getenv 2.2 getcwd 2.3 putenv 3、獲取用戶輸入的命令 4、解析命令 5、處理內建命令 6、處理外部命令 7、完整代碼 7.1 myshell.cpp 7.2 Makefile 1、目標 實現一個Linux的myshell&#xff0c;有以下基本的功能。 顯示命令提示…

Laplace 噪聲

Laplace 噪聲是一種特定概率分布&#xff08;拉普拉斯分布&#xff09;產生的隨機擾動。它是差分隱私&#xff08;Differential Privacy, DP&#xff09;中最核心、最常用的噪聲機制之一。它的核心作用是在不泄露個體信息的前提下&#xff0c;允許從包含敏感數據的數據庫中提取…

基于空天地一體化網絡的通信系統matlab性能分析

目錄 1.引言 2.算法仿真效果演示 3.數據集格式或算法參數簡介 4.MATLAB核心程序 5.算法涉及理論知識概要 5.1 QPSK調制原理 5.2 空天地一體化網絡信道模型 5.3 空天地一體化網絡信道特性 6.參考文獻 7.完整算法代碼文件獲得 1.引言 空天地一體化網絡是一種將衛星通信…

【Delphi】接收windows文件夾中文件拖拽

本文根據EmailX45的視頻文件&#xff0c;進行了優化改進&#xff0c;原文參見&#xff1a;Delphi: Drag and Drop Files from Explorer into TPanel / TMemo - YouTube 在Windows中&#xff0c;如果將選擇的文件拖動到Delphi程序的控件上&#xff0c;有很多實現方法&#xff0c…

基于熱力學熵增原理的EM-GAN

簡介 簡介:提出基于熱力學熵增原理的EM-GAN,通過生成器熵最大化約束增強輸出多樣性。引入熵敏感激活函數與特征空間熵計算模塊,在MNIST/CelebA等數據集上實現FID分數提升23.6%,有效緩解模式崩潰問題。 論文題目:Entropy-Maximized Generative Adversarial Network (EM-G…

HashMap與ConcurrentHashMap詳解:實現原理、源碼分析與最佳實踐

引言 在Java編程中&#xff0c;集合框架是最常用的工具之一&#xff0c;而HashMap和ConcurrentHashMap則是其中使用頻率最高的兩個Map實現。它們都用于存儲鍵值對數據&#xff0c;但在實現機制、性能特點和適用場景上有著顯著差異。 HashMap作為單線程環境下的首選Map實現&am…

CSS之動畫(奔跑的熊、兩面反轉盒子、3D導航欄、旋轉木馬)

一、 2D轉換 1.1 transform: translate( ) 轉換&#xff08;transform&#xff09; 是CSS3中具有顛覆性的特征之一&#xff0c;可以實現元素的位移、旋轉、縮放等效果 移動&#xff1a;translate 旋轉&#xff1a;rotate 縮放&#xff1a;scale 下圖為2D轉換的坐標系 回憶…

【筆記】在 MSYS2(MINGW64)中安裝 python-maturin 的記錄

#工作記錄 &#x1f4cc; 安裝背景 操作系統&#xff1a;MSYS2 MINGW64當前時間&#xff1a;2025年6月1日Python 版本&#xff1a;3.12&#xff08;通過 pacman 安裝&#xff09;目標工具&#xff1a;maturin —— 用于構建和發布 Rust 編寫的 Python 包 &#x1f6e0;? 安裝…

基于微信小程序的垃圾分類系統

博主介紹&#xff1a;java高級開發&#xff0c;從事互聯網行業六年&#xff0c;熟悉各種主流語言&#xff0c;精通java、python、php、爬蟲、web開發&#xff0c;已經做了六年的畢業設計程序開發&#xff0c;開發過上千套畢業設計程序&#xff0c;沒有什么華麗的語言&#xff0…

工作日記之權限校驗-token的實戰案例

背景說明 我們組負責維護的一個系統&#xff0c;前端界面掛載在其他兩個系統上&#xff0c;因為歷史遺留原因&#xff0c;同時也掛在公網上&#xff0c;沒有登陸功能和用戶體系&#xff0c;只要輸入網址就能訪問&#xff0c;雖然這個系統是給公司內部人員使用&#xff0c;但是…

mysql雙主模式下基于keepalived的虛擬ip實現高可用模式搭建

數據庫安裝和升級和雙主配置的操作可以參考我的另一篇文章&#xff1a; 數據庫安裝和升級和雙主配置 1、在兩臺服務器都下載和安裝keepalived 下載&#xff1a; yumdownloader --resolve keepalived 下載后得到&#xff1a; [rootlocalhost keepalivedRpm]# ll 總用量 1896 …

展會聚焦丨漫途科技亮相2025西北水務博覽會!

2025第三屆西北水務數字化發展論壇暨供排水節水灌溉新技術設備博覽會在蘭州甘肅國際會展中心圓滿落幕。本屆展會以“科技賦能水資源&#xff0c;數智引領新動能”為主題&#xff0c;活動匯集水務集團、科研院所、技術供應商等全產業鏈參與者&#xff0c;旨在通過前沿技術展示與…

單調棧(打卡)

本篇基于b站靈茶山艾府。 下面是靈神上課講解的題目與課后作業&#xff0c;課后作業還有三道實在寫不下去了&#xff0c;下次再寫。 739. 每日溫度 給定一個整數數組 temperatures &#xff0c;表示每天的溫度&#xff0c;返回一個數組 answer &#xff0c;其中 answer[i] 是…

【機器學習基礎】機器學習入門核心算法:層次聚類算法(AGNES算法和 DIANA算法)

機器學習入門核心算法&#xff1a;層次聚類算法&#xff08;AGNES算法和 DIANA算法&#xff09; 一、算法邏輯二、算法原理與數學推導1. 距離度量2. 簇間距離計算&#xff08;連接標準&#xff09;3. 算法偽代碼&#xff08;凝聚式&#xff09; 三、模型評估1. 內部評估指標2. …

已有的前端項目打包到tauri運行(windows)

1.打包前端項目產生靜態html、css、js 我們接下來用vue3 vite編寫一個番茄鐘案例來演示。 我們執行npm run build 命令產生的dist目錄下的靜態文件。 2.創建tarui項目 npm create tauri-applatest一路回車&#xff0c;直到出現。 3.啟動運行 我們將打包產生的dist目錄下的…

Unity3D仿星露谷物語開發55之保存地面屬性到文件

1、目標 將游戲保存到文件&#xff0c;并從文件中加載游戲。 Player在游戲中種植的Crop&#xff0c;我們希望保存到文件中&#xff0c;當游戲重新加載時Crop的GridProperty數據仍然存在。這次主要實現保存地面屬性&#xff08;GridProperties&#xff09;信息。 我們要做的是…

Java面試:企業協同SaaS中的技術挑戰與解決方案

Java面試&#xff1a;企業協同SaaS中的技術挑戰與解決方案 面試場景 在一家知名互聯網大廠&#xff0c;面試官老王正在對一位應聘企業協同SaaS開發職位的程序員謝飛機進行技術面試。 第一輪提問&#xff1a;基礎技術 老王&#xff1a;謝飛機&#xff0c;你好。首先&#xf…