AOP(Aspect Oriented Programming)面向切面編程
百度百科上面這樣介紹的:通過預編譯方式和運行期間動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點,也是Spring框架中的一個重要內容,是函數式編程的一種衍生範型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。
我在此就不獻醜進行說明了,我覺得就是用過一些手段對方法進行了增強。要我進行詳細的說明,我還真的說不明白。
Spring中AOP相關術語
Joinpoint(連接點): 在Sping程序中允許你使用通知或增強的地方,這種地方比較多,可以是方法前後,也可以是拋出異常的時,這裏只提到了方法連接點,這是因爲Spring只支持方法類型的連接點。再通俗一點就是哪些方法可以被增強(使用通知),這些方法稱爲連接點。
Pointcut(切入點): 連接點是Spinrg程序中允許你使用通知或增強的地方,但是不是所有允許使用通知或增強的地方的地方都需要通知(增強)的,只有那些被我們使用了通知或者增強的地方纔能稱之爲切入點。再通俗一點就是類中實際被增加(使用了通知)的方法稱爲切入點。
Advice(通知/增強): 是指在找到連接點後做的增強操作(通知操作),實際上就是通知操作代碼(增強操作代碼)。例如對登錄帳號功能做增強,即在登錄之後添加了權限判定,那麼這個權限判定就是增強(通知)。通知可以分爲以下類型:前置通知、後置通知、異常通知、最終通知、環繞通知。
Aspect(切面): 切入點和通知的結合,通知和切點共同定義了切面的全部功能——它是什麼,在何時何處完成其功能。即把增強寫入到切入點的過程。
Introduction(引介): 允許我們向現有的類中添加方法或屬性。即把切面應用到目標類。
Weaving(織入): 是指把增強(通知)應用到目標對象來創建新的代理對象的過程。Spring採用動態代理織入。
Target(目標): 增強邏輯要寫入的類,即需要被增強(通知)的類。如果沒有AOP,這些目標業務類需要自己實現所有邏輯,在存在AOP的情況下,這些目標業務類只要實現業務邏輯,很多業務增強可以通過AOP方式完成。
Proxy(代理): 目標業務類被AOP進行增強後,就會返回一個結果代理類。這個編寫過或者瞭解過代理模式應該深有體會。
實例說明Spring的AOP
1. 基於XML的AOP配置
導入相關jar包
<!--Spring開發包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.1.RELEASE</version>
</dependency>
<!--解析切入點表達式 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
<scope>runtime</scope>
</dependency>
<!--用於測試-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
編寫業務類接口及其實現類
/**
* 賬戶的業務層接口模擬
*/
public interface AccountServiceImitate {
/**
* 模擬清除所以賬戶
*/
void deleteAccounts();
/**
* 模擬更新賬戶
*/
void updateAccount(int i);
/**
* 模擬查詢賬戶總數
* @return
*/
int findAccountTotal();
}
/**
*賬戶的業務層接口模擬 的 實現類
*/
public class AccountServiceImitateImpl implements AccountServiceImitate {
public void deleteAccounts() {
System.out.println("刪除了所有的賬戶......");
}
public void updateAccount(int i) {
System.out.println("更新用戶記錄......."+i);
}
public int findAccountTotal() {
System.out.println("查詢用戶總數.......");
return 0;
}
}
編寫通知類
/**
* 通知/增強類
*/
public class Logger {
/**
* 前置通知
*/
public void printBeforeLog()
{
System.out.println(" 前置通知 -- 在指定增強的方法在切入點方法之前執行 ");
}
/**
* 後置通知
*/
public void printAfterReturningLog()
{
System.out.println(" 後置通知 -- 在切入點方法正常執行以後執行 ");
}
/**
* 異常通知
*/
public void printAfterThrowingLog()
{
System.out.println(" 異常通知 -- 切入點方法執行產生異常後執行 ");
}
/**
* 最終通知
*/
public void printAfterLog()
{
System.out.println(" 最終通知 -- 無論切入點方法執行時是否有異常,它都會在其後面執行 相當於finally裏面的代碼 ");
}
}
編寫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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="accountServiceImitate" class="com.liang.service.impl.AccountServiceImitateImpl"></bean>
<bean id="logger" class="com.liang.utils.Logger"></bean>
<!--配置AOP -->
<aop:config>
<!--配置切面-->
<aop:aspect id="loggerAdvice" ref="logger">
<!--配置通知的類型,並且建立通知方法和切入點方法的關聯-->
<aop:before method="printBeforeLog" pointcut="execution(public void com.liang.service.impl.AccountServiceImitateImpl.deleteAccounts())"></aop:before>
<aop:after-returning method="printAfterReturningLog" pointcut="execution(public void com.liang.service.impl.AccountServiceImitateImpl.deleteAccounts())"></aop:after-returning>
<aop:after-throwing method="printAfterThrowingLog" pointcut="execution(public void com.liang.service.impl.AccountServiceImitateImpl.deleteAccounts())"></aop:after-throwing>
<aop:after method="printAfterLog" pointcut="execution(public void com.liang.service.impl.AccountServiceImitateImpl.deleteAccounts())"></aop:after>
</aop:aspect>
</aop:config>
</beans>
編寫測試方法
public class AOPTest {
@Test
public void testAOP(){
//加載配置文件,獲取容器
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");
//獲取對象
AccountServiceImitate accountServiceImitate = (AccountServiceImitate) applicationContext.getBean("accountServiceImitate");
accountServiceImitate.deleteAccounts();
}
}
AOP配置的一般步驟
-
用bean標籤配置通知類。 例如:<bean id="logger" class="com.liang.utils.Logger"></bean>
-
使用aop:config標籤聲明aop配置。例如:<aop:config>配置切面代碼</aop:config>
-
使用aop:aspect配置切面。 屬性id:賦予切面一個唯一標識; 屬性ref:引用配置好的通知類bean的id。 例如: <aop:aspect id="loggerAdvice" ref="logger">在切面裏面配置通知的類型</aop:aspect>
-
使用aop:xxx配置對應的通知類型。
通知類型 | 作用 | 屬性 |
---|---|---|
aop:before | 用於配置前置類型,在切入點方法之前執行 | method:指定通知類中的增強方法名稱;pointcut:指定切入點表達式;pointcut-ref:指定切入點表達式的引用 |
aop:after-returning | 用於配置後置通知,在切入點方法正常執行之後執行 | method:指定通知類中的增強方法名稱;pointcut:指定切入點表達式;pointcut-ref:指定切入點表達式的引用 |
aop:after-throwing | 用於配置異常通知,在切入點方法執行產生異常後執行 | method:指定通知類中的增強方法名稱;pointcut:指定切入點表達式;pointcut-ref:指定切入點表達式的引用 |
aop:after | 用於配置最終通知,無論切入點方法執行時是否有異常,它都會在其後面執行 相當於finally裏面的代碼 | method:指定通知類中的增強方法名稱;pointcut:指定切入點表達式;pointcut-ref:指定切入點表達式的引用 |
aop:around | 用於配置環繞通知,是Spring框架提供的一種可以在代碼中手動控制增強代碼執行的方式 | method:指定通知類中的增強方法名稱;pointcut:指定切入點表達式;pointcut-ref:指定切入點表達式的引用 |
其中切入點表達式可以使用aop:pointcut來配置,通過pointcut-ref引用。例如:
<bean id="logger" class="com.liang.utils.Logger"></bean>
<!--配置AOP -->
<aop:config>
<!--配置切面-->
<aop:aspect id="loggerAdvice" ref="logger">
<aop:pointcut id="printLogPointcut" expression="execution(public void com.liang.service.impl.AccountServiceImitateImpl.deleteAccounts())"/>
<!--配置通知的類型,並且建立通知方法和切入點方法的關聯-->
<aop:before method="printBeforeLog" pointcut-ref="printLogPointcut"></aop:before>
<aop:after-returning method="printAfterReturningLog" pointcut-ref="printLogPointcut"></aop:after-returning>
<aop:after-throwing method="printAfterThrowingLog" pointcut-ref="printLogPointcut"></aop:after-throwing>
<aop:after method="printAfterLog" pointcut-ref="printLogPointcut"></aop:after>
</aop:aspect>
</aop:config>
切入點表達式說明
表達式語法: execution( [修飾符] 返回值類型 全限定類型.方法名(參數) )
表達式寫法說明:
- 全匹配方式(實例中用到的);
- 訪問修飾符可以省略;
- 返回值可以使用通配符*表示任意返回值;
- 全限定類型中包名可以使用通配符*表示任意包,有幾級包,就需要幾個通配符*;
- 使用..表示當前包及其子包;
- 類名可以使用通配符*表示任意類;
- 方法名可以使用通配符*表示任意方法;
- 參數列表可以使用通配符*表示任意類型數據參數,但必須有參數;
- 參數列表可以使用..表示有無參數均可,有參數參數可以是任意類型。
通過以上說明,如果我們要匹配所有包下所有方法,則可使用的表達式是.* ….*(…)
環繞通知
修改通知類
/**
* 環繞通知
*/
public Object printAroundLog(ProceedingJoinPoint proceedingJoinPoint)
{
Object returnValue = null;
try {
//獲取切入點方法執行所需參數
Object[] args = proceedingJoinPoint.getArgs();
System.out.println("環繞通知 --- 前置通知");
returnValue = proceedingJoinPoint.proceed(args);//調用切入點方法
System.out.println("環繞通知 --- 後置通知");
return returnValue;
} catch (Throwable throwable) {
System.out.println("環繞通知 --- 異常通知");
throw new RuntimeException(throwable);
}finally {
System.out.println("環繞通知 --- 最終通知");
}
}
修改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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="accountServiceImitate" class="com.liang.service.impl.AccountServiceImitateImpl"></bean>
<bean id="logger" class="com.liang.utils.Logger"></bean>
<!--配置AOP -->
<aop:config>
<!--配置切面-->
<aop:aspect id="loggerAdvice" ref="logger">
<!--配置切入點表達式-->
<aop:pointcut id="printLogPointcut" expression="execution(* com.liang.service.impl.*.*(..))"/>
<!--配置環繞通知-->
<aop:around method="printAroundLog" pointcut-ref="printLogPointcut"></aop:around>
</aop:aspect>
</aop:config>
</beans>
在配置環繞通知的時候需要使用Spring爲我們提供的一個接口(ProceedingJoinPoint),該接口proceed()方法相當於調用切入點方法,該接口可以作爲環繞通知方法的參數,在程序執行時,spring框架會爲我們提供該接口的實現類供我們使用。
2. 註解和XML結合的AOP配置
編寫業務類接口及其實現類
/**
* 賬戶的業務層接口模擬
*/
public interface AccountServiceImitate {
/**
* 模擬清除所以賬戶
*/
void deleteAccounts();
/**
* 模擬更新賬戶
*/
void updateAccount(int i);
/**
* 模擬查詢賬戶總數
* @return
*/
int findAccountTotal();
}
/**
*賬戶的業務層接口模擬 的 實現類
*/
@Service(value = "accountServiceImitate")
public class AccountServiceImitateImpl implements AccountServiceImitate {
public void deleteAccounts() {
System.out.println("刪除了所有的賬戶......");
}
public void updateAccount(int i) {
System.out.println("更新用戶記錄......."+i);
}
public int findAccountTotal() {
System.out.println("查詢用戶總數.......");
return 0;
}
}
編寫通知類
/**
* 通知/增強類
*/
@Component(value = "logger")
@Aspect//表示當前類是一個切面類
public class Logger {
/**
* 前置通知
*/
@Before("execution(* com.liang.service.impl.*.*(..))")
public void printBeforeLog()
{
System.out.println(" 前置通知 -- 在指定增強的方法在切入點方法之前執行 ");
}
/**
* 後置通知
*/
@AfterReturning("execution(* com.liang.service.impl.*.*(..))")
public void printAfterReturningLog()
{
System.out.println(" 後置通知 -- 在切入點方法正常執行以後執行 ");
}
/**
* 異常通知
*/
@AfterThrowing("execution(* com.liang.service.impl.*.*(..))")
public void printAfterThrowingLog()
{
System.out.println(" 異常通知 -- 切入點方法執行產生異常後執行 ");
}
/**
* 最終通知
*/
@After("execution(* com.liang.service.impl.*.*(..))")
public void printAfterLog()
{
System.out.println(" 最終通知 -- 無論切入點方法執行時是否有異常,它都會在其後面執行 相當於finally裏面的代碼 ");
}
/**
* 環繞通知
*/
@Around("execution(* com.liang.service.impl.*.*(..))")
public Object printAroundLog(ProceedingJoinPoint proceedingJoinPoint)
{
Object returnValue = null;
try {
//獲取切入點方法執行所需參數
Object[] args = proceedingJoinPoint.getArgs();
System.out.println("環繞通知 --- 前置通知");
returnValue = proceedingJoinPoint.proceed(args);//調用切入點方法
System.out.println("環繞通知 --- 後置通知");
return returnValue;
} catch (Throwable throwable) {
System.out.println("環繞通知 --- 異常通知");
throw new RuntimeException(throwable);
}finally {
System.out.println("環繞通知 --- 最終通知");
}
}
}
編寫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
https://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">
<!--告知Spring需要掃描的包-->
<context:component-scan base-package="com.liang"></context:component-scan>
<!--配置spring開啓註解AOP的支持-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>
編寫測試方法
public class AOPTest {
@Test
public void testAOP(){
//加載配置文件,獲取容器
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("bean.xml");
//獲取對象
AccountServiceImitate accountServiceImitate = (AccountServiceImitate) applicationContext.getBean("accountServiceImitate");
accountServiceImitate.deleteAccounts();
}
}
測試代碼的時候,最好將環繞通知和其他通知分開進行測試 那樣比較明瞭。
上面用到了新的註解,在此做個簡單的說明:
註解 | 功能 |
---|---|
@Aspect | 在類上應用,把當前類聲明爲切面類 |
@Before | 作用在方法上,表示把當前方法作爲前置通知 value屬性用於指定切入點表達式或者切入點表達式引用 |
@AfterReturning | 作用在方法上,表示把當前方法作爲後置通知 value屬性用於指定切入點表達式或者切入點表達式引用 |
@AfterThrowing | 作用在方法上,表示把當前方法作爲異常通知 value屬性用於指定切入點表達式或者切入點表達式引用 |
@After | 作用在方法上,表示把當前方法作爲最終通知 value屬性用於指定切入點表達式或者切入點表達式引用 |
@Around | 作用在方法上,表示把當前方法作爲環繞通知 value屬性用於指定切入點表達式或者切入點表達式引用 |
@Pointcut | 指定切入點表達式 value屬性指定表達式的內容 |
上面的實例中並沒有使用到@Pointcut註解,其實該註解相當於標籤<aop:pointcut />,具體用法如下:
/**
* 通知/增強類
*/
@Component(value = "logger")
@Aspect//表示當前類是一個切面類
public class Logger {
@Pointcut("execution(* com.liang.service.impl.*.*(..))")
private void printLogPointcut(){}
/**
* 前置通知
*/
@Before("printLogPointcut()")
public void printBeforeLog()
{
System.out.println(" 前置通知 -- 在指定增強的方法在切入點方法之前執行 ");
}
/**
* 後置通知
*/
@AfterReturning("printLogPointcut()")
public void printAfterReturningLog()
{
System.out.println(" 後置通知 -- 在切入點方法正常執行以後執行 ");
}
/**
* 異常通知
*/
@AfterThrowing("printLogPointcut()")
public void printAfterThrowingLog()
{
System.out.println(" 異常通知 -- 切入點方法執行產生異常後執行 ");
}
/**
* 最終通知
*/
@After("printLogPointcut()")
public void printAfterLog()
{
System.out.println(" 最終通知 -- 無論切入點方法執行時是否有異常,它都會在其後面執行 相當於finally裏面的代碼 ");
}
/**
* 環繞通知
*/
@Around("printLogPointcut()")
public Object printAroundLog(ProceedingJoinPoint proceedingJoinPoint)
{
Object returnValue = null;
try {
//獲取切入點方法執行所需參數
Object[] args = proceedingJoinPoint.getArgs();
System.out.println("環繞通知 --- 前置通知");
returnValue = proceedingJoinPoint.proceed(args);//調用切入點方法
System.out.println("環繞通知 --- 後置通知");
return returnValue;
} catch (Throwable throwable) {
System.out.println("環繞通知 --- 異常通知");
throw new RuntimeException(throwable);
}finally {
System.out.println("環繞通知 --- 最終通知");
}
}
}
2. 純註解的AOP配置
使用純註解,只需要編寫配置文件類,同時刪除XML配置文件即可,配置類具體內容如下:
@Configuration
@ComponentScan(basePackages = "com.liang")
@EnableAspectJAutoProxy//配置spring開啓註解AOP的支持
public class SpringConfig {
}
修改測試類
public class AOPTest {
@Test
public void testAOP(){
//加載配置類,獲取容器
ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
//獲取對象
AccountServiceImitate accountServiceImitate = (AccountServiceImitate) applicationContext.getBean("accountServiceImitate");
accountServiceImitate.deleteAccounts();
}
}
如有問題,歡迎來探討哦。謝謝