Spring 學習筆記②:動態代理及面向切面編程


收藏這三篇筆記,完整回顧Spring常見問題及使用方式速查:

  1. Spring 學習筆記①:IoC容器、Bean與注入
  2. Spring 學習筆記②:動態代理及面向切面編程(即本篇)
  3. Spring 學習筆記③:JDBC與事務管理

0. 基本概念

  • 面向切面編程,它將業務邏輯的各個部分進行隔離,使開發人員在編寫業務邏輯時可以專心於核心業務,從而提高了開發效率。
  • 關注點(切入點)代碼,就是指重複執行的代碼。
  • 業務代碼與關注點代碼分離:關注點代碼寫一次即可;開發者只需要關注核心業務,運行時期,執行核心業務代碼時候動態植入關注點代碼。
   // 關注點代碼舉例
   public void add(User user) { 
       Session session = null; 
       Transaction trans = null; 
       try { 
           session = HibernateSessionFactoryUtils.getSession();   // 【關注點代碼】
           trans = session.beginTransaction();    // 【關注點代碼】
           session.save(user);     // 核心業務代碼:如何保存、用戶有效性校驗
           trans.commit();     //…【關注點代碼】
       } catch (Exception e) {     
           e.printStackTrace(); 
           if(trans != null){ 
               trans.rollback();   //..【關注點代碼】
           } 
       } finally{ 
           HibernateSessionFactoryUtils.closeSession(session);   ////..【關注點代碼】
   
       } 
   }

1. AOP概念及術語

術語 解釋
Joinpoint(連接點) 指那些被攔截到的點,在 Spring 中,可以被動態代理攔截目標類的方法。
Pointcut(切入點) 指要對哪些 Joinpoint 進行攔截,即被攔截的連接點(方法)。
Advice(通知) 指攔截到 Joinpoint 之後要做的事情,即對切入點增強的內容
Target(目標) 指代理的目標對象。
Weaving(植入) 指把增強代碼應用到目標上,生成代理對象的過程。
Proxy(代理) 指生成的代理對象。
Aspect(切面) 切入點和通知的結合。

2. 動態代理

2.1 代理模式

爲其他對象提供一個代理以控制對某個對象的訪問,代理類不現實具體服務,而是利用委託類來完成服務,並將執行結果封裝處理。在Spring中被用來做無侵入的代碼增強。

和裝飾器模式有什麼不同?答:不會產生太多的裝飾類。

2.1.1 靜態代理

  1. 被代理類承擔、插入自己的方法。
  2. 創建一個代理類,持有被代理類(目標對象)的引用,實現接口(但具體業務由創建一個接口,被代理類(目標對象)實現接口。

顯然,一個代理類只能代理一個目標對象,會造成目標類的泛濫。這也是“靜態”的意思。


業務邏輯的接口:

public interface TargetInterface {
    void doSomething();
}

目標對象:

public class TargetImpl implements TargetInterface{
    @Override
    public void doSomething() {
        System.out.println("Hello World!");
    }
}

代理類:

public class TargetProxy implements TargetInterface{
    private TargetInterface target = new TargetImpl(); // 持有引用
    @Override
    public void doSomething() {
        System.out.println("Before invoke" );
        this.target.doSomething();
        System.out.println("After invoke");
    }
}

UML圖:

image.png

2.1.2 動態代理

  1. 目標接口和目標對象和靜態代理類一致。
  2. 運用反射來創建代理類。

代理類對象:

   public class ProxyHandler implements InvocationHandler{
       private Object object;
       public ProxyHandler(Object object){
           this.object = object;
       }
       
       @Override
       public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
           System.out.println("Before invoke");
           method.invoke(object, args);
           System.out.println("After invoke");
           return null;
       }
   }

最後基於反射完成代理過程,詳見2.2小節:

InvocationHandler handler = new ProxyHandler(new TargetImpl());
TargetInterface targetProxy = (TargetInterface) Proxy.newProxyInstance(TargetImpl.getClassLoader(), TargetImpl.getInterfaces(), handler);
targetProxy.doSomething();

2.1.3 代理模式的缺點

  • 靜態代理:由於靜態代理需要實現目標對象的相同接口,那麼可能會導致代理類會非常非常多,不好維護。
  • 動態代理:目標對象一定是要有接口的,沒有接口就不能實現動態代理。

2.2 java.lang.reflect.Proxy

  • java.lang.reflect.Proxy 是基於反射的動態代理,是屬於JDK的原生實現。

2.2.1 實現Invoke接口、注入增強代碼

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

例如:

   public class ProxyHandler implements InvocationHandler{
       private Object object;
       public ProxyHandler(Object object){
           this.object = object;
       }
       
       @Override
       public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
           System.out.println("Before invoke");  // 增強代碼1
           Object obj = method.invoke(object, args);
           System.out.println("After invoke"); // 增強代碼2
           return obj;
       }
   }

2.2.2 基於JDK的Proxy獲得代理對象

利用 static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces,InvocationHandler invocationHandler ) 構建代理對象。其中各參數如下:

  1. ClassLoader loader :指定當前target對象使用類加載器,獲取加載器的方法是固定的,如 TargetInterface.class.getClassLoader()
  2. Class<?>[] interfaces :target對象實現的接口的類型,使用泛型方式確認類型,如 new Class[] { TargetInterface.class}
  3. InvocationHandler invocationHandler :事件處理,執行target對象的方法時,會觸發事件處理器的方法,會把當前執行target對象的方法作爲參數傳入。

2.3 CGLib

相較於基於JDK的動態代理仍有侷限性,即其目標對象必須要實現至少一個接口。而借用CGlib則不需要,其憑藉一個小而快的字節碼處理框架ASM轉換字節碼並生成新的類。由於其基於內存構建出一個子類來擴展目標對象的功能,也被稱爲“子類代理”。

需要注意的是,目標類不能爲不可繼承的 final 類型或目標對象的方法不能爲靜態類型。

public class TargetProxyFactory {
    public static TargetProxy getProxyBean() {
        // 1. 準備目標類和自定義的切面類(用於增強目標對象)
        final Target goodsDao = new Target();
        final Aspect aspect = new Aspect();
        // 2. 構建CgLib的核心類`Enhancer`
        Enhancer enhancer = new Enhancer(); 
        // 3. 確定需要增強的類
        enhancer.setSuperclass(goodsDao.getClass());
        // 4. 添加回調函數:實現一個MethodInterceptor接口
        enhancer.setCallback(() -> {
            // intercept 相當於 jdk invoke,前三個參數與 jdk invoke—致
            @Override
            public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                aspect.myBefore(); // 前增強
                Object obj = method.invoke(goodsDao, args); // 目標方法執行
                aspect.myAfter(); // 後增強
                return obj;
            }
        });
        // 5. 創建代理類
        TargetProxy targetProxy = (TargetProxy) enhancer.create();
        return targetProxy;
    }
}

構建CGLib依賴的pom.xml 文件爲:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

3. Spring中的AOP

3.1 pom.xml文件

<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aop</artifactId>
  <version>5.2.6.RELEASE</version>
</dependency>

<!-- Spring 2.0 以後,新增了對 AspectJ 方式的支持,新版本的 Spring 框架,建議使用 AspectJ 方式開發 AOP -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aspects</artifactId>
  <version>5.2.6.RELEASE</version>
</dependency>

3.2 基於通知類型代理增強的Bean

3.2.1 AOP通知類型

  • 通知(Advice)其實就是對目標切入點進行增強的內容。
名稱 說明
org.springframework.aop.MethodBeforeAdvice(前置通知) 在方法之前自動執行的通知稱爲前置通知,可以應用於權限管理等功能。
org.springframework.aop.AfterReturningAdvice(後置通知) 在方法之後自動執行的通知稱爲後置通知,可以應用於關閉流、上傳文件、刪除臨時文件等功能。
org.aopalliance.intercept.MethodInterceptor(環繞通知) 在方法前後自動執行的通知稱爲環繞通知,可以應用於日誌、事務管理等功能。
org.springframework.aop.ThrowsAdvice(異常通知) 在方法拋出異常時自動執行的通知稱爲異常通知,可以應用於處理異常記錄日誌等功能。
org.springframework.aop.IntroductionInterceptor(引介通知) 在目標類中添加一些新的方法和屬性,可以應用於修改舊版本程序(增強類)。

3.2.2 示例:攔截器與工廠方法

現在,假設要增強 UserDao ,切入點是 save() 方法,要在之前加入自己面向 User 的增強方法,如校驗等切面業務。核心要點有:

  1. 目標對象要實現一個通用接口(除非使用CGlib)。
  2. 代碼增強類(切面類)實現一種通知的接口,在其中做增強。
  3. 在配置文件中利用 org.springframework.aop.framework.ProxyFactoryBean 創建代理類,需要給出 proxyInterfaces (目標對象的接口)、 target (目標對象的引用)、 interceptorNames (攔截器/切面類的名字)。

@Repository("userDao")
public class UserDao implements UserDaoInterface { // 實現一個通用接口
    public void save(User user){
        System.out.println("數據庫已保存" + user); // 業務代碼
    }
}

代碼切面類:

public class UserDaoAspect implements MethodInterceptor { // 此處以環繞通知爲例子
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("Dao enhanced before"); // 增強1(重複的切入點代碼)
        Object obj = methodInvocation.proceed();   // 這裏會由Spring替我們注入target對象
        System.out.println("Dao enhanced after");  // 增強2(重複的切入點代碼)
        return obj;
    }
}

創建配置文件:

   <beans>
       <bean id="userDaoAspect" class="MVC.Aspect.UserDaoAspect"/> <!-- 攔截器/切面類-->
   
       <bean id="targetUserDao" class="MVC.Model.Dao.UserDao"/> <!-- 目標類 -->
       
       <bean id="userDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
           <!--代理對象實現的接口、目標對象、攔截者(切面類) -->
           <property name="proxyInterfaces" value="MVC.Model.Dao.UserDaoInterface"/>
           <property name="target" ref="targetUserDao"/> <!-- 此處是一個引用ref -->
           <property name="interceptorNames" value="userDaoAspect"/>
           <!-- 如何生成代理,true:使用CGLib; false :使用JDK動態代理(默認) -->
           <property name="proxyTargetClass" value="true"/>
       </bean>
   </beans>

其中, UserService 類需要進行修改:

   @Service("userService")
   public class UserService {
   
       @Resource(name = "userDaoProxy")  // 注入ProxyFactoryBean工廠方法獲得的代理類(增強類)
       private UserDaoInterface userDao; // 修改爲其接口
   
       public void service(User user){
           System.out.println("MVC Service sth. with " + user);
           this.userDao.save(user);
           System.out.println("MVC Service Over.");
       }
   }

工程結構:

TIM截圖20200609230711.png

3.3 使用AspectJ開發AOP(推薦)

  • AspectJ 是一個基於 Java 語言的 AOP 框架,它擴展了 Java 語言。Spring 2.0 以後,新增了對 AspectJ 方式的支持,新版本的 Spring 框架,建議使用 AspectJ 方式開發 AOP。
  • 主要有兩種開發方法:“基於XML的聲明式開發”和“基於註解的聲明式開發”。

3.3.1 示例①:基於XML的聲明式開發

需要引入命名空間:

<beans xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
            http://www.springframework.org/schema/aop
            http://www.springframework.org/schema/aop/spring-aop.xsd">

編寫切面類:

   public class UserDaoAspect { // 以前、後通知爲例
       public void asBefore() {
           System.out.println("Dao enhanced before"); // 一些重複的代碼
       }
   
       public void asAfter() {
           System.out.println("Dao enhanced after");
       }
   }

被增強的目標類(不再需要接口):

   @Repository("userDao")
   public class UserDao {
       public void save(User user){
           System.out.println("數據庫已保存" + user); // 業務代碼
       }
   }

編寫配置文件:

   <beans>
       <bean id="userDaoAspect" class="MVC.Aspect.UserDaoAspect"/> <!-- 切面類 -->
   
       <bean id="targetUserDao" class="MVC.Model.Dao.UserDao"/> <!-- 目標類(隨後都會變成代理類) -->
   
       <!-- 面向切面編程,交由Spring管理-->
       <aop:config>
           <!-- 配置切入點 -->
           <aop:pointcut expression="execution(* MVC.Model.Dao.UserDao.save(..))" id="pointcut-save"/>
   
           <!-- 對切入點配置切面 -->
           <aop:aspect ref="userDaoAspect">
               <!-- 配置前置增強 -->
               <aop:before method="asBefore" pointcut-ref="pointcut-save" />
               <aop:after method="asAfter" pointcut-ref="pointcut-save"/>
           </aop:aspect>
       </aop:config>
       <!-- 如何生成代理,true:使用CGLib; false :使用JDK動態代理(默認)|如果目標類沒有聲明接口,則Spring將自動使用CGLib動態代理 -->
       <aop:aspectj-autoproxy  proxy-target-class="true"/>
   </beans>

注意此處 proxy-target-class="false" 的話注入會報錯 ...but was actually of type ‘com.sun.proxy.$Proxy7'

獲取增強類:

 UserDao userDaoProxy = (UserDao) applicationContext.getBean("targetUserDao");

3.3.2 示例中的標籤及對應的切面類解析

<aop> 標籤格式概覽:

  <aop:config>
    <!-- 配置切入點,通知最後增強哪些方法 -->
    <aop:pointcut expression="execution ( * target.* (..))" id="pointcut-id-x" />
    
    <aop:aspect ref="myAspect">
      <!--前置通知,關聯通知 Advice和切入點PointCut -->
      <aop:before method="myBefore" pointeut-ref="pointcut-id-x" />
      
      <!--後置通知,在方法返回之後執行,就可以獲得返回值returning 屬性 -->
      <aop:after-returning method="myAfterReturning" pointcut-ref="pointcut-id-x" returning="returnVal" />
      
      <!--環繞通知 -->
      <aop:around method="myAround" pointcut-ref="pointcut-id-x" />
      
      <!--拋出通知:用於處理程序發生異常,可以接收當前方法產生的異常 -->
      <!-- * 注意:如果程序沒有異常,則不會執行增強 -->
      <!-- * throwing屬性:用於設置通知第二個參數的名稱,類型Throwable -->
      <aop:after-throwing method="myAfterThrowing" pointcut-ref="pointcut-id-x" throwing="e" />
      
      <!--最終通知:無論程序發生任何事情,都將執行 -->
      <aop:after method="myAfter" pointcut-ref="pointcut-id-x" />
    </aop:aspect>
  </aop:config>

對應的切面類:

   //切面類
   public class MyAspect {
       // 前置通知
       public void myBefore(JoinPoint joinPoint) {
           System.out.print("前置通知,目標:" + joinPoint.getTarget() + " 方法名稱: " + joinPoint.getSignature().getName());
       }
       
       // 後置通知
       public void myAfterReturning(JoinPoint joinPoint) {
           System.out.print("後置通知,方法名稱:" + joinPoint.getSignature().getName());
       }
       
       // 環繞通知
       public Object myAround(ProceedingJoinPoint proceedingJoinPoint)
               throws Throwable {
           System.out.println("環繞開始"); // 開始
           Object obj = proceedingJoinPoint.proceed(); // 執行當前目標方法
           System.out.println("環繞結束"); // 結束
           return obj;
       }
       
       // 異常通知
       public void myAfterThrowing(JoinPoint joinPoint, Throwable e) {
           System.out.println("異常通知" + "出錯了" + e.getMessage());
       }
       
       // 最終通知
       public void myAfter() {
           System.out.println("最終通知");
       }
   }

3.3.3 示例②:基於註解的聲明式開發

名稱 說明
@Aspect 用於定義一個切面。
@Before 用於定義前置通知,相當於 BeforeAdvice。
@AfterReturning 用於定義後置通知,相當於 AfterReturningAdvice。
@Around 用於定義環繞通知,相當於MethodInterceptor。
@AfterThrowing 用於定義拋出通知,相當於ThrowAdvice。
@After 用於定義最終final通知,不管是否異常,該通知都會執行。
@DeclareParents 用於定義引介通知,相當於IntroductionInterceptor。

編寫配置文件:

<context:component-scan base-package="MVC"/> <!-- 掃描註解 -->
<aop:aspectj-autoproxy  proxy-target-class="true"/> <!-- 使用CGlib植入切面代碼 -->

構建切面類:

   @Aspect
   @Component
   public class UserDaoAspect {
       @Pointcut("execution(* MVC.Model.Dao.UserDao.save(..))") // 配置切入點
       private void pointCut(){} // 要求:方法必須是private,沒有值,名稱自定義,沒有參數
       
       @Before("pointCut()")
       public void asBefore() {
           System.out.println("Dao enhanced before");
       }
       @After("pointCut()")
       public void asAfter() {
           System.out.println("Dao enhanced after");
       }
   }

3.4 一對多的切面及相關問題

3.4.1 實現多切面的有序運行

  • 當有多個切面時,它不會存在任何順序,這些順序代碼會隨機生成,但是有時候我們希望它按照指定的順序運行。
  • 此時,需要藉助 org.springframework.core.annotation.Order 註解類或實現 org.springframework.core.Ordered 接口。

構建一個新的切面:

   @Aspect
   @Component
   @Order(value = 2) // 會在第二個執行
   public class UserDaoAspect2 {
       @Pointcut("execution(* MVC.Model.Dao.UserDao.save(..))")
       private void pointCut(){} // 要求:方法必須是private,沒有值,名稱自定義,沒有參數
   
   
       @Before("pointCut()")
       public void asBefore() {
           System.out.println("Dao enhanced before 2");
       }
   
       @After("pointCut()")
       public void asAfter() {
           System.out.println("Dao enhanced after 2");
       }
   }

3.4.2 在註解中實現多個切入點

假設有一個新的業務需要被 UserDao 切入:

@Repository
public class ShopDao {
    public void load(){
        System.out.println("載入商品");
    }
}

UserDao 需要修改爲:

   @Aspect
   @Component
   public class UserDaoAspect {
       @Pointcut("execution(* MVC.Model.Dao.UserDao.save(..))")
       private void pointCut(){} 
   
       @Pointcut("execution(* MVC.Model.Dao.ShopDao.load())")
       private void pointCut2(){} // 新的切入點
   
       @Before("pointCut()")
       public void asBefore() {
           System.out.println("Dao enhanced before");
       }
   
       @After("pointCut() || pointCut2()") // 修改表達式語句,植入代碼
       public void asAfter() {
           System.out.println("Dao enhanced after");
       }
   }

3.4.3 獲取代理對象的目標對象

由於被CGLib植入之後,IoC容器中所有的目標對象都會變成代理對象,且Spring沒有提供獲取原生對象的API。

參考解決方法:CSDN@在spring中獲取代理對象代理的目標對象工具類

import java.lang.reflect.Field;  
import org.springframework.aop.framework.AdvisedSupport;  
import org.springframework.aop.framework.AopProxy;  
import org.springframework.aop.support.AopUtils;  
  
public class AopTargetUtils {  
    public static Object getTarget(Object proxy) throws Exception {  
        return !AopUtils.isAopProxy(proxy) ? proxy :
            (AopUtils.isJdkDynamicProxy(proxy) ? getJDKDynamicProxyTargetObject(proxy) : getCGlibProxyTargetObject(proxy))
    }  
    // 獲取CGLib 代理的對象
    private static Object getCGlibProxyTargetObject(Object proxy) throws Exception { 
        Field h = proxy.getClass().getDeclaredField("CGLIB$CALLBACK_0");  
        h.setAccessible(true);  
        Object dynamicAdvisedInterceptor = h.get(proxy);  
        Field advised = dynamicAdvisedInterceptor.getClass().getDeclaredField("advised");  
        advised.setAccessible(true);  
        return ((AdvisedSupport)advised.get(dynamicAdvisedInterceptor)).getTargetSource().getTarget();  
    }  
    // 獲取JDK代理的對象
    private static Object getJDKDynamicProxyTargetObject(Object proxy) throws Exception {  
        Field h = proxy.getClass().getSuperclass().getDeclaredField("h");  
        h.setAccessible(true);  
        AopProxy aopProxy = (AopProxy) h.get(proxy); 
        Field advised = aopProxy.getClass().getDeclaredField("advised");  
        advised.setAccessible(true);  
        return ((AdvisedSupport)advised.get(aopProxy)).getTargetSource().getTarget();  
    }
} 

3.5 切入點表達式

切入點表達式爲:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

符號講解

  • ? 號代表0或1,表明可選參數。
  • * 號代表任意類型取0或多,常用作通配符。

表達式匹配參數講解

  • modifiers-pattern? :【可選】連接點的類型。
  • ret-type-pattern :【必填】連接點返回值類型,常用 * 做匹配。
  • declaring-type-pattern? :【可選】連接點的類型(包.類),如 com.example.User ,通常不省略。
  • name-pattern(param-pattern) :【必填】要匹配的連接點名稱,即 方法 (如果給出了連接點的類型,要用 . 隔開),如 save(..) ;括號裏面是方法的參數(匹配方法見下)。
  • throws-pattern? :【可選】連接點拋出的異常類型。

方法參數****的匹配方法:

  • () 匹配不帶參數的方法。
  • (..) 匹配帶參數的方法(任意個)。
  • (*, String) 匹配帶兩個參數的方法且第二個必爲String。

3.6 PointCut指示符

除了使用 execution 作爲切入點表達式進行配置,還可以使用以下表達式內容(需要保證所有的連接點都在IoC容器內):

  • within :匹配所有在指定子包內的類的連接點,如 within(com.xyz.service.*)within(com.xyz.service..*);嚴格匹配目標對象,不理會繼承關係 。
  • this : 匹配所有代理對象爲目標類型中的連接點,如this(com.xyz.service.AccountService)
  • target :匹配所有實現了指定接口的目標對象中的連接點,如 target(com.xyz.service.UserDaoInterfece)
  • bean :匹配所有指定命名方式的類的連接點,如 bean(userDao)
  • args :匹配任何方法參數是指定的類型的連接點,如 args(*, java.lang.String)args(java.lang.Long, ..)
  • @within :匹配標註有指定註解的類(不可爲接口)的所有連接點(要求註解的Retention級別爲CLASS),如 @within(com.google.common.annotations.Beta) ;對子類不起效,除非使用 @within(xxxx)+ 或者子類中繼承的方法未進行重載。
  • @target :匹配標註有指定註解的類(不可爲接口)的所有連接點(要求註解的Retention級別爲RUNTIME),如 @target(org.springframework.stereotype.Repository) ;對子類不起效。
  • @args :匹配傳入的參數類標註有指定註解的所有連接點,如 @args(org.springframework.stereotype.Repository)
  • @anntation :匹配所有標註有指定註解的連接點,如 @annotation(com.aop.annotation.AdminOnly)

除此之外,表達式還可以用 &&||! 進行合併,詳見3.4.2小節。

@within 和 @target的區別:

  • @within:若當前類有註解,則該類對父類重載及自有方法被攔截。子類中未對父類的方法進行重載時,亦被攔截。
  • @target:若當前類有註解,則該類對父類繼承、重載及自有的方法被攔截;對子類不起效。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章