spring AOP是什麼?你都拿它做什麼?
對於最近博主最近寫博客的興致大發,我也在思考:爲什麼而寫博客?在互聯網時代,無論你是牛人大咖,還是小白菜鳥,都有發表自己看法的權利。無論你是對的還是錯的,都會在這個平臺上找到答案。所以,我會儘可能去寫自己感興趣的內容,無論正面或者負面的消息,都儘可能回覆我的每一位讀者。即使自己只有一個讀者,也會堅持寫下去。有一個平臺,去表達自己,記錄自己的點滴,難道不是一種快樂嗎?同樣,瞭解技術是一個深入和拓展的過程,需要一個人清晰嚴謹的邏輯思維。有時候,寫博客更像是給自己做筆記,鞏固分散的知識!
上一篇文章中,我對spring源碼進行了分析討論,此處不再贅述,有興趣的同學可以看看向spring大佬低頭–大量源碼流出解析,本文是對上一篇文章的一個補充。回到正題,爲什麼會有面向切面編程(AOP)?我們知道java是一個面向對象(OOP)的語言,但它有一些弊端,比如當我們需要爲多個不具有繼承關係的對象引入一個公共行爲,例如日誌,權限驗證,事務等功能時,只能在在每個對象裏引用公共行爲,這樣做不便於維護,而且有大量重複代碼。AOP的出現彌補了OOP的這點不足。
爲了闡述清楚spring AOP,我們從將以下方面進行討論:
1.代理模式。
2.靜態代理原理及實踐。
3.動態代理原理及實踐。
4.spring AOP原理及實戰。
1.代理模式。
代理模式:爲其他對象提供一種代理以控制對這個對象的訪問。這段話比較官方,但我更傾向於用自己的語言理解:比如A對象要做一件事情,在沒有代理前,自己來做,在對A代理後,由A的代理類B來做。代理其實是在原實例前後加了一層處理,這也是AOP的初級輪廓。
2.靜態代理原理及實踐。
靜態代理模式:靜態代理說白了就是在程序運行前就已經存在代理類的字節碼文件,代理類和原始類的關係在運行前就已經確定。廢話不多說,我們看一下代碼,爲了方便閱讀,博主把單獨的class文件合併到接口中,讀者可以直接複製代碼運行:
package test.staticProxy;
// 接口
public interface IUserDao {
void save();
void find();
}
//目標對象
class UserDao implements IUserDao{
@Override
public void save() {
System.out.println("模擬:保存用戶!");
}
@Override
public void find() {
System.out.println("模擬:查詢用戶");
}
}
/**
靜態代理
特點:
1. 目標對象必須要實現接口
2. 代理對象,要實現與目標對象一樣的接口
*/
class UserDaoProxy implements IUserDao{
// 代理對象,需要維護一個目標對象
private IUserDao target = new UserDao();
@Override
public void save() {
System.out.println("代理操作: 開啓事務...");
target.save(); // 執行目標對象的方法
System.out.println("代理操作:提交事務...");
}
@Override
public void find() {
target.find();
}
}
測試結果:
靜態代理雖然保證了業務類只需關注邏輯本身,代理對象的一個接口只服務於一種類型的對象,如果要代理的方法很多,勢必要爲每一種方法都進行代理。再者,如果增加一個方法,除了實現類需要實現這個方法外,所有的代理類也要實現此方法。增加了代碼的維護成本。那麼要如何解決呢?答案是使用動態代理。
3.動態代理原理及實踐。
動態代理模式:動態代理類的源碼是在程序運行期間通過JVM反射等機制動態生成,代理類和委託類的關係是運行時才確定的。實例如下:
package test.dynamicProxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
// 接口
public interface IUserDao {
void save();
void find();
}
//目標對象
class UserDao implements IUserDao{
@Override
public void save() {
System.out.println("模擬: 保存用戶!");
}
@Override
public void find() {
System.out.println("查詢");
}
}
/**
* 動態代理:
* 代理工廠,給多個目標對象生成代理對象!
*
*/
class ProxyFactory {
// 接收一個目標對象
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
// 返回對目標對象(target)代理後的對象(proxy)
public Object getProxyInstance() {
Object proxy = Proxy.newProxyInstance(
target.getClass().getClassLoader(), // 目標對象使用的類加載器
target.getClass().getInterfaces(), // 目標對象實現的所有接口
new InvocationHandler() { // 執行代理對象方法時候觸發
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
// 獲取當前執行的方法的方法名
String methodName = method.getName();
// 方法返回值
Object result = null;
if ("find".equals(methodName)) {
// 直接調用目標對象方法
result = method.invoke(target, args);
} else {
System.out.println("開啓事務...");
// 執行目標對象方法
result = method.invoke(target, args);
System.out.println("提交事務...");
}
return result;
}
}
);
return proxy;
}
}
測試結果如下:
在運行測試類中創建測試類對象代碼中
IUserDao proxy = (IUserDao)new ProxyFactory(target).getProxyInstance();
其實是jdk動態生成了一個類去實現接口,隱藏了這個過程:
class $jdkProxy implements IUserDao{}
使用jdk生成的動態代理的前提是目標類必須有實現的接口。但這裏又引入一個問題,如果某個類沒有實現接口,就不能使用jdk動態代理,所以Cglib代理就是解決這個問題的。
Cglib是以動態生成的子類繼承目標的方式實現,在運行期動態的在內存中構建一個子類,如下:
public class UserDao{}
//Cglib是以動態生成的子類繼承目標的方式實現,程序執行時,隱藏了下面的過程
public class $Cglib_Proxy_class extends UserDao{}
Cglib使用的前提是目標類不能爲final修飾。因爲final修飾的類不能被繼承。
現在,我們可以看看AOP的定義:面向切面編程,核心原理是使用動態代理模式在方法執行前後或出現異常時加入相關邏輯。
通過定義和前面代碼我們可以發現3點:
1.AOP是基於動態代理模式。
2.AOP是方法級別的(要測試的方法不能爲static修飾,因爲接口中不能存在靜態方法,編譯就會報錯)。
3.AOP可以分離業務代碼和關注點代碼(重複代碼),在執行業務代碼時,動態的注入關注點代碼。切面就是關注點代碼形成的類。
4.spring AOP原理及實戰。
前文提到jdk代理和cglib代理兩種動態代理,優秀的spring框架把兩種方式在底層都集成了進去,我們無需擔心自己去實現動態生成代理。那麼,spring是如何生成代理對象的?:
1.創建容器對象的時候,根據切入點表達式攔截的類,生成代理對象。
2.如果目標對象有實現接口,使用jdk代理。如果目標對象沒有實現接口,則使用cglib代理。然後從容器獲取代理後的對象,在運行期植入”切面”類的方法。通過查看spring源碼,我們在DefaultAopProxyFactory類中,找到這樣一段話。
簡單的從字面意思看出,如果有接口,則使用Jdk代理,反之使用Cglib,這剛好印證了前文所闡述的內容。spring AOP綜合兩種代理方式的使用前提有會如下結論:如果目標類沒有實現接口,且class爲final修飾的,則不能進行spring AOP編程!
知道了原理,現在我們將自己手動實現spring的AOP:
package test.spring_aop_anno;
import org.aspectj.lang.ProceedingJoinPoint;
public interface IUserDao {
void save();
}
//用於測試Cglib動態代理
class OrderDao {
public void save() {
//int i =1/0;用於測試異常通知
System.out.println("保存訂單...");
}
}
//用於測試jdk動態代理
class UserDao implements IUserDao {
public void save() {
//int i =1/0;用於測試異常通知
System.out.println("保存用戶...");
}
}
//切面類
class TransactionAop {
public void beginTransaction() {
System.out.println("[前置通知] 開啓事務..");
}
public void commit() {
System.out.println("[後置通知] 提交事務..");
}
public void afterReturing(){
System.out.println("[返回後通知]");
}
public void afterThrowing(){
System.out.println("[異常通知]");
}
public void arroud(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("[環繞前:]");
pjp.proceed(); // 執行目標方法
System.out.println("[環繞後:]");
}
}
spring的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:context="http://www.springframework.org/schema/context"
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/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- dao實例加入容器 -->
<bean id="userDao" class="test.spring_aop_anno.UserDao"></bean>
<!-- dao實例加入容器 -->
<bean id="orderDao" class="test.spring_aop_anno.OrderDao"></bean>
<!-- 實例化切面類 -->
<bean id="transactionAop" class="test.spring_aop_anno.TransactionAop"></bean>
<!-- Aop相關配置 -->
<aop:config>
<!-- 切入點表達式定義 -->
<aop:pointcut expression="execution(* test.spring_aop_anno.*Dao.*(..))" id="transactionPointcut"/>
<!-- 切面配置 -->
<aop:aspect ref="transactionAop">
<!-- 【環繞通知】 -->
<aop:around method="arroud" pointcut-ref="transactionPointcut"/>
<!-- 【前置通知】 在目標方法之前執行 -->
<aop:before method="beginTransaction" pointcut-ref="transactionPointcut" />
<!-- 【後置通知】 -->
<aop:after method="commit" pointcut-ref="transactionPointcut"/>
<!-- 【返回後通知】 -->
<aop:after-returning method="afterReturing" pointcut-ref="transactionPointcut"/>
<!-- 異常通知 -->
<aop:after-throwing method="afterThrowing" pointcut-ref="transactionPointcut"/>
</aop:aspect>
</aop:config>
</beans>
切入點表達式不在這裏介紹。ref:Spring AOP 切入點表達式
代碼的測試結果如下:
到這裏,我們已經全部介紹完spring AOP,回到開篇的問題,我們拿它做什麼?
1.Spring聲明式事務管理配置,博主的另一篇文章:分佈式系統架構實戰demo:ssm+dubbo
2.Controller層的參數校驗。ref:spring aop攔截Controller做參數校驗
3.使用Spring AOP實現MySQL數據庫讀寫分離案例分析
4.在執行方法前,判斷是否具有權限。
5.,對部分函數的調用進行日誌記錄。監控部分重要函數,若拋出指定的異常,可以以短信或郵件方式通知相關人員。
6.信息過濾,頁面轉發等等功能,博主一個人的力量有限,只能列舉這麼多,歡迎評論區對文章做補充。
spring AOP還能做什麼,實現什麼魔幻功能,就在於我們每一個平凡而又睿智的程序猿!