Spring AOP 是 Java 面試的必考點,咱們須要瞭解 AOP 的基本概念及原理。那麼 Spring AOP 究竟是啥,爲何面試官這麼喜歡問它呢?本文先介紹 AOP 的基本概念,而後根據 AOP 原理,實現一個接口返回統一格式的小示例,方便你們理解 Spring AOP 到底如何用!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 理解:
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";
}複製代碼
上述代碼內有些概念須要解釋說明:
到這裏,一個完整的自定義註解就定義完成了。
@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 :
本文參考連接:
------