一、概述
面向切面編程(AOP)是針對面向對象編程(OOP)的補充,可以非侵入式的為多個不具有繼承關系的對象引入相同的公共行為例如日志、安全、事務、性能監控等等。SpringAOP允許將公共行為從業務邏輯中抽離出來,並將這些行為以一種非侵入的方式織入到所有需要的業務邏輯中,相較於OOP縱向模式的業務邏輯實現,其關注的方向是橫向的切面。
從Spring2.0開始,引入AspectJ注釋來對POJO進行標注,支持通過切點函數、邏輯運算符、通配符等高級功能來對切點進行靈活的定義,結合各種類型的通知來形成強大的連接點描述能力。
二、使用示例
2.1、基礎示例,攔截指定類與方法
1、pom
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> </dependency>
或者
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
核心模塊:兩者效果都一樣,將Spring核心模塊Spring core、Spring beans、Spring context、Spring aop以及AspectJ注釋提供庫aspectjweaver引入項目。(另外包括SpringBoot特有的自動配置模塊等等)
2、正常的業務邏輯

@Service public class MyTestService { Logger logger = LoggerFactory.getLogger(MyTestService.class); public String doSomething1(){ logger.info("invoking doSomething1......"); return "doSomething1"; } public String doSomething2(){ logger.info("invoking doSomething2......"); return "doSomething2"; } }
3、定義切面

@Aspect @Component public class MyIntercepter { private static final Logger logger = LoggerFactory.getLogger(MyIntercepter.class); @Pointcut("execution(public * com.github.bjlhx15.springaop.service.MyTestService.doSomething1*(..))") public void doSomethingPointcut(){}; @Before("doSomethingPointcut()") public void auth(JoinPoint pjp) throws Throwable{ MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); //獲取被攔截的方法 String methodName = method.getName(); //獲取被攔截的方法名 logger.info("權限認證:調用方法為:{}", methodName); }; @AfterReturning(value = "doSomethingPointcut()", returning = "returnVal") public void logNormal(JoinPoint pjp, Object returnVal) throws Throwable{ MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); //獲取被攔截的方法 String methodName = method.getName(); //獲取被攔截的方法名 logger.info("正常返回記日志:調用方法為:{};返回結果為:{}", methodName, returnVal); }; @AfterThrowing(value = "doSomethingPointcut()", throwing = "e") public void logThrowing(JoinPoint pjp, Throwable e) throws Throwable{ MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); //獲取被攔截的方法 String methodName = method.getName(); //獲取被攔截的方法名 logger.info("拋出異常記日志:調用方法為:{};異常信息為:{}", methodName, e.getMessage()); }; @After(value = "doSomethingPointcut()") public void afterall(JoinPoint pjp) throws Throwable{ MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); //獲取被攔截的方法 String methodName = method.getName(); //獲取被攔截的方法名 logger.info("方法調用完成:調用方法為:{}", methodName); } @Around("doSomethingPointcut()") public Object timer(ProceedingJoinPoint pjp) throws Throwable{ long beginTime = System.currentTimeMillis(); MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); //獲取被攔截的方法 String methodName = method.getName(); //獲取被攔截的方法名 logger.info("計時切面:請求開始,方法:{}", methodName); Object result = null; try { // 一切正常的情況下,繼續執行被攔截的方法 result = pjp.proceed(); } catch (Throwable e) { logger.info("exception: ", e); } long endTime = System.currentTimeMillis(); logger.info("計時切面:請求結束,方法:{},執行時間:{}", methodName, (endTime-beginTime)); return result; } }
上面定義了一個切點Pointcut,並圍繞這個切點定義了5中不同類型的通知Advice,每個切點及其通知以及通知執行的邏輯共同構成了一個切面Advisor,用以在方法執行過程的各個時間點切入,執行一些特定邏輯。
4、調用
這時程序調用即有對應的切面信息,測試
@RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = ApplicationMain.class) public class MyTestServiceTest { @Autowired MyTestService myTestService; @Test public void doSomething1() { myTestService.doSomething1(); } }
輸出
c.g.b.s.interceptor.MyIntercepter : 計時切面:請求開始,方法:doSomething1
c.g.b.s.interceptor.MyIntercepter : 權限認證:調用方法為:doSomething1
c.g.b.springaop.service.MyTestService : invoking doSomething1......
c.g.b.s.interceptor.MyIntercepter : c.g.b.s.interceptor.MyIntercepter : 方法調用完成:調用方法為:doSomething1
c.g.b.s.interceptor.MyIntercepter : 正常返回記日志:調用方法為:doSomething1;返回結果為:doSomething1
以無侵入的形式在方法調用的前后增加了很多橫切向的業務邏輯,業務邏輯代碼並不必關心這些橫切邏輯,只需要專注於自己的業務邏輯的實現就好。
在實際執行當中,在方法調用前后我們定義的切面都開始執行了。SpringAOP確保我們定義的切面織入到業務邏輯代碼中,並在執行時發揮作用。
另外如結果所示多個切面的執行順序也並不是按照方法定義的順序執行。
2.2、基於自定義注解的切面
直接使用execution(public * com.github.bjlhx15.springaop.service.MyTestService.doSomething1*(..))這種切面定義方式與實際的類路徑、類名或方法名緊密綁定,不利於擴展,后續使用需要編寫切面的人才能繼續。
我們希望像SpringCache那樣基於自定義注解的方式啟動各種切面,SpringAOP通過切點函數@annotation和@Within來支持這種方式。
1、pom同上
2、定義注解
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface TestLogger { } @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface TestTimer { }
其中TestLogger是定義在類上的注釋;TestTimer是定義在方法上的注釋。
3、定義基於自定義注解的切面
@Aspect @Component public class MyIntercepter2 { private static final Logger logger = LoggerFactory.getLogger(MyIntercepter2.class); @Pointcut("@annotation(com.github.bjlhx15.springaop.anno.TestTimer)") public void timerPointcut(){}; @Pointcut("@within(com.github.bjlhx15.springaop.anno.TestLogger)") public void recordLogPointcut(){}; @Before("recordLogPointcut()") public void log(JoinPoint pjp) throws Throwable{ MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); //獲取被攔截的方法 String methodName = method.getName(); //獲取被攔截的方法名 logger.info("開始記日志:調用方法為:{}", methodName); } @Around("timerPointcut()") public Object timer(ProceedingJoinPoint pjp) throws Throwable{ long beginTime = System.currentTimeMillis(); MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); //獲取被攔截的方法 String methodName = method.getName(); //獲取被攔截的方法名 logger.info("請求開始,方法:{}", methodName); Object result = null; try { // 一切正常的情況下,繼續執行被攔截的方法 result = pjp.proceed(); } catch (Throwable e) { logger.info("exception: ", e); } long endTime = System.currentTimeMillis(); logger.info("請求結束,方法:{},執行時間:{}", methodName, (endTime-beginTime)); return result; } }
上述代碼表示打了@TestLogger注釋的類,其中的所有方法被調用時都會記日志;而不管什么類,其打了@TestTimer注釋的方法都會監控其執行時間。
切點函數@annotation表示匹配方法上的注解,切點函數@within表示匹配類上的注解。
4、編寫業務邏輯並使用切面注解

@Service @TestLogger public class MyTestService2 { Logger logger = LoggerFactory.getLogger(MyTestService2.class); public String sayHello(){ logger.info("invoking method sayHello......"); return "Hello world!"; } @TestTimer public int count(){ logger.info("invoking method count......"); return 10; } }
根據服務MyTestService2中的注釋,其表達的意思是MyTestService2中所有方法調用時都需要記日志,另外count()方法被調用時候需要監控執行時間。
5、測試輸出
c.g.b.s.interceptor.MyIntercepter2 : 開始記日志:調用方法為:sayHello
c.g.b.springaop.service.MyTestService2 : invoking method sayHello......
c.g.b.s.interceptor.MyIntercepter2 : 請求開始,方法:count
c.g.b.s.interceptor.MyIntercepter2 : 開始記日志:調用方法為:count
c.g.b.springaop.service.MyTestService2 : invoking method count......
c.g.b.s.interceptor.MyIntercepter2 : 請求結束,方法:count,執行時間:0
由上可見,由於我們標注的注解的不同,在調用方法sayHello時只將記日志的邏輯切入進來,而在調用方法count時,將記日志和監控執行時間的邏輯都切入進來了。