Spring AOP


Spring整合單元測試

在前面的案例中我么需要自己創建ApplicationContext對象,然后在調用getBean來獲取需要測試的Bean

Spring提供了一種更加方便的方式來創建測試所需的ApplicationContext,並且可以幫助我們把需要測試的Bean直接注入到測試類中

添加依賴:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.2.2.RELEASE</version>
</dependency>

測試代碼:

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

@RunWith(SpringJUnit4ClassRunner.class)//固定寫法
@ContextConfiguration("classpath:applicationContext.xml") //指定加載的配置文件
public class MyTest2 {
    @Resource(name = "userService")   //直接使用DI獲取Bean
    private UserService userService;

    @Test
    public void test(){
        userService.getUserDao().save();//測試
    }
}

AOP概念

在軟件業,AOP為Aspect Oriented Programming的縮寫,翻譯為:面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點,也 是Spring框架中的一個重要內容,是函數式編程的一種衍生范型。利用AOP可以對業務邏輯的各個部分 進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效 率。

以上內容來自百度百科,看完你會發現這玩兒說個啥呢?看完和沒看差不多,太過於抽象,咱們還是帶着問題來看AOP吧;

為什么需要AOP

案例分析

在項目開發中我們經常遇到一系列通用需求比如:權限控制,日志輸出,事務管理,數據統計等,這些看似簡單的需求,在實際開發中卻會帶來麻煩,舉個例子:

在某個的Dao層如UserDao,存在以下幾個方法

public class UserDao{
  public void save(){
    System.out.println("save sql");
  }
  public void delete(){
    System.out.println("delete sql");
  }
  public void update(){
    System.out.println("update sql");
  }
}

在第一個版本中,已經實現了程序的實際功能,但是后來發現數據庫操作出現瓶頸,這時領導說要對這些方法進行執行時間統計,並輸出日志分析問題;

解決方案

這點小需求對於你來說太easy了,於是你打開了代碼,熟練的添加代碼

public class UserDao{
    public void save(){
      	//獲取類名和方法名
        String className = Thread.currentThread().getStackTrace()[1].getClassName();
        String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
      	//開始時間
        long startTime = new Date().getTime();
      
				//原邏輯
        System.out.println("save sql");
      	
      	//耗時
        long runTime = new Date().getTime() - startTime;
        System.out.printf("info [class:%s method:%s runtime:%s]",className,methodName,runTime);
    }
    public void delete(){
        String className = Thread.currentThread().getStackTrace()[1].getClassName();
        String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
        long startTime = new Date().getTime();

        System.out.println("delete sql");

        long runTime = new Date().getTime() - startTime;
        System.out.printf("info [class:%s method:%s runtime:%s]",className,methodName,runTime);
    }
    public void update(){
	    System.out.println("update sql");
  	}

    public static void main(String[] args) {
        new UserDao().save();
    }
}

問題

需求實現了,但是作為優秀的開發工程師你當然不會就這么完事了,因為上述代碼存在以下問題:

  • 1.修改了源代碼(違反了OCP,違反了對修改封閉,但滿足調用方式不變)

  • 2.大量重復代碼

再看AOP

我們先不考慮如何解決這些問題,其實AOP之所以出現就是因為,我們需要對一些已經存在的方法進行功能擴展,但是又不能通過修改源代碼或改變調用方式的手段來解決

反過來說就是要在保證不修改源代碼以及調用方式不變的情況下為原本的方法增加功能

而由於需要擴展的方法有很多,於是把這些方法稱作一個切面,即切面就是一系列需要擴展功能的方法的集合

AOP的目的

將日志記錄,性能統計,安全控制,事務處理,異常處理等重復代碼從業務邏輯代碼中划分 出來,通過對這些行為的分離,我們希望可以將它們獨立到非業務邏輯的方法中,進而改變這些行為的時候不會影響業務邏輯的代碼。

吐槽:

直接看名字的確是比較抽象的,沒辦法,當你創造了一個全新的東西時,你往往也會想給它取一個nb的名字,而這個解決方案是針對一些固定場景的,我們很難找到一個非常准確的名字去描述這個方案

假設你想出了一個解決方案,那么你會給他取個什么名字呢?

AOP相關術語

AOP這一概念是AOP聯盟aopalliance提出的,相關的概念也出自aopalliance定義

  • 連接點(joinpoint)

是擴展內容與原有內容的交互的點,可以理解為可以被擴展的地方,通常是一個方法,而AspectJ中也支持屬性作為連接點

示例:案例中的三個方法

  • 切點(pointcut)

切點指的是要被擴展(增加了功能)的內容,包括方法或屬性(joinpoint)

示例:案例中的兩個增加了功能的方法

  • 通知(adivce)

通知指的是要在切點上增加的功能

按照執行時機不同分為:

​ 前置,后置,異常,最終,環繞,引介

​ 引介通知指的是在不修改類代碼的前提下,為類增加方法或屬性(了解即可非重點)

示例:上述案例中的輸出執行時間功能

  • 目標(target)

目標就是要應用通知的對象,即要被增強的對象

示例:上述案例中的userDao

  • 織入(weaving)

織入是一個動詞,描述的是將擴展功能應用到target的這個過程

示例:案例中修改源代碼的過程

  • 代理(proxy)

Spring是使用代理來完成AOP,對某個對象增強后就得到一個代理對象;

Spring AOP的整個過程就是對target應用advice最后產生proxy,我們最后使用的都是proxy對象; 狸貓換太子,偷梁換柱;

  • 切面(aspect)

是切入點和通知的結合切面,是一個抽象概念; 一個切面指的是所有應用了同一個通知的切入點的集合

示例:案例中的save 和 delete方法共同組成一個切面

AOP的傳統實現

就在官方努力退出動態代理時,民間開發者也安耐不住自己躁動的新,開發了自己的一套實現AOP的方案,兩者都是利用代理對象,都屬於代理模式,但是實現原理略有不同;

動態代理(官方)

JDK1.4出現

接口:

public interface UserDao {
    public void save();
    public void delete();
}

實現類:

package com.yh.demo4;

public class UserDaoImpl implements UserDao {
    public void save(){
        System.out.println("save run");
    }
    public void delete(){
        System.out.println("delete run");
    }
}

代理類及測試代碼:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Date;

public class MyProxy implements InvocationHandler {
    private Object target;


    public MyProxy(Object target) {
        this.target = target;
    }

    //創建代理對象 本質是動態的產生一個target對象的接口實現類
    public Object createProxy(){
        Object o = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
        return o;
    }

    //方法處理 動態代理核心方法,在調用代理對象方法時都會自動調用該方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //獲取類信息和方法名
        String className = target.getClass().getName();
        String methodName = method.getName();
        //記錄開始時間
        long startTime = new Date().getTime();

        //調用原始方法
        Object result = method.invoke(target,args);

        //計算耗時
        long runtime = new Date().getTime() - startTime;
        System.out.printf("info [class:%s method:%s runtime:%s]\n",className,methodName,runtime);
        //返回原始方法執行結果
        return result;
    }

    public static void main(String[] args) {
        //目標對象
        UserDao userDao = new UserDaoImpl();
        //代理對象
        UserDao proxyDao = (UserDao) new MyProxy(userDao).createProxy();

        proxyDao.save();
        proxyDao.delete();
    }
}

當我們要對某些方法進行權限控制時也非常簡單,只需要判斷方法名稱,然后增加權限控制邏輯即可;

注意:
1.動態代理,要求被代理的target對象必須實現了某個接口,且僅能代理接口中聲明的方法,這給開發帶來了一些限制,當target不是某接口實現類時,則無法使用動態代理,CGLib則可以解決這個問題
2.被攔截的方法包括接口中聲明的方法以及代理對象和目標對象都有的方法如:toString
3.對代理對象執行這些方法將造成死循環

CGLib(民間)

CGLib是第三方庫,需要添加依賴:

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

被代理類:

public class UserDaoImpl {
    public void save(){
        System.out.println("save run");
    }

    public void delete(){
        System.out.println("delete run");
    }
}

代理類及測試代碼:

public class MyProxy implements MethodInterceptor {
    private Object target;
    public MyProxy(Object target) {
        this.target = target;
    }

    //創建代理對象 本質是動態的產生一個target對象的接口實現類
    public Object createProxy(){
        //CGLib核心類
        Enhancer enhancer = new Enhancer();
        //指定要代理的對象類型
        enhancer.setSuperclass(target.getClass());
        //設置方法回調 即代理調用代理對象的方法時會執行的方法
        enhancer.setCallback(this);
        //創建代理對象
        Object o = enhancer.create();
        return o;
    }
    /***
     * @param o 代理對象
     * @param method 客戶要執行的方法
     * @param objects 方法參數
     * @param methodProxy 方法代理對象 用於執行父類(目標)方法
     * @return  原始方法的返回值
     * @throws Throwable*/
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        //注意不要對第一個參數(代理對象) 執行任何方法,會造成死循環

        //獲取類信息和方法名
        String className = target.getClass().getName();
        String methodName = method.getName();
        //記錄開始時間
        long startTime = new Date().getTime();

        //調用原始方法1 傳入的是目標對象
        Object result = method.invoke(target,objects);
      
	      //調用原始方法2 傳入的是代理對象
        Object result = methodProxy.invokeSuper(o,objects);

        //計算耗時
        long runtime = new Date().getTime() - startTime;
        System.out.printf("info [class:%s method:%s runtime:%s]\n",className,methodName,runtime);
        //返回原始方法執行結果
        return result;
    }

    public static void main(String[] args) {
        //目標對象
        UserDaoImpl userDao = new UserDaoImpl();
        //代理對象
        UserDaoImpl proxyDao = (UserDaoImpl) new MyProxy(userDao).createProxy();

        proxyDao.save();
        proxyDao.delete();
    }
}

注意:
1.CGLib可以攔截代理目標對象的所有方法
2.CGLib采用的是產生一個繼承目標類的代理類方式產生代理對象,所以如果類被final修飾將無法使用CGLib

利用上述兩種方法我們就可以實現OAP了

Spring中的AOP

Spring在運行期,可以自動生成動態代理對象,不需要特殊的編譯器,Spring AOP的底層就是通過JDK動態代理和CGLib動態代理技術 為目標Bean執行橫向織入。並且Spring會自動選擇代理方式

1.若目標對象實現了若干接口,spring使用JDK的java.lang.reflect.Proxy類代理。

2.若目標對象沒有實現任何接口,spring使用CGLIB庫生成目標對象的子類。

Spring通知類型

  • 前置org.springframework.aop.MethodBeforeAdvice用於在原始方法執行前的預處理

  • 后置org.springframework.aop.AfterReturningAdvice用於在原始方法執行后的后處理

  • 環繞org.aopalliance.intercept.MethodInterceptor這個名字不知道誰給起的,其實不算是通知,而是叫攔截器,在這里我們可以阻止原始方法的執行,而其他通知做不到

  • 異常org.springframework.aop.ThrowsAdvice用於在原始方法拋出異常時處理

  • 引介org.springframework.aop.IntroductionInterceptor在目標類中添加一些新的方法和屬

    性(非重點)

Spring切面類型

普通的切面(Advisor)

普通切面指的是未指定具體切入點的切面,那么將把目標對象中所有方法作為切入點(全部增強)

接口:

public interface StudentDao {
    public void save();
    public void update();
    public void delete();
    public void select();
}

實現類:

public class StudentDaoImpl implements StudentDao {
    public void save() { System.out.println("save run"); }

    public void update() { System.out.println("update run"); }

    public void delete() { System.out.println("delete run"); }

    public void select() { System.out.println("select run"); }
}

配置文件:

<!--目標Bean-->
<bean id="studentDao" class="com.yh.demo7.StudentDaoImpl"/>
<!--通知-->
<bean id="before" class="com.yh.demo7.MyAdvice"/>

<!---代理Bean-->
 <bean id="studentDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
   		 <!--指定目標-->
       <property name="target" ref="studentDao"/>
       <!--指定目標實現的接口-->
       <property name="proxyInterfaces" value="com.yh.demo7.StudentDao"/>
       <!--通知(攔截)Bean的名稱 多個用逗號隔開-->
       <property name="interceptorNames" value="before"/>

       <!--其他設置:-->
       <!--告知spring目標對象是否是一個普通類 true時使用CGlib-->
       <property name="proxyTargetClass" value="true"/>
       <!--代理類是否采用單例,默認true-->
       <property name="singleton" value="false"/>
       <!--是否強制使用CGlib-->
       <property name="optimize" value="true"/>
</bean>

通知類:

public class MyAdvice implements MethodBeforeAdvice,AfterReturningAdvice, MethodInterceptor {
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("前置通知....");
    }
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
        System.out.println("后置通知....");
    }
  
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        System.out.println("環繞前....");
        Object result = methodInvocation.proceed(); //執行原始方法
        System.out.println("環繞后....");
        return result;
    }
}

測試代碼:

@RunWith(SpringJUnit4ClassRunner.class)//固定寫法
@ContextConfiguration("classpath:applicationContext4.xml") //指定加載的配置文件
public class MyTest4 {
    @Resource(name = "studentDaoProxy")
    private StudentDao studentDao;
    @Test
    public void test(){
        studentDao.delete();
        studentDao.save();
        studentDao.update();
        studentDao.select();
    }
}

切入點切面使用(PointcutAdvisor)

顧名思義,也就是指定為目標對象中僅某進行增強

  • 使用正則匹配方法的切面:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

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

        <!--目標Bean-->
        <bean id="studentDao" class="com.yh.demo7.StudentDaoImpl"/>
        <!--通知-->
        <bean id="advice" class="com.yh.demo7.MyAdvice"/>

        <!--組織切面信息-->
        <bean id="myAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
                <!--指定一個正則表達式  方法名稱匹配則將方法作為切入點-->
                <property name="pattern" value=".*save"/>
                <!--指定多個正則表達式  多個表達式用逗號隔開即可-->
                <property name="patterns" value=".*save,.*update"/>
                <!--指定要應用的通知-->
                <property name="advice" ref="advice"/>
        </bean>

        <!---代理Bean-->
        <bean id="studentDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
                <!--指定目標-->
                <property name="target" ref="studentDao"/>
                <!--指定目標實現的接口-->
                <property name="proxyInterfaces" value="com.yh.demo7.StudentDao"/>
                <!--指向Pointcut切入點-->
                <property name="interceptorNames" value="myAdvisor"/>
        </bean>
</beans>
  • 使用默認的切點切面:

整體的配置與正則相同 同樣是在代理Bean中指定目標,通知切點之間的關系;

只是需要增加一個表示切點的Bean

<!--切點-->
<bean id="pointcutBean" class="org.springframework.aop.support.NameMatchMethodPointcut">
  <!--指定要增強的方法名稱 -->
    <property name="mappedNames">
        <list>
          	<!--*是通配符 標識任意長度的任意內容 與RE無關 如.*表示方法名稱帶有點的-->
            <value>*save</value>
        </list>
    </property>
</bean>
<!--切面-->
<bean id="myAdvisor2" class="org.springframework.aop.support.DefaultPointcutAdvisor">
    <property name="advice" ref="advice"/>
    <property name="pointcut" ref="pointcutBean"/>
</bean>

 <!---代理Bean-->
<bean id="studentDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
    <!--指定目標-->
    <property name="target" ref="studentDao"/>
    <!--指定目標實現的接口-->
    <property name="proxyInterfaces" value="com.yh.demo7.StudentDao"/>
    <!--指向Pointcut切入點-->
    <property name="interceptorNames" value="myAdvisor2"/>
</bean>

自動生成代理

如果每個Bean都需要配置代理Bean的話,開發維護的工作量將是巨大的;

自動生成代理有三種方式

  • 根據BeanName來查找目標對象並且其生成代理

  • 根據切面信息來查找目標對象並且其生成代理

  • 通過AspectJ注解來指定目標對象(AspectJ中介紹)

基於BeanName生成代理

<!--根據BeanName自動生成代理對象-->

    <!--通知-->
    <bean id="befor" class="com.kkb.demo1.MyAdvice"/>
    <bean id="after" class="com.kkb.demo1.MyAdvice2"/>

    <!--目標-->
    <bean id="userDao1" class="com.kkb.demo1.UserDaoImpl"/>
    <bean id="userDao2" class="com.kkb.demo1.UserDaoImpl"/>

    <!--自動代理生成器-->
    <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <property name="interceptorNames" value="after,befor"/>
      	<!--可使用 * 通配符-->
        <property name="beanNames" value="userDao1,userDao2"/>
    </bean>

我們會發現上面的配置中沒有與切點相關的信息,的確這種方式定義的切面是普通切面,即所有目標的所有方法都會被增強

基於切點信息生成代理

<!--    根據切面信息自動生成代理對象-->

    <!--通知-->
    <bean id="befor" class="com.kkb.demo1.MyAdvice"/>
    <bean id="after" class="com.kkb.demo1.MyAdvice2"/>

    <!--目標-->
    <bean id="userDao1" class="com.kkb.demo1.UserDaoImpl"/>
    <bean id="userDao2" class="com.kkb.demo1.UserDaoImpl"/>

    <bean class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        <property name="advice" ref="after"/>
        <property name="pattern" value=".*save"/>
    </bean>
   <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
</beans>

DefaultAdvisorAutoProxyCreator將會在容器中查找所有Advisor,然后按照re表達式來查找目標對象和切點,最后為目標對象生成代理對象;

兩種方式存在一個共同點:都會將容器中的目標對象直接替換為代理對象,這樣一來,我們在使用Bean時就不用在考慮獲取的是原始對象還是代理對象了,直接使用即可

上述的配置單獨拿出一種都是比較簡單的,混在一起就很容易亂,你只需要記住,要使用AOP則必須明確的幾個關鍵點及其關系:

  • 目標

    要被增強的Bean 沒有什么特殊之處

  • 通知

    要增強的具體代碼

  • 切點

    需要明確目標對象中要增強的方法是哪些,pointcut要做的事情

  • 切面

    需要明確在某個切點上應用某些通知,即advisor要做的事情

  • 代理

    需要明確目標

若是普通切面則 只需要明確,目標,和通知即可;

問題:

看起來指定切點的切面比普通切面更強大,那么為什么還需要普通切面呢?

那你設想一下,若你的需求是給所有方法全部加上日志輸出,那這時采用普通切面是最簡便的方式;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM