1.AOP介紹
AOP為Aspect Oriented Programming的縮寫,意為:面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點,也是Spring框架中的一個重要內容,是函數式編程的一種衍生范型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。
1.1 AOP通俗解釋
我們一般做活動的時候,一般對每一個接口都會做活動的有效性校驗(是否開始、是否結束等等)、以及這個接口是不是需要用戶登錄。
按照正常的邏輯,我們可以這么做。
這有個問題就是,有多少接口,就要多少次代碼copy。對於一個“懶人”,這是不可容忍的。好,提出一個公共方法,每個接口都來調用這個接口。這里有點切面的味道了。
同樣有個問題,我雖然不用每次都copy代碼了,但是,每個接口總得要調用這個方法吧。於是就有了切面的概念,我將方法注入到接口調用的某個地方(切點)。
1.2 AOP中的相關概念
- Aspect(切面): Aspect 聲明類似於 Java 中的類聲明,在 Aspect 中會包含着一些 Pointcut 以及相應的 Advice。
- Joint point(連接點):表示在程序中明確定義的點,典型的包括方法調用,對類成員的訪問以及異常處理程序塊的執行等等,它自身還可以嵌套其它 joint point。
- Pointcut(切點):表示一組 joint point,這些 joint point 或是通過邏輯關系組合起來,或是通過通配、正則表達式等方式集中起來,它定義了相應的 Advice 將要發生的地方。
- Advice(增強):Advice 定義了在 Pointcut 里面定義的程序點具體要做的操作,它通過 before、after 和 around 來區別是在每個 joint point 之前、之后還是代替執行的代碼。
- Target(目標對象):織入 Advice 的目標對象.。
- Weaving(織入):將 Aspect 和其他對象連接起來, 並創建 Adviced object 的過程
1.3 過濾器、攔截器、AOP功能區別
1.3.1 過濾器和攔截器的區別
從一個請求到返回數據的整個過程看,首先是請求進入Servlet,在Servlet中存在過濾器。對url進行攔截。后經過分發器后,會進入到spring容器,在請求尚未到達具體的controller時,會先經過攔截器的preHandle方法。方法通過后,進入業務層處理,返回實體,在進入postHandle方法。經過JSP后會進入afterCompletion方法,返回頁面ModleAndView.
1.3.2 AOP
面向切面攔截的是類的元數據(包、類、方法名、參數等),攔截是在controller層的類,接口,或者service層的類,方法的執行前后進行攔截。
相對於攔截器更加細致,而且非常靈活,攔截器只能針對URL做攔截,而AOP針對具體的代碼,能夠實現更加復雜的業務邏輯。
1.3.3 過濾器、攔截器、AOP使用場景
三者功能類似,但各有優勢,從過濾器–>攔截器–>切面,攔截規則越來越細致,執行順序依次是過濾器、攔截器、切面。一般情況下數據被過濾的時機越早對服務的性能影響越小,因此我們在編寫相對比較公用的代碼時,優先考慮過濾器,然后是攔截器,最后是aop。
比如權限校驗,一般情況下,所有的請求都需要做登陸校驗,此時就應該使用過濾器在最頂層做校驗;日志記錄,一般日志只會針對部分邏輯做日志記錄,而且牽扯到業務邏輯完成前后的日志記錄,因此使用過濾器不能細致地划分模塊,此時應該考慮攔截器,然而攔截器也是依據URL做規則匹配,因此相對來說不夠細致,因此我們會考慮到使用AOP實現,AOP可以針對代碼的方法級別做攔截,很適合日志功能.
1.3.4 AfterReturning
AfterReturning源碼:
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) public @interface AfterReturning { String value() default ""; String pointcut() default ""; String returning() default ""; String argNames() default ""; }
returning:自定義的變量,標識目標方法的返回值
pointcut/value:這兩個屬性的作用是一樣的,它們都屬於指定切入點對應的切入表達式。一樣既可以是已有的切入點,也可直接定義切入點表達式。當指定了pointcut屬性值后,value屬性值將會被覆蓋。
2.使用@Before, @AfterReturning自定義注解的AOP實際使用場景
2.1 引入Pom依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
2.2 自定義注解
/** * @Author lucky * @Date 2022/1/26 15:33 * 統計耗時 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface ExecuteTime { }
2.3 AOP綁定自定義注解。並在aop中實現處理邏輯
package com.ttbank.flep.core.aspect; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.core.NamedThreadLocal; import org.springframework.stereotype.Component; /** * @Author lucky * @Date 2022/1/26 15:35 */ @Aspect @Component @Slf4j public class ExecuteTimeAspect { private static final ThreadLocal<Long> beginTimeThreadLocal = new NamedThreadLocal<>("ThreadLocal beginTime"); //切點:決定用注解方式的方法切還是針對某個路徑下的所有類和方法進行切,方法必須是返回void類型 @Pointcut("@annotation(com.ttbank.flep.core.aspect.ExecuteTime)") private void logTimeCalculateCut(){ } /** * 前置通知 (在方法執行之前返回)用於攔截Controller層記錄用戶的操作的開始時間 * @param joinPoint 切點 */ @Before("logTimeCalculateCut()") public void doBefore(JoinPoint joinPoint) throws InterruptedException{ beginTimeThreadLocal.set(System.currentTimeMillis()); } /** * 后置通知(在方法執行之后並返回數據) 用於攔截Controller層無異常的操作 */ @AfterReturning(pointcut = "logTimeCalculateCut()",returning = "ret") public void after(Object ret){ Long executeTime=System.currentTimeMillis()-beginTimeThreadLocal.get(); log.info("執行時間為:"+executeTime); log.info("sleepTime:"+ret); } }
2.4 AOP的使用
用在接口層校驗登陸的角色是否有權限使用該接口
/** * <p> * 前端控制器 * </p> * * @author lucky * @since 2021-11-25 */ @RestController @RequestMapping("/student") @Slf4j public class StudentController { @Autowired private IStudentService studentService; @PostMapping("/getStudentName") @ExecuteTime public int getStudentName(){ log.info("studentName:lucky"); Random random=new Random(); int sleepTime = random.nextInt(10000); try { Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } return sleepTime; } }
postman測試:
控制台輸出:
3.@Around簡單使用示例——SpringAOP增強處理
3.1 @Around的作用
- 既可以在目標方法之前織入增強動作,也可以在執行目標方法之后織入增強動作;
- 可以決定目標方法在什么時候執行,如何執行,甚至可以完全阻止目標目標方法的執行;
- 可以改變執行目標方法的參數值,也可以改變執行目標方法之后的返回值; 當需要改變目標方法的返回值時,只能使用Around方法;
雖然Around功能強大,但通常需要在線程安全的環境下使用。因此,如果使用普通的Before、AfterReturing增強方法就可以解決的事情,就沒有必要使用Around增強處理了。
3.2 注解方式
如果需要對某一方法進行增強,只需要在相應的方法上添加上自定義注解即可
(1)定義注解類
package com.ttbank.flep.aspect; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * @Author lucky * @Date 2022/6/27 9:47 */ @Retention(RetentionPolicy.RUNTIME)//運行時有效 @Target(ElementType.METHOD)//作用於方法 public @interface MyAnnotation { String methodName () default ""; }
(2) 定義增強處理類(切面類)
package com.ttbank.flep.aspect; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; /** * @Author lucky * @Date 2022/6/27 9:44 */ @Aspect //標注增強處理類(切面類) @Component //交由Spring容器管理 public class AnnotationAspect { /* 可自定義切點位置,針對不同切點,方法上的@Around()可以這樣寫ex:@Around(value = "methodPointcut() && args(..)") @Pointcut(value = "@annotation(com.rq.aop.common.annotation.MyAnnotation)") public void methodPointcut(){} @Pointcut(value = "@annotation(com.rq.aop.common.annotation.MyAnnotation2)") public void methodPointcut2(){} */ //定義增強,pointcut連接點使用@annotation(xxx)進行定義 @Around(value = "@annotation(around)") //around 與 下面參數名around對應 public Object processAuthority(ProceedingJoinPoint point, MyAnnotation around) throws Throwable{ System.out.println("ANNOTATION welcome"); System.out.println("ANNOTATION 調用方法:"+ around.methodName()); System.out.println("ANNOTATION 調用類:" + point.getSignature().getDeclaringTypeName()); System.out.println("ANNOTATION 調用類名" + point.getSignature().getDeclaringType().getSimpleName()); Object result = point.proceed();//調用目標方法 System.out.println("ANNOTATION login success"); return result; } }
(3)Controller
package com.ttbank.flep.controller; import com.ttbank.flep.aspect.MyAnnotation; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; /** * @Author lucky * @Date 2022/6/27 9:49 */ @RestController @RequestMapping("/hello") public class HelloController { @PostMapping("/login") @MyAnnotation(methodName = "login") public void login(String name){ System.out.println("hello!"+name); } }
@RestController可以使得postman能夠接收返回值,回顯在postman頁面中;而@Controller並不能;
postman測試:
控制台輸出:
3.3 匹配方法執行連接點方式
@Aspect @Component @Order(0) //設置優先級,值越低優先級越高 public class ExecutionAspect { @Around(value = "execution(* com.ttbank.flep.controller..*.*(..))") public Object processAuthority (ProceedingJoinPoint point)throws Throwable{ System.out.println("EXECUTION welcome"); System.out.println("EXECUTION 調用方法:" + point.getSignature().getName()); System.out.println("EXECUTION 目標對象:" + point.getTarget()); //System.out.println("EXECUTION 首個參數:" + point.getArgs()[0]); Object proceed = point.proceed(); System.out.println("EXECUTION success"); return proceed; } }
注意:切面需要返回執行結果,否則會導致返回結果丟失;
eg.
任意公共方法的執行:execution(public * *(..))
任何一個以“set”開始的方法的執行:execution(* set*(..))
AccountService 接口的任意方法的執行:execution(* com.xyz.service.AccountService.*(..))
定義在service包里的任意方法的執行: execution(* com.xyz.service.*.*(..))
定義在service包和所有子包里的任意類的任意方法的執行:execution(* com.xyz.service..*.*(..))
控制台輸出:
切面執行順序:
參考文獻:
https://blog.csdn.net/zhanglf02/article/details/89787937
https://blog.csdn.net/qq_41981107/article/details/85260765 (經典)
https://www.cnblogs.com/niceyoo/p/10907203.html