1.前言
開發中最常見就是使用Aspect完成AOP功能的實現,下面將簡單使用@Aspect實現AOP的面向切面編程。關於Spring原生AOP的用法可參考之前的一篇博客文章 Spring5框架之AOP-ProxyFactory底層實現(五)
在使用AspectJ之前讓我先溫習一下幾個比較重要的概念如下所示:
- before:在某個連接點之前執行程序邏輯。
- after returning:連接點正常後執行的程序邏輯,需要注意的是如果程序拋出異常該通知並不會執行。
- after throwing :當程序出現異常時候執行的程序邏輯。
- after:當連接點結束執行的程序邏輯(無論是否出現異常都會執行)
- around:spring中最強大的通知功能,它可以完成並實現上面4種功能的實現。
2.使用Aspect實現AOP功能
如果使用@Aspect註解需要在項目中支持引入下述Aspect的依賴:
<!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.10</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.10</version>
</dependency>
下面我們依然使用Calculator這個計算器接口演示AspectJ方法的使用如下所示:
- 新增接口及其實現
首先AspectJ爲我們提供瞭如下幾個通知註解:
@Before
、 @AfterReturning
、 @After
、@Around
、 @AfterThrowing
接下來將使用一個簡單的案例演示這幾個註解的使用。
public interface Calculator {
int add(int a, int b);
int sub(int a, int b);
double divide(int a, int b);
}
@Service
public class CalculatorImpl implements Calculator {
@Override
public int add(int a, int b) {
return a + b;
}
@Override
@AdviceRequired
public int sub(int a, int b) {
return a - b;
}
@Override
public double divide(int a, int b) {
return a / b;
}
}
- 定義切面類
@Aspect
@Component
public class LogAspect {
/**
* 定義切點,其中execution定義切入點表達式
*/
@Pointcut(value = "execution(* *..aop..*(..))")
public void logPoint() {
}
/**
* 前置通知在方法執行前執行
*/
@Before(value = "execution(public * com.codegeek.aop.day1.CalculatorImpl.*(..))")
public static void logStart(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
// 拿到執行方法的源對象-----即切入表達式實際切入的地方並運行的地方
System.out.println(joinPoint.getTarget().getClass());
// System.out.println(joinPoint.getStaticPart()); 打印詳細切入點表達式
System.out.println("LogAspect-普通通知方法@before:" + joinPoint.getSignature().getName() + "日誌開始了....方法參數:" + Arrays.asList(args));
}
/**
* 方法執行後返回結果後執行該通知
* @param result
*/
@AfterReturning(value = "execution(public * com.codegeek.aop.day1.CalculatorImpl.*(..))", returning = "result")
public static void logRun(Object result) {
System.out.println("LogAspect-普通通知@AfterReturning" + "運行結果爲:" + result);
}
/**
*
* 如果方法執行出現異常將執行此通知
*/
@AfterThrowing(value = "execution(public * com.codegeek.aop.day1.Calculator.*(..))", throwing = "e")
public static void logException(Exception e) {
System.out.println("LogAspect-普通通知@AfterThrowing出異常啦:" + e);
}
/**
* 後置通知切入點執行後
*/
@After(value = "execution(public * com.codegeek.aop.day1.Calculator.*(..))")
public void logEnd() {
System.out.println("LogAspect-普通通知@After日誌結束了");
}
@Around(value = "logPoint()")
public Object logAround(ProceedingJoinPoint proceedingJoinPoint) {
Object proceed = null;
try {
// @Before
System.out.println("環繞前通知.....當前執行的方法:" + proceedingJoinPoint.getSignature().getName());
proceed = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
// @AfterReturn
System.out.println("環繞後通知.....");
} catch (Throwable throwable) {
// @AfterThrowing
System.out.println("環繞異常通知.......");
// 如果不拋出異常----則就將異常catch掉了,普通通知異常將無法感知到異常對象,所以認爲是正常執行的
throw new RuntimeException(throwable);
} finally {
// @After
System.out.println("環繞結束通知.....");
}
return proceed;
}
}
在我們定義完切面類後有幾個重要的點需要注意一下:
- 使用了@Component和@Aspect註解聲明一個切面類對象,且可以被Spring IOC容器掃描到此對象。
- 使用了 @Pointcut註解定義了方法的切點,所謂的切點的作用就是決定通知能夠在那些目標類的目標方法執行。
- @Before、@Around、@After、@AfterReturning、@AfterThrowing 中可以引入切點的值,亦可自定義切點的值
接下來我們還需要定義配置類以使Spring可以掃描到切面類併成功可以執行通知。
@Configuration
@ComponentScan(basePackages = {"com.codegeek.aop.day1"})
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AOPConfig {
}
- 測試方法:
需要注意的是在測試類上可以引入此配置類如下所示:
@Test
public void testAspect() {
// 配置切面類IOC就生成接口的代理類否則就是基本類
System.out.println();
Calculator bean = applicationContext.getBean(Calculator.class);
System.out.println(bean.add(1, 5));
System.out.println("-------------------");
}
這裏我們使用了正常的方法測試其運行結果如下:
環繞前通知.....當前執行的方法:add
class com.codegeek.aop.day1.CalculatorImpl
LogAspect-普通通知方法@before:add日誌開始了....方法參數:[1, 5]
環繞後通知.....
環繞結束通知.....
LogAspect-普通通知@After日誌結束了
LogAspect-普通通知@AfterReturning運行結果爲:6
6
-------------------
接下來我們測試一個異常的情況如下:
@Test
public void testAspect() {
// 配置切面類IOC就生成接口的代理類否則就是基本類
System.out.println();
Calculator bean = applicationContext.getBean(Calculator.class);
System.out.println(bean.add(1, 5));
System.out.println("-------------------");
System.out.println(bean.divide(5, 0));
System.out.println(bean);
System.out.println(bean.getClass());
}
運行測試方法後輸出結果如下所示:
環繞前通知.....當前執行的方法:add
class com.codegeek.aop.day1.CalculatorImpl
LogAspect-普通通知方法@before:add日誌開始了....方法參數:[1, 5]
環繞後通知.....
環繞結束通知.....
LogAspect-普通通知@After日誌結束了
LogAspect-普通通知@AfterReturning運行結果爲:6
6
-------------------
環繞前通知.....當前執行的方法:divide
class com.codegeek.aop.day1.CalculatorImpl
LogAspect-普通通知方法@before:divide日誌開始了....方法參數:[5, 0]
環繞異常通知.......
環繞結束通知.....
LogAspect-普通通知@After日誌結束了
LogAspect-普通通知@AfterThrowing出異常啦:java.lang.RuntimeException: java.lang.ArithmeticException: / by zero
java.lang.RuntimeException: java.lang.ArithmeticException: / by zero
我們發現了異常通知執行了,但是程序運行正常情況下異常通知將不會執行,於此同時可以發現@AfterReturning註解通知並沒有執行,它只有在程序運行正常下才會執行通知。關於通知執行順序可以很形象表示如下:
[普通前置]
try {
環繞通知前置
環繞執行
環繞返回
} catch(Exception e) {
環繞異常通知
} finally {
環繞後置通知
}
[普通後置]
[普通方法返回/普通異常通知]
3.使用xml實現AOP實現
我們首先將LogAspect類上的@Aspect註解進行註釋,然後新建立一個切面類OrderAspect如下:
@Component
//@Aspect
@Order(1)
public class OrderAspect {
// @Before(value = "execution(* *..aop..*(..))")
public static void logStart(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
// System.out.println(joinPoint.getStaticPart()); 打印詳細切入點表達式
System.out.println("OrderAspect-普通通知方法@before:" + joinPoint.getSignature().getName() + "日誌開始了....方法參數:" + Arrays.asList(args));
}
// @Pointcut(value = "execution(* *..aop..*(..))")
public void logPoint() {
}
// returning 告訴方法執行完畢後返回的值
// @AfterReturning(value = "execution(public * com.codegeek.aop.day1.CalculatorImpl.*(..))", returning = "result")
public static void logRun(Object result) {
System.out.println("OrderAspect-普通通知@AfterReturning" + "運行結果爲:" + result);
}
//@AfterThrowing(value = "execution(public * com.codegeek.aop.day1.Calculator.*(..))", throwing = "e")
public static void logException(Exception e) {
System.out.println("OrderAspect-普通通知@AfterThrowing出異常啦:" + e);
}
// @After(value = "execution(public * com.codegeek.aop.day1.Calculator.*(..))")
public void logEnd() {
System.out.println("OrderAspect-普通通知@After日誌結束了");
}
}
然後在xml中配置如下內容:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<!-- false使用jdk代理否則使用CGlib-->
<aop:aspectj-autoproxy proxy-target-class="true"/>
<context:component-scan base-package="com.codegeek.aop.day1"/>
<aop:config>
<aop:pointcut id="logPoint" expression="execution(* *..aop..*(..))"/>
<aop:aspect id="myAspect" ref="orderAspect">
<aop:before method="logStart" pointcut-ref="logPoint" arg-names="joinPoint"/>
<aop:after method="logEnd" pointcut-ref="logPoint" />
<aop:after-returning method="logRun" pointcut-ref="logPoint" returning="result"/>
<aop:after-throwing method="logException" pointcut-ref="logPoint" throwing="e"/>
</aop:aspect>
</aop:config>
<bean id="orderAspect" class="com.codegeek.aop.day1.OrderAspect"/>
</beans>
我們再次運行測試方法如下所示:
@Test
public void testAspect() {
// 配置切面類IOC就生成接口的代理類否則就是基本類
System.out.println();
Calculator bean = applicationContext.getBean(Calculator.class);
System.out.println(bean.add(1, 5));
System.out.println("-------------------");
System.out.println(bean.divide(5, 0));
System.out.println(bean);
運行結果如下所示:
OrderAspect-普通通知方法@before:add日誌開始了....方法參數:[1, 5]
OrderAspect-普通通知@After日誌結束了
OrderAspect-普通通知@AfterReturning運行結果爲:6
6
-------------------
OrderAspect-普通通知方法@before:divide日誌開始了....方法參數:[5, 0]
OrderAspect-普通通知@After日誌結束了
OrderAspect-普通通知@AfterThrowing出異常啦:java.lang.ArithmeticException: / by zero
java.lang.ArithmeticException: / by zero
4. 如何選擇AOP類型
在上面我們演示了一個基於Aspect風格註解的AOP實現,也看到了一個基於XML配置的AOP實現。這些受到多種因素的影響,例如程序需求、開發工具、開發團隊對AOP熟悉的程度等等。由於Spring AOP與AspectJ都使用了相同的Aspect風格,如果有額外的需要AspectJ的功能需求,可以將現有的Aspect風格註解的代碼遷移到Aspect上。