Spring_3 AOP
一、AOP 的相關概念
1、AOP概述
(1) 什麼是 AOP
AOP:全稱是 Aspect Oriented Programming 即:面向切面編程。
簡單的說它就是把我們程序重複的代碼抽取出來,在需要執行的時候,使用動態代理的技術,在不修改源碼的
基礎上,對我們的已有方法進行增強。
(2) AOP 的作用及優勢
作用:
在程序運行期間,不修改源碼對已有方法進行增強。
優勢:
減少重複代碼 提高開發效率 維護方便
(3) AOP 的實現方式
(解決重複代碼,提取重複代碼,在方法執行時加進去)使用動態代理技術
2、AOP 的具體應用
(1) 動態代理回顧
特點:字節碼隨用隨創建,隨用隨加載。
它與靜態代理的區別也在於此。因爲靜態代理是字節碼一上來就創建好,並完成加載。
裝飾者模式就是靜態代理的一種體現。
(2) 動態代理常用的有兩種方式
基於接口的動態代理
提供者:JDK 官方的 Proxy 類。
如何創建代理對象:使用Proxy類中的newProxyInstance方法
要求:被代理類最少實現一個接口。
基於子類的動態代理
提供者:第三方的 CGLib,如果報 asmxxxx 異常,需要導入 asm.jar。
要求:被代理類不能用 final 修飾的類(最終類)。
(3) 使用 JDK 官方的 Proxy 類創建代理對象
對生產廠家要求的接口
public interface IProducer {
/**
* 銷售
* @param money
*/
public void saleProduct(float money);
/**
* 售後
* @param money
*/
public void afterService(float money);
}
實現要求接口的廠家類
public class Producer implements IProducer{
/**
* 銷售
* @param money
*/
@Override
public void saleProduct(float money){
System.out.println("銷售產品,並拿到錢:"+money);
}
/**
* 售後
* @param money
*/
@Override
public void afterService(float money){
System.out.println("提供售後服務,並拿到錢:"+money);
}
}
模擬一個消費者
/**
* 模擬一個消費者
*/
public class Client {
public static void main(String[] args) {
//new一個廠家
final Producer producer = new Producer();
/*
* 如何創建代理對象:
* 使用Proxy類中的newProxyInstance方法
* 創建代理對象的要求:
* 被代理類最少實現一個接口,如果沒有則不能使用
* newProxyInstance方法的參數:
* ClassLoader:類加載器
* 它是用於加載代理對象字節碼的。和被代理對象使用相同的類加載器。固定寫法。
* class[]:字節碼數組
它是用於讓代理對象和被代理對象具有相同的方法。實現相同的接口。
*
* InvocationHandler:用於提供增強的代碼
* 它是讓我們寫如何代理。我們一般都是些一個該接口的實現類,通常情況下都是匿名內部類,但不是必須的。
* 此接口的實現類都是誰用誰寫。
*/
IProducer proxyProducer = (IProducer) Proxy.newProxyInstance(producer.getClass().getClassLoader(),
producer.getClass().getInterfaces(),
new InvocationHandler() {
/**
* 作用:執行被代理對象的任何接口方法都會經過該方法
* 方法參數的含義
* @param proxy 代理對象的引用
* @param method 當前執行的方法
* @param args 當前執行方法所需的參數
* @return 和被代理對象方法有相同的返回值
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//提供增強的代碼
Object returnValue = null;
//1.獲取方法執行的參數
Float money = (Float)args[0];
//2.判斷當前方法是不是銷售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
proxyProducer.saleProduct(10000f);
}
}
(4) 使用 CGLib 的 的 Enhancer 類創建代理對象
不實現接口,子類的動態代理
/**
* 一個生產者
*/
public class Producer {
//銷售
public void saleProduct(float money){
System.out.println("銷售產品,並拿到錢:"+money);
}
//售後
public void afterService(float money){
System.out.println("提供售後服務,並拿到錢:"+money);
}
}
/**
* 模擬一個消費者
*/
public class Client {
public static void main(String[] args) {
final Producer producer = new Producer();
/**
* 基於子類的動態代理:
* 涉及的類:Enhancer
* 提供者:第三方cglib庫
* 如何創建代理對象:
* 使用Enhancer類中的create方法
* 創建代理對象的要求:
* 被代理類不能是最終類
* create方法的參數:
* Class:字節碼
* 它是用於指定被代理對象的字節碼。
*
* Callback:用於提供增強的代碼
* 它是讓我們寫如何代理。我們一般都是些一個該接口的實現類,通常情況下都是匿名內部類,但不是必須的。
* 此接口的實現類都是誰用誰寫。
* 我們一般寫的都是該接口的子接口實現類:MethodInterceptor
*/
Producer cglibProducer = (Producer)Enhancer.create(producer.getClass(), new MethodInterceptor() {
/**
* 執行被代理對象的任何方法都會經過該方法
* @param proxy
* @param method
* @param args
* 以上三個參數和基於接口的動態代理中invoke方法的參數是一樣的
* @param methodProxy :當前執行方法的代理對象
* @return
* @throws Throwable
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//提供增強的代碼
Object returnValue = null;
//1.獲取方法執行的參數
Float money = (Float)args[0];
//2.判斷當前方法是不是銷售
if("saleProduct".equals(method.getName())) {
returnValue = method.invoke(producer, money*0.8f);
}
return returnValue;
}
});
cglibProducer.saleProduct(12000f);
}
}
二、Spring 中的 AOP
1、 Spring 中 AOP 的細節
(1) AOP 相關術語
Joinpoint (連接點):
所謂連接點是指那些被攔截到的點。在 spring 中,這些點指的是方法,因爲 spring 只支持方法類型的連接點。
Pointcut (切入點):
所謂切入點是指我們要對哪些 Joinpoint 進行攔截的定義。
Advice (通知/增強):
所謂通知是指攔截到 Joinpoint 之後所要做的事情就是通知。
通知的類型:前置通知,後置通知,異常通知,最終通知,環繞通知。
Introduction (引介):
引介是一種特殊的通知在不修改類代碼的前提下, Introduction 可以在運行期爲類動態地添加一些方法或 Field。
Target (目標對象):
代理的目標對象。
Weaving (織入):
是指把增強應用到目標對象來創建新的代理對象的過程。
spring 採用動態代理織入,而 AspectJ 採用編譯期織入和類裝載期織入。
Proxy (代理):
一個類被 AOP 織入增強後,就產生一個結果代理類。
Aspect (切面):
是切入點和通知(引介)的結合。
(2)spring AOP要明確的事
a、開發階段(我們做的)
編寫核心業務代碼(開發主線):大部分程序員來做,要求熟悉業務需求。
把公用代碼抽取出來,製作成通知。(開發階段最後再做):AOP 編程人員來做。
在配置文件中,聲明切入點與通知間的關係,即切面。:AOP 編程人員來做。
b、運行階段(Spring框架完成的)
Spring 框架監控切入點方法的執行。一旦監控到切入點方法被運行,使用代理機制,動態創建目標對
象的代理對象,根據通知類別,在代理對象的對應位置,將通知對應的功能織入,完成完整的代碼邏輯運行。
(3) 關於代理的選擇
在 spring 中,框架會根據目標類是否實現了接口來決定採用哪種動態代理的方式。
2、基於 XML 的 AOP 配置
(1) 導入座標
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<!--解析切入點表達式-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.2</version>
</dependency>
</dependencies>
(2) service包中的IAccountService和其實現類impl.AccountServiceImpl
public interface IAccountService {
/**
* 模擬保存賬戶
*/
void saveAccount();
/**
* 模擬更新賬戶
* @param i
*/
void updateAccount(int i);
/**
* 刪除賬戶
* @return
*/
int deleteAccount();
}
public class AccountServiceImpl implements IAccountService {
/**
* 模擬保存賬戶
*/
@Override
public void saveAccount() {
System.out.println("保存...");
}
/**
* 模擬更新賬戶
* @param i
*/
@Override
public void updateAccount(int i) {
System.out.println("更新...");
}
/**
* 刪除賬戶
* @return
*/
@Override
public int deleteAccount() {
System.out.println("刪除...");
return 0;
}
}
(3) Logger通知類
/**
* 用戶記錄日誌的工具類,它裏面提供了公共的代碼
*/
public class Logger {
/**
* 前置通知
*/
public void beforePrintLog() {
System.out.println("logger類中的 前置通知");
}
/**
* 後置通知
*/
public void afterReturningPrintLog() {
System.out.println("logger類中的 後置通知");
}
/**
* 異常通知
*/
public void afterThrowingPrintLog() {
System.out.println("logger類中的 異常通知");
}
/**
* 最終通知
*/
public void afterPrintLog() {
System.out.println("logger類中的 最終通知");
}
/**
* 環繞通知
* 問題:
* 當我們配置了環繞通知之後,切入點方法沒有執行,而通知方法執行了。
* 分析:
* 通過對比動態代理中的環繞通知代碼,發現動態代理的環繞通知有明確的切入點方法調用,而我們的代碼中沒有。
* 解決:
* Spring框架爲我們提供了一個接口:ProceedingJoinPoint。該接口有一個方法proceed(),此方法就相當於明確調用切入點方法。
* 該接口可以作爲環繞通知的方法參數,在程序執行時,spring框架會爲我們提供該接口的實現類供我們使用。
*
* spring中的環繞通知:
* 它是spring框架爲我們提供的一種可以在代碼中手動控制(寫入的位置)增強方法何時執行的方式。
*/
public Object aroundPrintLog(ProceedingJoinPoint pjp) {
Object rtValue = null;
try {
Object[] args = pjp.getArgs();//得到方法執行所需的參數
System.out.println("Logger類中的aroundPringLog方法開始記錄日誌了。。。前置");
rtValue = pjp.proceed(args);//明確調用業務層方法(切入點方法)
System.out.println("Logger類中的aroundPringLog方法開始記錄日誌了。。。後置");
return rtValue;
} catch (Throwable t) {
System.out.println("Logger類中的aroundPringLog方法開始記錄日誌了。。。異常");
throw new RuntimeException(t);
} finally {
System.out.println("Logger類中的aroundPringLog方法開始記錄日誌了。。。最終");
}
}
}
(4) bean.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
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 配置srping的Ioc,把service對象配置進來-->
<bean id="accountService" class="top.zoick.service.impl.AccountServiceImpl"></bean>
<!-- 配置Logger類 -->
<bean id="logger" class="top.zoick.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切入點表達式,id屬性用於指定表達式唯一標識 expression屬性用於指定表達式內容
此標籤寫在aop:aspect標籤內部只能當前切面使用。
它還可以寫在aop:aspect外面,此時就變成了所有切面可用。注意:由於約束的要求,若要寫在外面一定要放在aop:aspect前面
-->
<aop:pointcut id="pt1" expression="execution(* top.zoick.service.impl.*.*(..))"/>
<!--配置切面 -->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置前置通知: 在切入點方法執行之前執行
<aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>-->
<!-- 配置後置通知:在切入點方法正常執行之後執行,它和異常通知永遠只能執行一個
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>-->
<!-- 配置異常通知:在切入點方法執行產生異常後執行。他和後置通知永遠只能執行一個
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>-->
<!-- 配置最終通知,無論切入點方法是否正常執行它都會在其後面執行
<aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>-->
<!--配置環繞通知 詳細的註釋在Logger類中-->
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
</aop:config>
</beans>
spring中基於XML的AOP配置步驟
1、把通知Bean也交給spring來管理
2、使用aop:config標籤表明開始AOP的配置
3、使用aop:aspect標籤表明配置切面
id屬性:是給切面提供一個唯一標識
ref屬性:是指定通知類bean的Id。
4、在aop:aspect標籤的內部使用對應標籤來配置通知的類型
aop:before:表示配置前置通知
method屬性:用於指定Logger類中哪個方法是前置通知
ponitcut-ref:用於指定切入點的表達式的引用
poinitcut:用於指定切入點表達式
aop:after-returning: 後置通知:在切入點方法正常執行之後執行,它和異常通知永遠只能執行一個
aop:after-throwing: 異常通知:在切入點方法執行產生異常後執行。他和後置通知永遠只能執行一個
aop:after: 最終通知,無論切入點方法是否正常執行它都會在其後面執行
aop:around: 環繞通知 詳細的註釋在Logger類中,通常情況下,環繞通知都是獨立使用的
切入點表達式相關的注意:
切入點表達式的寫法:
關鍵字:execution(表達式)
表達式:
訪問修飾符 返回值 包名.包名.包名...類名.方法名(參數列表)
標準的表達式寫法:
public void top.zoick.service.impl.AccountServiceImpl.saveAccount()
訪問修飾符可以省略
void top.zoick.service.impl.AccountServiceImpl.saveAccount()
返回值可以使用通配符,表示任意返回值
* top.zoick.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包。但是有幾級包,就需要寫幾個*.
* *.*.*.*.AccountServiceImpl.saveAccount())
包名可以使用..表示當前包及其子包
* *..AccountServiceImpl.saveAccount()
類名和方法名都可以使用*來實現通配
* *..*.*()
參數列表:
可以直接寫數據類型:
基本類型直接寫名稱 int
引用類型寫包名.類名的方式 java.lang.String
可以使用通配符表示任意類型,但是必須有參數
可以使用..表示有無參數均可,有參數可以是任意類型
全通配寫法:
* *..*.*(..)
實際開發中切入點表達式的通常寫法:
切到業務層實現類下的所有方法
* top.zoick.service.impl.*.*(..)
<aop:pointcut>配置切入點表達式,id屬性用於指定表達式唯一標識 expression屬性用於指定表達式內容
此標籤寫在aop:aspect標籤內部只能當前切面使用。
它還可以寫在aop:aspect外面,此時就變成了所有切面可用。注意:由於約束的要求,若要寫在外面一定要放在aop:aspect前面
<aop:pointcut id="pt1" expression="execution(* top.zoick.service.impl.*.*(..))"/>
(5) 測試
public class AOTest {
public static void main(String[] args) {
//1.獲取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//2.獲取對象
IAccountService accountService = (IAccountService) ac.getBean("accountService");
//3.執行方法
accountService.saveAccount();
}
}
3、基於註解的 AOP 配置
(1) service類中impl.AccountServiceImpl
@Service("accountService")
public class AccountServiceImpl implements IAccountService {
/**
* 模擬保存賬戶
*/
@Override
public void saveAccount() {
System.out.println("保存...");
}
/**
* 模擬更新賬戶
* @param i
*/
@Override
public void updateAccount(int i) {
System.out.println("更新...");
}
/**
* 刪除賬戶
*
* @return
*/
@Override
public int deleteAccount() {
System.out.println("刪除...");
return 0;
}
}
(2) utils中的Logger
/**
* 用戶記錄日誌的工具類,它裏面提供了公共的代碼
*/
@Component("logger")
@Aspect
public class Logger {
@Pointcut("execution(* top.zoick.service.impl.*.*(..))")//切入點表達式
private void pt1(){}
/**
* 前置通知
*/
// @Before("pt1()")
public void beforePrintLog() {
System.out.println("logger類中的 前置通知");
}
/**
* 後置通知
*/
// @AfterReturning("pt1()")
public void afterReturningPrintLog() {
System.out.println("logger類中的 後置通知");
}
/**
* 異常通知
*/
// @AfterThrowing("pt1()")
public void afterThrowingPrintLog() {
System.out.println("logger類中的 異常通知");
}
/**
* 最終通知
*/
// @After("pt1()")
public void afterPrintLog() {
System.out.println("logger類中的 最終通知");
}
@Around("pt1()")
public Object aroundPrintLog(ProceedingJoinPoint pjp) {
Object rtValue = null;
try {
Object[] args = pjp.getArgs();//得到方法執行所需的參數
System.out.println("Logger類中的aroundPringLog方法開始記錄日誌了。。。前置");
rtValue = pjp.proceed(args);//明確調用業務層方法(切入點方法)
System.out.println("Logger類中的aroundPringLog方法開始記錄日誌了。。。後置");
return rtValue;
} catch (Throwable t) {
System.out.println("Logger類中的aroundPringLog方法開始記錄日誌了。。。異常");
throw new RuntimeException(t);
} finally {
System.out.println("Logger類中的aroundPringLog方法開始記錄日誌了。。。最終");
}
}
}
(3) bean.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
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<!--註解掃描-->
<context:component-scan base-package="top.zoick"/>
<!--配置spring開啓AOP註解-->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
</beans>