AOP概述
在軟件業,AOP爲Aspect Oriented Programming的縮寫,意爲:面向切面編程,通過預編譯方式和運行期間動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點,也是Spring框架中的一個重要內容,是函數式編程的一種衍生範型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。
-
在業務層裏,每個增刪改方法,都要對事務進行操作, 即執行增刪改前要開啓事務,發生異常要回滾事務,沒有異常則提交事務等編碼操作,這樣就會有很多的重複的代碼,而使用AOP(實現方式:動態代理技術),則可以把重複的代碼抽取出來,在需要執行的時候,使用動態代理的技術,在不修改源碼的基礎上,對我們的已有方法進行增強。
-
對於在所有方法執行的前後,加入日誌的輸出這類需求,原有的方法是沒有這個功能的,這時就可以考慮對方法進行增強,原本方法的代碼不變(這裏只編寫業務代碼),而需要增強的功能(日誌輸出),則在需要執行的時候,使用動態代理的技術,在原本方法執行前後執行 輸出日誌記錄方法 即可。
Spring AOP的實現
Spring AOP代理的選擇
框架會根據目標類是否實現了接口來決定採用哪種動態代理的方式
AOP相關術語
Joinpoint
(連接點):連接點是指那些被攔截到的點。在spring中,這些點指的是方法,spring只支持方法類型的連接點。Pointcut
(切入點):切入點是指我們要對哪些Joinpoint進行攔截的定義。Advice
(通知/增強):通知是指攔截到Joinpoint之後所要做的事情就是通知。通知的類型:前置通知,後置通知,異常通知,最終通知,環繞通知。Introduction
(引介):引介是一種特殊的通知在不修改類代碼的前提下, Introduction可以在運行期爲類動態地添加一些方法或Field。Target
(目標對象):代理的目標對象。Weaving
(織入):是指把增強應用到目標對象來創建新的代理對象的過程。spring採用動態代理織入,而AspectJ採用編譯期織入和類裝載期織入。Proxy
(代理):一個類被AOP織入增強後,就產生一個結果代理類。Aspect
(切面): 是切入點和通知(引介)的結合。
基於XML的AOP配置
配置案例:調用方法時加入日誌的輸出。
- 引入依賴
<!--單元測試-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.2.RELEASE</version>
<scope>test</scope>
</dependency>
<!--AOP相關包-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
- 用戶業務層接口與實現類
//用戶業務層接口
public interface UserService {
//根據id查詢用戶
String getUserNameId(Integer id);
void deleteUser(Integer id);
}
//用戶業務層實現類
public class UserServiceImpl implements UserService {
public String getUserNameId(Integer id) {
System.out.println("執行UserServiceImpl getUserNameId方法");
// Object a=2/0;
return "張三三";
}
public void deleteUser(Integer id) {
System.out.println("執行UserServiceImpl deleteUser方法");
}
}
- 模擬的記錄日誌的工具類
//模擬的記錄日誌的工具類
public class Logger {
//指定方法調用前執行
public void beforeLog(){
System.out.println("=====前置通知執行======");
}
//指定方法調用之後執行
public void afterrturningLog(){
System.out.println("=====後置通知執行======");
}
// 異常時,異常通知
public void afterThrowing(){
System.out.println("====異常通知執行=====");
}
//finally時,最終通知
public void after(){
System.out.println("=====最終通知執行====");
}
/***
環繞通知
參數: ProceedingJoinPoint,被調用方法的簽名對象
ProceedingJoinPoint接口可以作爲環繞通知的方法參數來使用。當環繞通知執行時,spring框架會爲我們注入該接口的實現類。
ProceedingJoinPoint有一個方法proceed(),相當於invoke,調用目標方法(寫法類似java動態代理的invoke方法)
*/
public Object around(ProceedingJoinPoint jp){
Object result = null;
try {
System.out.println("=====環繞通知開始執行======");
//前置通知
System.out.println("=====前置通知執行======");
//調用目標方法
result = jp.proceed();
//執行類似後置通知
System.out.println("=====後置通知執行======");
} catch (Throwable throwable) {
//異常通知
System.out.println("====異常通知執行=====");
throwable.printStackTrace();
}finally {
//最終通知
System.out.println("=====最終通知執行====");
}
return result;
}
}
- 配置aop
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--配置spring的ioc-->
<!-- 配置service -->
<bean id="userService" class="com.mycode.service.impl.UserServiceImpl"></bean>
<!--【1】.配置通知類 把通知類的創建交給spring管理-->
<bean id="logger" class="com.mycode.util.Logger"></bean>
<!--【2】.Spring AOP配置 aop:config
用於聲明開始aop的配置
-->
<aop:config>
<!--定義一個切點表達式 aop:pointcut
用於配置切入點表達式。就是指定對哪些類的哪些方法進行增強。
id屬性:給切入點表達式提供一個唯一標識
expression屬性:用於定義切入點表達式。
-->
<aop:pointcut id="pointLog" expression="execution(* com..*.*(..))" />
<!-- 【3】.配置切面 aop:aspect
id屬性:切面的唯一標識。
ref屬性:引用配置好的通知類bean的id。
-->
<aop:aspect id="log" ref="logger">
<!--【4】.使用對應的標籤在aop:aspect標籤內部配置通知的類型-->
<!--不引用切點表達式的通知配置
method屬性:表示要執行的增強方法 logger.beforeLog
pointcut屬性:切點配置,指定執行的方法名字
execution:匹配方法的執行, execution(切入點表達式)
<aop:before method="beforeLog" pointcut="execution(java.lang.String com.mycode.service.impl.UserServiceImpl.getUserNameId(java.lang.Integer))"></aop:before>
-->
<!--配置前置通知 com子包下全部調用的方法都會執行前置通知
aop:before
method屬性:用於指定通知類中的增強方法名稱
ponitcut-ref屬性:用於指定切入點的表達式的引用
poinitcut屬性:用於指定切入點表達式
執行時間點:切入點方法執行之前執行
-->
<aop:before method="beforeLog" pointcut="execution(* com..*.*(..))"></aop:before>
<!--配置後置通知
aop:after-returning
執行時間點:切入點方法正常執行之後,後置通知和異常通知只能有一個執行
-->
<aop:after-returning method="afterrturningLog" pointcut-ref="pointLog"></aop:after-returning>
<!--配置異常通知
aop:after-throwing
執行時間點:切入點方法執行產生異常後執行,異常通知和後置通知只能執行一個
-->
<aop:after-throwing method="afterThrowing" pointcut-ref="pointLog"></aop:after-throwing>
<!--配置最終通知
aop:after
執行時間點:無論切入點方法執行時是否有異常,它都會在切入點方法執行後執行。
-->
<aop:after method="after" pointcut-ref="pointLog"></aop:after>
<!--配置環繞通知
aop:around
包含前面的四個通知,所以通常情況下,環繞通知都是獨立使用的。
spring的環繞通知是spring爲我們提供的一種 可以在代碼中手動控制增強方法何時執行的方式。
即在通知類的增強方法中類似java動態代理invoke方法中的寫法
-->
<aop:around method="around" pointcut-ref="pointLog"></aop:around>
</aop:aspect>
</aop:config>
</beans>
- 註釋其他通知類型,查看環繞通知運行輸出:
public class SpringAOPTest {
private UserService userService;
@Before
public void init() {
//創建Spring容器對象ApplicationContext
ApplicationContext act = new ClassPathXmlApplicationContext("beans.xml");
//根據bean的id獲取userService
userService = (UserService) act.getBean("userService");
}
@Test
public void SelectTest(){
String name = userService.getUserNameId(1);
System.out.println(name);
System.out.println("----------------------------");
userService.deleteUser(2);
}
}
- 運行:Spring框架監控切入點方法的執行。一旦監控到切入點方法被運行,使用代理機制,動態創建目標對象的代理對象,根據通知類別,在代理對象的對應位置,將通知對應的功能織入,完成完整的代碼邏輯運行。
基於註解的AOP配置
配置案例:調用方法時加入日誌的輸出。
純註解配置
- Logger
//模擬的記錄日誌的工具類
@Component("logger") //創建Logger對象的實例,並存入SpringIOC容器中,給SpringIOC容器管理
@Aspect //在通知類上使用@Aspect註解聲明爲切面,表示當前類是一個切面類
public class Logger {
//切入點表達式定義(切入點表達式註解)
@Pointcut("execution(* com..*.*(..))")
public void logPoint(){}
/* @Before
把當前方法看成是前置通知。
value屬性:用於指定切入點表達式,也可以指定切入點表達式的引用。
*/
@Before("logPoint()")//logPoint()指定切入點表達式的引用
public void beforeLog(){
System.out.println("=====前置通知執行======");
}
//把當前方法看成是後置通知。
@After("execution(* com..*.*(..))")//value指定的是切入點表達式
public void afterrturningLog(){
System.out.println("=====後置通知執行======");
}
//把當前方法看成是異常通知
@AfterThrowing("logPoint()")
public void afterThrowing(){
System.out.println("====異常通知執行=====");
}
//把當前方法看成是最終通知。
@After("logPoint()")
public void after(){
System.out.println("=====最終通知執行====");
}
/***
環繞通知
參數: ProceedingJoinPoint,被調用方法的簽名對象
ProceedingJoinPoint接口可以作爲環繞通知的方法參數來使用。當環繞通知執行時,spring框架會爲我們注入該接口的實現類。
ProceedingJoinPoint有一個方法proceed(),相當於invoke,調用目標方法
*/
@Around("logPoint()")
public Object around(ProceedingJoinPoint jp){
Object result = null;
try {
System.out.println("=====環繞通知開始執行======");
//前置通知
System.out.println("=====前置通知執行======");
//調用目標方法
result = jp.proceed();
//執行類似後置通知
System.out.println("=====後置通知執行======");
} catch (Throwable throwable) {
//異常通知
System.out.println("====異常通知執行=====");
throwable.printStackTrace();
}finally {
//最終通知
System.out.println("=====最終通知執行====");
}
return result;
}
}
- spring配置類,替換xml配置
@Configuration //指定當前類是一個spring配置類
@ComponentScan(basePackages = "com.mycode") //指定spring在初始化容器時要掃描的包。
@EnableAspectJAutoProxy //開啓spring對註解AOP的支持
public class SpringConfig {
}
註釋環繞通知類型,查看運行輸出:
xml配置和註解組合配置
需要在spring配置文件中開啓spring對註解AOP的支持
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">
<!--包掃描-->
<context:component-scan base-package="com.mycode" />
<!--開啓spring對註解AOP的支持-->
<aop:aspectj-autoproxy proxy-target-class="true" />
</beans>
切入點表達式說明
表達式語法:execution(修飾符 返回值類型 包名.類名.方法名(參數))
寫法:
全匹配方式:
public void com.mycode.service.impl.UserServiceImpl.add(com.mycode.domain.User)
省略修飾符方式:
void com.mycode.service.impl.UserServiceImpl.add(com.mycode.domain.User)
返回值使用*號代替,表示任意返回值
* com.mycode.service.impl.UserServiceImpl.add(com.mycode.domain.User)
包名使用*號代替,表示任意包。有幾級包,寫幾個*
* *.*.*.*.UserServiceImpl.add(com.mycode.domain.User)
使用..來表示當前包及其子包
* com..UserServiceImpl.add(com.mycode.domain.User)
使用*號表示任意類
* com..*.add(com.mycode.domain.User)
使用*號表示任意方法
* com..*.*( com.mycode.domain.User)
參數列表使用*代替,表示參數可以是任意數據類型,但是必須有參數
* com..*.*(*)
參數列表使用..代替表示有無參數均可,有參數可以是任意類型
* com..*.*(..)
全通配方式:
* *..*.*(..)
瞭解:
通常情況下,都是對業務層的方法進行增強,所以切入點表達式都是切到業務層實現類,寫法:
execution(* com.mycode.service.impl.*.*(..))