Spring基礎(二)_面向切面(AOP)

面向切面編程

面向切面編程【AOP,Aspect Oriented Programming】:經過預編譯方式和運行期間動態代理實現程序功能的統一維護的技術。AOP 是 Spring 框架中的一個重要內容,利用 AOP 能夠對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度下降,提升程序的可重用性,同時提升了開發的效率。java

在 Spring 中,依賴注入管理和配置應用對象,有助於應用對象之間的解耦。而面向切面編程能夠實現橫切關注點與它們所影響的對象之間的解耦。spring

橫切關注點:散佈在應用中多處的功能,能夠被提取出來集中處理。express

面向切面編程所要解決的問題是:將橫切關注點與應用的業務邏輯相分離。編程

AOP 常見的場景:日誌、聲明式事務、安全和緩存。緩存

使用面向切面編程時,在一個地方定義通用功能,而後經過聲明的方式定義這個通用功能要以何種方式在何處應用,而無需修改受影響的類。橫切關注點能夠被模塊化爲特殊的類,該類被稱爲切面。安全

好處app

  1. 每一個關注點集中在一個地方,而不是分散在多處代碼中;
  2. 模塊更簡潔,主要的代碼只關注業務邏輯代碼;

一、專業術語

通知(Advice)框架

切面的工做被稱爲通知通知定義了切面是什麼以及什麼時候使用。Spring 含有 5 中類型的通知:ide

  • 前置通知(Before):在目標方法被==調用以前==調用通知;
  • 後置通知(After):在目標方法被==調用以後==調用通知,此時不關心方法的輸出是什麼;
  • 返回通知(After-returning):在目標方法==成功執行後==調用通知;
  • 異常通知(After-throwing):在目標方法==拋出異常後==調用通知;
  • 環繞通知(Around):在目標方法==調用以前和調用以後==均調用通知;

鏈接點(Join point)模塊化

鏈接點是應用執行過程當中可以插入切面的一個點。這個點能夠是調用方法時、拋出異常時、甚至修改一個字段時。簡單理解:一個方法即爲一個鏈接點,只是在這個鏈接點調用通知的時間點能夠自定義。

切點(Pointcut)

切點是必定數量的鏈接點;切點定義所要織入通知的一個或多個鏈接點。Spring 基於動態代理,只支持方法鏈接點。切點指明目標方法,當目標方法調用執行時,會調用對應的通知。

切面(Aspect)

切面是通知和切點的結合。通知和切點共同定義了切面的功能、在什麼時候和何地完成功能。

引入(Introduction)

向現有的類添加新的方法或屬性稱爲引入。

織入(Weaving)

織入是把切面應用到目標對象並建立新的代理對象的過程。目標對象的生命週期裏有多個點能夠織入切面:

  • 編譯期:切面在目標類編譯時被織入;須要使用特殊的編譯器,好比:AspectJ 的織入編譯器。
  • 類加載器:切面在目標類加載到 JVM 時被織入;
  • 運行期:切面在應用運行在某個時刻被織入;通常狀況下,在織入切面時,AOP 容器會爲目標對象動態地建立一個代理對象。

二、Spring的AOP實現

Spring 提供了 4 種類型的 AOP 支持:

  • 基於代理的經典 Spring AOP;
  • 純 POJO 切面;
  • @AspectJ 註解驅動的切面;
  • 注入式 AspectJ 切面(適用於 Spring 各版本);

前三種都是 Spring AOP 實現的變體,Spring AOP 構建在動態代理基礎之上,所以,Spring 對 AOP 的支持侷限於 方法攔截。

Spring 通知是用標準的 Java 類編寫的。定義通知所應用的切點一般使用註解或在 Spring XML 配置文件中編寫

Spring 在運行時通知對象。Spring 的切面由包裹了目標對象的代理類實現。代理類處理方法的調用,執行額外的切面邏輯,並調用目標方法。直到裝配須要被代理的 Bean 時,Spring 纔會建立代理對象

2.1 AspectJ 指示器

前面講到,切點做用是用於定位,在所在位置執行時調用切面的通知。在 Spring AOP 中,要使用 AspectJ 的切點表達式來定義切點;而在 Spring AOP 所支持的 AspectJ 切點指示器有:

指示器 描述
arg() 限制鏈接點匹配參數爲指定類型的執行方法;
@args() 限制鏈接點匹配參數由指定註解標註的執行方法;
execution() 用於匹配是鏈接點的執行方法;
this() 限制鏈接點匹配 AOP 代理的 Bean 引用爲指定類型的類;
target() 限制鏈接點匹配目標對象爲指定類型的類;
@target() 限制鏈接點匹配特定的執行對象,這些對象對應的類要具備指定類型的註解;
within() 限制鏈接點匹配指定的類型;
@within() 限制鏈接點匹配指定註解所標註的類型;
@annotation() 限定匹配帶有指定註解的鏈接點;
@bean() 限定鏈接點匹配指定 ID 的 Bean;

上述指示器中,除了 execution 以外,均用於匹配鏈接點;而 execution 指示器是執行通知:當括號內的鏈接點對應的方法被調用時,execution 會執行對應的通知操做

2.2 使用註解建立切面

2.2.1 建立切面類

AspectJ 5 引入了重要的特性:使用註解建立切面。要在類中使用註解建立切面,首先,必需要導入對應的 jar包( aspectjweaver.jar ),下面,咱們來建立一個切面類:

import org.aspectj.lang.annotation.Aspect;

@Aspect
public class AspectClass {

}

使用註解 AspectJ 代表,這個類不只僅是一個簡單的 Java 類,仍是一個切面。可是前面說過,一個完整的切面應該包含切點和通知

2.2.2 定義通知方法

在切面類中,5 種通知類型分別對應 5 種註解,使用註解標註方法定義通知方法,這些註解分別是:

註解 通知
@After 目標方法返回或拋出異常後調用
@AfterReturning 目標方法返回後調用
@AfterThrowing 目標方法拋出異常後調用
@Around 將目標方法封裝起來
@Before 目標方法調用以前執行

使用註解定義通知

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class AspectClass {
    
    @Before()
    public void before(){
        System.out.println("前---置通知");
    }

    @After()
    public void before(){
        System.out.println("後---置通知");
    }
    
    @Around()
    public Object around(ProceedingJoinPoint jp){
        System.out.println("環繞通知---前");
        Object proceed=null;
        try {
            Object[] args = jp.getArgs();   //獲取傳入目標方法的參數
            proceed = jp.proceed(); //調用執行目標方法
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }

        System.out.println("環繞通知---後");

        return proceed;
    }
}

其中,環繞通知的方法定義不一樣。環繞通知方法中須要傳入一個參數 ProceedingJoinPoint 接口,該接口控制目標方法:

  • getArgs() 獲取目標方法的形參;
  • 經過 proceed() 執行目標方法,會返回目標方法的返回值;若是不調用此方法,會阻塞目標方法的調用;也能夠屢次調用
2.2.3 編寫切點表達式

在註解內放入切點表達式。所謂切點表達式,是使用指示器匹配鏈接點。切點表達式中須要使用 execution 指示器(以下圖)。

execution指示器

不一樣的指示器之間可使用邏輯運算(and、or、not)拼接一塊兒使用

下面,咱們爲切面添加切點,當執行器內指定的目標方法執行時會調用對應的通知方法。

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class AspectClass {

    @Before("execution(* 包名.類名.目標方法())")
    public void before(){
        System.out.println("前---置通知");
    }

    @After("execution(* 包名.類名.目標方法())")
    public void after(){
        System.out.println("後---置通知");
    }
    
    @Around("execution(* 包名.類名.目標方法())")
    public Object around(ProceedingJoinPoint jp){
        System.out.println("環繞通知---前");
        Object proceed=null;
        try {
            proceed = jp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }

        System.out.println("環繞通知---後");

        return proceed;
    }
}

還有一種方式能夠簡化編寫切點。在上面這個例子中,每一個通知中都使用切點表達式來匹配鏈接點,這樣作很繁瑣。使用 @Poingcut 註解標註,爲一個方法編寫切點,而後在通知註解中引用切點方法便可

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;

@Aspect
public class AspectClass {

    //切點方法
    @Pointcut("execution(* 包名.類名.目標方法())")
    public void work() {

    }

    @Before("work()")//引用切點方法
    public void before() {
        System.out.println("前---置通知");
    }

    @After("work()")//引用切點方法
    public void after() {
        System.out.println("後---置通知");
    }

    @Around("work()")//引用切點方法
    public Object around(ProceedingJoinPoint jp) throws Throwable {


        System.out.println("環繞通知---前");
        Object proceed = null;

        Object[] args = jp.getArgs();
        jp.proceed();

        System.out.println("環繞通知---後");

        return proceed;
    }
}
2.2.4 啓動自動代理

到此爲止,一個切面就建立好了。可是,若是沒有啓用自動代理功能,這個切面只能被當作一個 Bean,AspectJ 註解也不會被解析。在 JavaConfig 配置類和 XML 配置文件啓動自動代理功能

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
//(1)啓動 AspectJ 自動代理
@EnableAspectJAutoProxy
public class WorkAspectConfig {
    
    //(2)聲明切面類的bean
    @Bean
    public AspectClass getAspectClass(){
        return new AspectClass();
    }
}
<?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/aop/spring-context.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <aop:aspectj-autoproxy/><!--(1)啓用 AspectJ 自動代理-->

    <bean class="AspectClass"/><!--(2)聲明切面類的bean-->

</beans>
2.2.4 處理通知中的參數

在此以前,咱們在講到前置通知和後置通知的方法定義中都是沒有參數,除了環繞通知,能夠經過 ProceedJoinPoint 接口獲取目標方法的參數和傳遞返回值。但,若是前置通知和後置通知的方法中定義了參數,那麼,切面如何訪問和使用目標方法中的參數呢?

@Before("execution(* 包名.類名.目標方法名(Type...)) && args(name,...)")
public void work(Type name,...){
    
}

不一樣之處在於:

  • Execution 指示器內的方法須要指定參數類型,這個參數類型與傳入通知方法的參數類型匹配
  • 使用邏輯運算符 && (或 ||、!拼接一個 args 指示器,這個指示器內指定參數名,代表傳遞給目標方法的 Type 類型參數也會傳遞給通知方法。
  • args 指示器內的參數名稱與通知方法的名稱要一致。
2.2.5 添加新功能

上面已經講解講解了:如何爲現有方法添加額外的功能。那如今咱們須要爲一個對象添加新的方法,如何實現呢?具體步驟以下:

  1. 建立一個新的接口,接口內定義了新功能的方法;並建立新接口的實現類;
  2. 建立一個新的切面類,該類內定義一個步驟1聲明接口的靜態屬性,該屬性使用註解 @DeclareParents 標註;
  3. 在配置文件中裝配切面類的 Bean,以及新接口的實現類的 Bean;

這樣就完成了爲一個現有對象添加了新方法。注意:要使用新方法,對象須要強制轉換爲新接口類型。在這裏須要重點了解的是:@DeclareParents 的使用

//@DeclareParents 由三部分組成
@DeclareParents(value="package.OldInterface+"
                defaultImpl= newClassImplNewInterface.class)
public static NewInterface newInterface;
  • value :指定須要添加新功能的類,+表示 OldInterface 類的全部子類;
  • defaultImpl :指定添加了新功能的實現類;
  • @DeclareParents 註解標註的靜態屬性:指明瞭要引入新功能的接口;

2.3 XML 配置建立切面

在 Spring 的 XML 配置文件中, aop 命名空間提供了元素用來聲明切面,如表:

AOP配置元素 用途
<aop:aspect-autoproxy> 啓用 @AspectJ 註解驅動的切面
<aop:config> 頂層AOP配置元素。大多數aop元素必須在該元素內
<aop:aspect> 定義一個切面
<aop:advisor> 定義AOP通知器
<aop:after> 定義AOP後置通知
<aop:after-returning> 定義AOP返回通知(無論目標方法是否執行成功)
<aop:after-throwing> 定義AOP異常通知
<aop:around> 定義AOP環繞通知
<aop:before> 定義AOP前置通知
<aop:declare-parents> 以透明的方式爲目標對象引入額外的接口
<aop:pointcut> 定義一個切點

已經瞭解了 XML 配置的基本使用元素。因爲前面對切面的瞭解已經比較深刻,如今瞭解如何使用 XML 配置 AOP,暫不深刻過多。直接上例子:

一、原有代碼,須要在現有代碼中添加新功能。簡稱:目標對象、目標方法。

package xml;

public class Work {

    public String working(){

        System.out.println("工做ing");

        return "工做ing";
    }

    public void working(int time){

        System.out.println("工做時長:"+time);

    }
}

二、要向目標方法添加的新功能類,並以此類添加切面

package xml;

import org.aspectj.lang.ProceedingJoinPoint;

public class AspectClass {

    public void before() {
        System.out.println("前---置通知");
    }

    public void after() {
        System.out.println("後---置通知");
    }

    public Object around(ProceedingJoinPoint jp) throws Throwable {

        System.out.println("環繞通知---前");
        Object proceed = null;

        //(1)獲取目標方法的參數
        Object[] args = jp.getArgs();
        //(2)調用目標方法,並獲取返回值
        proceed = jp.proceed(args);

        System.out.println("環繞通知---後");

        return proceed;
    }

    public void afterWork(int time) {

        if (time > 0 && time < 8) {
            System.out.println("工做時長不夠8小時");
        }else{
            System.out.println("工做時長:"+time);
        }
    }

}

三、要向目標對象添加新的方法

package xml;

public interface OtherWork {
    void addWorkTime();
}
package xml;

public class NightWork implements OtherWork {
    @Override
    public void addWorkTime() {
        System.out.println("加夜班");
    }
}

四、編寫 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">

    
    <!--原有對象,須要添加新功能的目標方法-->
    <bean class="xml.Work" id="w"/>  

    <!--建立Bean ,該Bean要做爲切面類-->
    <bean class="xml.AspectClass" id="aspectClass"/>
    
    <!--對引入新方法的接口的實現類建立bean-->
    <bean class="xml.NightWork" id="nightWork"/>

    <!--AOP配置-->
    <aop:config>
        <!--定義一個切面-->
        <aop:aspect ref="aspectClass">

            <!--<aop:before method="before" 
                     pointcut="execution(* xml.Work.working())"/>-->

            <!--定義切點-->
            <aop:pointcut id="work_pointcut" 
                          expression="execution(* xml.Work.working())"/>

            <!--定義通知-->
            <aop:before method="before" pointcut-ref="work_pointcut"/>
            <aop:after method="after" pointcut-ref="work_pointcut"/>
            <aop:around method="around" pointcut-ref="work_pointcut"/>

            <!--定義帶有參數的通知-->
            <aop:after method="afterWork" 
                       pointcut="execution(* xml.Work.working(int)) and args(time)"/>
            
            <!--向原有對象中添加新的方法-->
            <aop:declare-parents types-matching="xml.Work"
                                 implement-interface="xml.OtherWork"
                                 default-impl="xml.NightWork"/>
        </aop:aspect>
    </aop:config>
</beans>

六、測試類

package test;

import xml.OtherWork;
import xml.Work;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:xml/AspectConfig.xml")
public class WorkTest {

    @Test
    public void test(){
        //解析XML配置文件
        ApplicationContext app =new ClassPathXmlApplicationContext("xml/AspectConfig.xml");
        
        //測試目標方法的新功能
        Work work = (Work) app.getBean("w");
        work.working(6);

        //測試目標對象的新方法
        OtherWork nightWork = (OtherWork) work;
        nightWork.addWorkTime();
    }
}

//測試結果:
//      工做時長:6
//      工做時長不夠8小時
//      加夜班

到此爲止,Spring AOP的基礎學習完畢。

相關文章
相關標籤/搜索