Spring AOP 是 Java 面試的必考點,咱們須要瞭解 AOP 的基本概念及原理。那麼 Spring AOP 究竟是啥,爲何面試官這麼喜歡問它呢?本文先介紹 AOP 的基本概念,而後根據 AOP 原理,實現一個接口返回統一格式的小示例,方便你們理解 Spring AOP 到底如何用!
<!--more-->html
在實際的開發過程當中,咱們的應用程序會被分爲不少層。一般來說一個 Java 的 Web 程序會擁有如下幾個層次:前端
雖然看起來每一層都作着全然不一樣的事情,可是實際上總會有一些相似的代碼,好比日誌打印和異常處理等。若是咱們選擇在每一層都獨立編寫這部分代碼,那麼長此以往代碼將變的很難維護。因此咱們提供了另外的一種解決方案:AOP。這樣能夠保證這些通用的代碼被聚合在一塊兒維護,並且咱們能夠靈活的選擇何處須要使用這些代碼。java
AOP(Aspect Oriented Programming,面向切面編程),能夠說是 OOP(Object Oriented Programing,面向對象編程)的補充和完善。OOP 引入封裝、繼承和多態性等概念來創建一種對象層次結構,用來模擬公共行爲的一個集合。當咱們須要爲分散的對象引入公共行爲的時候,OOP則顯得無能爲力。也就是說,OOP 容許你定義從上到下的關係,但並不適合定義從左到右的關係。例如日誌功能,日誌代碼每每水平地散佈在全部對象層次中,而與它所散佈到的對象的核心功能毫無關係。對於其餘類型的代碼,如權限管理、異常處理等也是如此。這種散佈在各處的無關的代碼被稱爲橫切(cross-cutting)代碼,在 OOP 設計中,它致使了大量代碼的重複,而不利於各個模塊的重用。git
而 AOP 技術則偏偏相反,它利用一種稱爲 「橫切」 的技術,剖解開封裝的對象內部,並將那些影響了多個類的公共行爲封裝到一個可重用模塊,並將其名爲 「Aspect」 ,即切面。所謂「切面」,簡單地說,就是將權限、事務、日誌、異常等與業務邏輯相對獨立的功能抽取封裝,便於減小系統的重複代碼,下降模塊間的耦合度,增長代碼的可維護性。AOP 表明的是一個橫向的關係,若是說 「對象」 是一個空心的圓柱體,其中封裝的是對象的屬性和行爲;那麼面向切面編程,就彷彿一把利刃,將這些空心圓柱體剖開,以得到其內部的消息,而後又以巧奪天功的妙手將這些剖開的切面復原,不留痕跡。github
切面理解:用刀將西瓜分紅兩瓣,切開的切口就是切面;炒菜、鍋與爐子共同來完成炒菜,鍋與爐子就是切面。Web 層級設計中,Controller 層、Service 層、Dao 層,每一層之間也是一個切面。編程中,對象與對象之間,方法與方法之間,模塊與模塊之間都是一個個切面。web
推薦網上的一篇通俗易懂的 AOP 理解:https://blog.csdn.net/qukaiwe... 。面試
AOP有不少專業術語,初看這麼多術語,可能一會兒不大理解,多讀幾遍,相信很快就會搞懂。spring
就是 Spring 容許你放通知(Advice)的地方,不少,基本每一個方法的前、後(二者都有也行)或拋出異常時均可以是鏈接點,Spring 只支持方法鏈接點,和方法有關的前先後後都是鏈接點。數據庫
Tips:可使用鏈接點獲取執行的類名、方法名和參數名等。編程
是在鏈接點的基礎上來定義切入點。好比在一個類中,有 15 個方法,那麼就會有幾十個鏈接點,但只想讓其中幾個方法的先後或拋出異常時乾點什麼,那麼就用切入點來定義這幾個方法,讓切入點來篩選鏈接點。
是通知(Advice)和切入點(Pointcut)的結合,通知(Advice)說明了幹什麼和何時(經過@Before、@Around、@After、@AfterReturning、@AfterThrowing來定義執行時間點)幹,切入點(Pointcut)說明了在哪(指定方法)幹,這就是一個完整的切面定義。
AOP Proxy:AOP 框架建立的對象,代理就是目標對象的增強。AOP 巧妙的例用動態代理優雅的解決了 OOP 力所不及的問題。Spring 中的 AOP 代理能夠是 jdk 動態代理,也能夠是 cglib 動態代理。前者基於接口,後者基於子類。
讀完上面這麼多抽象概念,若是不來一個 AOP 具體示例,吸取效果或者理解深度可能不是那麼好。因此,請接着往下看:
import lombok.Data; @Data public class Result<T> { // code 狀態值:0 表明成功,其餘數值表明失敗 private Integer code; // msg 返回信息。若是code爲0,msg則爲success;若是code爲1,msg則爲error private String msg; // data 返回結果集,使用泛型兼容不一樣的類型 private T data; }
import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; @AllArgsConstructor @NoArgsConstructor public enum ExceptionEnum { UNKNOW_ERROR(-1, "未知錯誤"), NULL_EXCEPTION(-2, "空指針異常:NullPointerException"), INVALID_EXCEPTION(1146, "無效的數據訪問資源使用異常:InvalidDataAccessResourceUsageException"); public Integer code; public String msg; }
//@ControllerAdvice @Component @Slf4j public class ExceptionHandle { // @ExceptionHandler(value = Exception.class) // @ResponseBody public Result exceptionGet(Throwable t) { log.error("異常信息:", t); if (t instanceof InvalidDataAccessResourceUsageException) { return ResultUtil.error(ExceptionEnum.INVALID_EXCEPTION); } else if (t instanceof NullPointerException) { return ResultUtil.error(ExceptionEnum.NULL_EXCEPTION); } return ResultUtil.error(ExceptionEnum.UNKNOW_ERROR); } }
製做一個結果返回工具類:
public class ResultUtil { /** * @return com.study.spring.entity.Result * @description 接口調用成功返回的數據格式 * @param: object */ public static Result success(Object object) { Result result = new Result(); result.setCode(0); result.setMsg("success"); result.setData(object); return result; } /** * @return com.study.spring.entity.Result * @description 接口調用失敗返回的數據格式 * @param: code * @param: msg */ public static Result error(Integer code, String msg) { Result result = new Result(); result.setCode(code); result.setMsg(msg); result.setData(null); return result; } /** * 返回異常信息,在已知的範圍內 * * @param exceptionEnum * @return */ public static Result error(ExceptionEnum exceptionEnum) { Result result = new Result(); result.setCode(exceptionEnum.code); result.setMsg(exceptionEnum.msg); result.setData(null); return result; } }
必需要添加 spring aop 等相關依賴:
<!-- web 依賴--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- aop 依賴 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <!-- 用於日誌切面中,以 json 格式打印出入參 --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>2.8.5</version> </dependency> <!-- lombok 簡化代碼--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface HandleResult { String desc() default "create17"; }
上述代碼內有些概念須要解釋說明:
@Retention:定義註解的保留策略
@Target:定義註解的做用目標,可多個,用逗號分隔。
到這裏,一個完整的自定義註解就定義完成了。
@Aspect @Component @Slf4j @Order(100) public class HandleResultAspect { ... }
使用 @Pointcut 來定義一個切點。
@Pointcut("@annotation(com.study.spring.annotation.HandleResult)") // @Pointcut("execution(* com.study.spring.controller..*.*(..))") public void HandleResult() { }
對於 execution 表達式,官網對 execution 表達式的介紹爲:
execution(<修飾符模式>?<返回類型模式><方法名模式>(<參數模式>)<異常模式>?)
除了返回類型模式、方法名模式和參數模式外,其它項都是可選的。這個解釋可能有點難理解,下面咱們經過一個具體的例子來了解一下。在 HandleResultAspect 中咱們定義了一個切點,其 execution 表達式爲:* com.study.spring.controller..*.*(..))
,下表爲該表達式比較通俗的解析:
標識符 | 含義 |
---|---|
execution() |
表達式的主體 |
第一個 * 符號 |
表示返回值的類型,* 表明全部返回類型 |
com.study.spring.controller |
AOP 所切的服務的包名,即須要進行橫切的業務類 |
包名後面的 .. |
表示當前包及子包 |
第二個 * |
表示類名,* 表示全部類 |
最後的 .*(..) |
第一個 . 表示任何方法名,括號內爲參數類型,.. 表明任何類型參數 |
上述的 execution 表達式是把 com.study.spring.controller 下全部的方法看成一個切點。@Pointcut 除了可使用 execution 表達式以外,還可用 @annotation 來指定註解切入,好比可指定上面建立的自定義註解 @HandleResult ,@HandleResult 在哪裏被使用,哪裏就是一個切點。
@Before(value = "HandleResult() && @annotation(t)", argNames = "joinPoint,t") public void doBefore(JoinPoint joinPoint, HandleResult t) throws Exception { // 類名 String className = joinPoint.getTarget().getClass().getName(); // 方法名 String methodName = joinPoint.getSignature().getName(); // 參數名 Object[] args = joinPoint.getArgs(); StringBuilder sb = new StringBuilder(); if (args != null && args.length > 0) { for (Object arg : args) { sb.append(arg).append(", "); } } log.info("接口 {} 開始被調用, 類名: {}, 方法名: {}, 參數名爲: {} .", t.desc(), className, methodName, sb.toString()); }
@Around("HandleResult()") public Result doAround(ProceedingJoinPoint point) { long startTime = System.currentTimeMillis(); log.info("---HandleResultAspect--Around的前半部分----------------------------"); Object result; try { // 執行切點。point.proceed 爲方法返回值 result = point.proceed(); // 打印出參 log.info("接口原輸出內容: {}", new Gson().toJson(result)); // 執行耗時 log.info("執行耗時:{} ms", System.currentTimeMillis() - startTime); return ResultUtil.success(result); } catch (Throwable throwable) { return exceptionHandle.exceptionGet(throwable); } }
@After("HandleResult()") public void doAfter() { log.info("doAfter..."); }
returning 可接收接口最終地返回信息。
@AfterReturning(pointcut = "@annotation(t)", returning = "res") public void afterReturn(HandleResult t, Object res) { log.info("接口 {} 被調用已結束, 最終返回結果爲: {} .", t.desc(), new Gson().toJson(res)); }
throwing 可用來獲取異常信息。
@AfterThrowing(throwing = "throwable", pointcut = "HandleResult()") public void afterThrowing(Throwable throwable) { log.info("After throwing...", throwable); }
關於這些通知的執行順序以下圖所示:
如下爲切面實現的所有代碼:
@Aspect @Component @Slf4j @Order(100) public class HandleResultAspect { @Autowired private ExceptionHandle exceptionHandle; /** * @return void * @description 定義切點 */ @Pointcut("@annotation(com.study.spring.annotation.HandleResult)") // @Pointcut("execution(* com.study.spring.controller..*.*(..))") public void HandleResult() { } /** * @return void * @description 打印接口名、類名、方法名及參數名 * @param: joinPoint * @param: t */ @Before(value = "@annotation(t)", argNames = "joinPoint,t") public void doBefore(JoinPoint joinPoint, HandleResult t) throws Exception { // 類名 String className = joinPoint.getTarget().getClass().getName(); // 方法名 String methodName = joinPoint.getSignature().getName(); // 參數名 Object[] args = joinPoint.getArgs(); StringBuilder sb = new StringBuilder(); if (args != null && args.length > 0) { for (Object arg : args) { sb.append(arg).append(", "); } } log.info("接口 {} 開始被調用, 類名: {}, 方法名: {}, 參數名爲: {} .", t.desc(), className, methodName, sb.toString()); } /** * @return java.lang.Object * @description 定義@Around環繞,用於什麼時候執行切點 * @param: proceedingJoinPoint */ @Around("HandleResult()") public Result doAround(ProceedingJoinPoint point) { long startTime = System.currentTimeMillis(); log.info("---HandleResultAspect--Around的前半部分----------------------------"); Object result; try { // 執行切點。point.proceed 爲方法返回值 result = point.proceed(); // 打印出參 log.info("接口原輸出內容: {}", new Gson().toJson(result)); // 執行耗時 log.info("執行耗時:{} ms", System.currentTimeMillis() - startTime); return ResultUtil.success(result); } catch (Throwable throwable) { return exceptionHandle.exceptionGet(throwable); } } /** * @return void * @description 程序不管正常仍是異常,均執行的方法 * @param: */ @After("HandleResult()") public void doAfter() { log.info("doAfter..."); } /** * @return void * @description 當程序運行正常,所執行的方法 * 以json格式打印接口執行結果 * @param: t * @param: res */ @AfterReturning(pointcut = "@annotation(t)", returning = "res") public void afterReturn(HandleResult t, Object res) { log.info("接口 {} 被調用已結束, 接口最終返回結果爲: {} .", t.desc(), new Gson().toJson(res)); } /** * @return void * @description 當程序運行異常,所執行的方法 * 可用來打印異常 * @param: throwable */ @AfterThrowing(throwing = "throwable", pointcut = "HandleResult()") public void afterThrowing(Throwable throwable) { log.info("After throwing...", throwable); } }
在生產中,咱們的項目可能不止一個切面,那麼在多切面的狀況下,如何指定切面的優先級呢?
咱們可使用 @Order(i) 註解來定義切面的優先級,i 值越小,優先級越高。
好比咱們再建立一個切面,代碼示例以下:
@Aspect @Component @Order(50) @Slf4j public class TestAspect2 { @Pointcut("@annotation(com.study.spring.annotation.HandleResult)") public void aa(){ } @Before("aa()") public void bb(JoinPoint joinPoint){ log.info("我是 TestAspect2 的 Before 方法..."); } @Around("aa()") public Object cc(ProceedingJoinPoint point){ log.info("我是 TestAspect2 的 Around 方法的前半部分..."); Object result = null; try { result = point.proceed(); } catch (Throwable throwable) { throwable.printStackTrace(); } log.info("我是 TestAspect2 的 Around 方法的後半部分..."); return result; } @After("aa()") public void doAfter() { log.info("我是 TestAspect2 的 After 方法..."); } @AfterReturning("aa()") public void afterReturn() { log.info("我是 TestAspect2 的 AfterReturning 方法..."); } @AfterThrowing("aa()") public void afterThrowing() { log.info("我是 TestAspect2 的 AfterThrowing 方法..."); } }
切面 TestAspect2 爲 @Order(50),以前的切面 HandleResultAspect 爲 Order(100)。測試接口返回的日誌以下圖所示:
總結一下規律就是:
也就是:先進後出的原則。爲了方便咱們理解,我畫了一個圖,以下圖所示:
通常在項目開發中,都會設置三個環境:開發、測試、生產。那麼若是我只想在 開發 和 測試 環境下使用某切面該怎麼辦呢?咱們只須要在指定的切面類上方加上註解 @Profile 就能夠了,以下所示:
這樣就指定了 HandleResultAspect 該切面只能在 dev(開發)環境、test(測試)環境下生效,prod(生產)環境不生效。固然,你須要建立相應的 application-${dev/test/prod}.yml 文件,最後在 application.yml 文件內指定 spring.profiles.active 屬性爲 dev 或 test 才能夠生效。
本文篇幅較長,但總算對 Spring AOP 有了一個簡單的瞭解。從 AOP 的起源到概念、使用場景,而後深刻了解其專業術語,利用 AOP 思想實現了示例,方便咱們本身理解。讀完這篇文章,相信你們能夠基本不懼面試官對這個知識點的考覈了!
本文所涉及的代碼已上傳至 github :
https://github.com/841809077/...
本文參考連接: