1. 程式人生 > >Spring AOP面向切面程式設計詳解(基於XML方式 註解方式 注入Aspectj方式)

Spring AOP面向切面程式設計詳解(基於XML方式 註解方式 注入Aspectj方式)

前言

AOP即面向切面程式設計,是一種程式設計思想,OOP的延續。在程式開發中主要用來解決一些系統層面上的問題,比如日誌,事務,許可權等等。在閱讀本文前希望您已經對Spring有一定的瞭解

注:在能對程式碼進行添加註解方式實現AOP的話,並不推薦使用XML方式。換言之在XML方式配置更適用於不能對程式碼添加註解的情況下(註解配置方式推薦值>XML配置方式推薦值)

AOP相關術語

1.通知(Advice):在切面的某個特定的連線點上執行的動作,即當程式到達一個執行點後會執行相對應的一段程式碼,也稱為增強處理。通知共有如下5種類型[前置通知 後置通知 返回通知 環繞通知 丟擲異常後通知]
2.連線點(JoinPoint):程式執行的某個特定位置,例如類初始化前,類初始化後,方法執行前,方法執行後,方法丟擲異常時等,Spring只支援方法級別的連線點,即方法執行前,方法執行後,方法丟擲異常時 3.切入點(Pointcut):切入點是一個篩選連線點的過程,因為在你的工程中可能有很多連線點,你只是想讓其中幾個,在呼叫這幾個方法之前、之後或者丟擲異常時乾點什麼,那麼就用切入點來定義這幾個方法,讓切點來篩選連線點,選中那幾個你想要的方法 4.切面(Aspect):切面通常是指一個類,是通知和切入點的結合。到這裡會發現連線點就是為了讓你好理解切點產生的。通俗來說切面的配置可以理解為:什麼時候在什麼地方做什麼事。切入點說明了在哪裡幹(指定到方法),通知說明了什麼時候幹什麼 5.引入(Introduction
):引入允許我們向現有的類新增新方法或屬性 6.織入(Weaving):把切面應用到目標物件來建立新的代理物件的過程,織入一般發生在如下幾個時機: (1)編譯時:當一個類檔案被編譯時進行織入,這需要特殊的編譯器才可以做的到,例如AspectJ的織入編譯器 (2)類載入時:使用特殊的ClassLoader在目標類被載入到程式之前增強類的位元組程式碼 (3)執行時:切面在執行的某個時刻被織入,SpringAOP就是以這種方式織入切面的,原理應該是使用了JDK的動態代理技術

基於XML方式配置AOP

正常通知

1.編寫業務類

public class HelloWorldBusiness {
    public
String sayHelloWorld(String language) { String result = "Hello World " + language; System.out.println("真正的業務方法執行啦~~~"); return result; } }

2.編寫切面類

public class HelloWorldBusinessAspect {
    public void beforeSayHelloWorld(String language) {
        System.out.println("執行方法前執行,引數為:" + language);
    }

    public void afterSayHelloWorld(String language) {
        System.out.println("執行方法後執行,引數為:" + language);
    }

    public void afterReturningSayHelloWorld(String language, String result) {
        System.out.println("執行方法返回後執行,引數為:" + language + " 方法返回值為:" + result);
    }

    public void afterThrowingHelloWorld(String language, Throwable e) {
        System.out.println("執行方法丟擲異常後執行,引數為:" + language + "異常為:" + e);
    }
}

3.編寫配置檔案

<?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-4.3.xsd">

    <!-- 配置業務Bean -->
    <bean id="helloWorldBusiness" class="roberto.growth.process.aop.HelloWorldBusiness" />

    <!-- 配置切面Bean -->
    <bean id="helloWorldBusinessAspect" class="roberto.growth.process.aop.HelloWorldBusinessAspect" />

    <!-- 配置一個切面 -->
    <aop:config>
        <aop:aspect id="helloWorldAspect" ref="helloWorldBusinessAspect">
            <!-- 配置一個切點 -->
            <aop:pointcut id="sayHelloWorldPoint" expression="execution(public * roberto.growth.process.aop.HelloWorldBusiness.sayHelloWorld(..)) and args(language)" />
            <!-- 配置前置通知 -->
            <aop:before pointcut-ref="sayHelloWorldPoint" method="beforeSayHelloWorld" arg-names="language"/>
            <!-- 配置前置通知 -->
            <aop:after pointcut-ref="sayHelloWorldPoint" method="afterSayHelloWorld" arg-names="language"/>
            <!-- 配置後置返回通知 -->
            <aop:after-returning pointcut-ref="sayHelloWorldPoint" method="afterReturningSayHelloWorld" arg-names="language,result" returning="result" />
            <!-- 異常通知 -->
            <aop:after-throwing pointcut-ref="sayHelloWorldPoint" method="afterThrowingHelloWorld" arg-names="language,e" throwing="e" />
        </aop:aspect>
    </aop:config>
</beans>

4.執行HelloWorldBusiness的sayHelloWorld方法輸出結果為

執行方法前執行,引數為:JAVA
真正的業務方法執行啦~~~
執行方法後執行,引數為:JAVA
執行方法返回後執行,引數為:JAVA 方法返回值為:Hello World JAVA

環繞通知

1.編寫業務類

public class HelloWorldBusiness {
    public String sayHelloWorld(String language) {
        String result = "Hello World " + language;
        System.out.println("真正的業務方法執行啦~~~");
        return result;
    }
}

2.編寫切面類

public class HelloWorldBusinessAspect {
    public void aroundSayHelloWorld(ProceedingJoinPoint joinPoint) {
        String language = (String) joinPoint.getArgs()[0];
        try {
            System.out.println("執行方法前執行,引數為:" + language);
            String result = (String) joinPoint.proceed();
            System.out.println("執行方法後執行,引數為:" + language + " 方法返回值為:" + result);
        } catch (Throwable e) {
            System.out.println("執行方法丟擲異常後執行,引數為:" + language + "異常為:" + e);
        }
    }
}

3.編寫配置檔案

<?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-4.3.xsd">

    <!-- 配置業務Bean -->
    <bean id="helloWorldBusiness" class="roberto.growth.process.aop.HelloWorldBusiness" />

    <!-- 配置切面Bean -->
    <bean id="helloWorldBusinessAspect" class="roberto.growth.process.aop.HelloWorldBusinessAspect" />

    <!-- 配置一個切面 -->
    <aop:config>
        <aop:aspect id="helloWorldAspect" ref="helloWorldBusinessAspect">
            <!-- 配置一個切點 -->
            <aop:pointcut id="sayHelloWorldPoint" expression="execution(public * roberto.growth.process.aop.HelloWorldBusiness.sayHelloWorld(..))" />
            <!-- 配置環繞通知 -->
            <aop:around pointcut-ref="sayHelloWorldPoint" method="aroundSayHelloWorld" />
        </aop:aspect>
    </aop:config>
</beans>

4.執行HelloWorldBusiness的sayHelloWorld方法輸出結果為

執行方法前執行,引數為:JAVA
真正的業務方法執行啦~~~
執行方法後執行,引數為:JAVA 方法返回值為:Hello World JAVA

使用MethodInterceptor實現AOP

1.編寫業務類

public class HelloWorldBusiness {
    public String sayHelloWorld(String language) {
        String result = "Hello World " + language;
        System.out.println("真正的業務方法執行啦~~~");
        return result;
    }
}

2.編寫攔截器類 實現MethodInterceptor方法

public class HelloWorldBusinessAspect implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        // 獲取被增強物件引數列表
        String language = (String) invocation.getArguments()[0];
        // 獲取被增強物件的方法
        Method method = invocation.getMethod();
        // 繼續執行業務方法
        System.out.println("執行" + method.getName() + "方法前執行,引數為: " + language);
        Object result = invocation.proceed();
        System.out.println("執行方法返回後執行,引數為:" + language + " 方法返回值為:" + result);
        return result;
    }
}

3.編寫配置檔案

<?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-4.3.xsd">

    <!-- 配置業務Bean -->
    <bean id="helloWorldBusiness" class="roberto.growth.process.aop.HelloWorldBusiness" />

    <!-- 配置切面Bean -->
    <bean id="helloWorldBusinessAspect" class="roberto.growth.process.aop.HelloWorldBusinessAspect" />

    <aop:config>
        <!-- 配置一個切點 -->
        <aop:pointcut id="sayHelloWorldPoint" expression="execution(public * roberto.growth.process.aop.HelloWorldBusiness.sayHelloWorld(..))" />

        <!-- 配置通知類 -->
        <aop:advisor advice-ref="helloWorldBusinessAspect" pointcut-ref="sayHelloWorldPoint" />
    </aop:config>
</beans>

4.執行HelloWorldBusiness的sayHelloWorld方法輸出結果為

執行sayHelloWorld方法前執行,引數為: JAVA
真正的業務方法執行啦~~~
執行方法返回後執行,引數為:JAVA 方法返回值為:Hello World JAVA

注意事項

1.在Spring的配置檔案中,所有的切面和通知都必須定義在<aop:config>元素內部。(一個application context可以包含多個<aop:config>)。一個<aop:config>可以包含pointcut,advisor和aspect元素(注意這三個元素必須按照這個順序進行宣告)

2.當我們使用<aop:config/>方式進行配置時,可能與Spring的自動代理方式相互衝突(<aop:aspectj-autoproxy/>),因此,建議要麼全部使用<aop:config/>配置方式,要麼全部使用自動代理方式,不要把兩者混合使用

基於註解方式配置AOP

正常通知

1.編寫業務類

@Component
public class HelloWorldBusiness {
    public String sayHelloWorld(String language) {
        String result = "Hello World " + language;
        System.out.println("真正的業務方法執行啦~~~");
        return result;
    }
}

2.編寫切面類

@Aspect
@Component
public class HelloWorldBusinessAspect {
    @Pointcut("execution(public * roberto.growth.process.aop.HelloWorldBusiness.sayHelloWorld(..)) && args(language)")
    public void sysHelloWorldPointCut(String language) {

    }

    @Before("sysHelloWorldPointCut(language)")
    public void beforeSayHelloWorld(String language) {
        System.out.println("執行方法前執行,引數為:" + language);
    }

    @After("sysHelloWorldPointCut(language)")
    public void afterSayHelloWorld(String language) {
        System.out.println("執行方法後執行,引數為:" + language);
    }

    @AfterReturning(pointcut = "sysHelloWorldPointCut(language)", returning = "result")
    public void afterReturningSayHelloWorld(String language, String result) {
        System.out.println("執行方法返回後執行,引數為:" + language + " 方法返回值為:" + result);
    }

    @AfterThrowing(pointcut = "sysHelloWorldPointCut(language)", throwing = "e")
    public void afterThrowingHelloWorld(String language, Throwable e) {
        System.out.println("執行方法丟擲異常後執行,引數為:" + language + "異常為:" + e);
    }
}

3.編寫配置類(使用EnableAspectJAutoProxy註解啟用自動代理功能)

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "roberto.growth.process")
public class ApplicationConfig {

}

4.執行HelloWorldBusiness的sayHelloWorld方法輸出結果為

執行方法前執行,引數為:JAVA
真正的業務方法執行啦~~~
執行方法後執行,引數為:JAVA
執行方法返回後執行,引數為:JAVA 方法返回值為:Hello World JAVA

環繞通知

1.編寫業務類

@Component
public class HelloWorldBusiness {
    public String sayHelloWorld(String language) {
        String result = "Hello World " + language;
        System.out.println("真正的業務方法執行啦~~~");
        return result;
    }
}

2.編寫切面類

@Aspect
@Component
public class HelloWorldBusinessAspect {
    @Pointcut("execution(public * roberto.growth.process.aop.HelloWorldBusiness.sayHelloWorld(..))")
    public void sysHelloWorldPointCut() {

    }

    @Around("sysHelloWorldPointCut()")
    public void aroundSayHelloWorld(ProceedingJoinPoint joinPoint) {
        String language = (String) joinPoint.getArgs()[0];
        try {
            System.out.println("執行方法前執行,引數為:" + language);
            String result = (String) joinPoint.proceed();
            System.out.println("執行方法後執行,引數為:" + language + " 方法返回值為:" + result);
        } catch (Throwable e) {
            System.out.println("執行方法丟擲異常後執行,引數為:" + language + "異常為:" + e);
        }
    }
}

3.編寫配置類(使用EnableAspectJAutoProxy註解啟用自動代理功能)

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "roberto.growth.process")
public class ApplicationConfig {

}

4.執行HelloWorldBusiness的sayHelloWorld方法輸出結果為

執行方法前執行,引數為:JAVA
真正的業務方法執行啦~~~
執行方法後執行,引數為:JAVA 方法返回值為:Hello World JAVA

純AspectJ方式配置AOP

雖然Spring AOP能夠滿足許多應用的切面要求,但是與AspectJ相比,Spring AOP是一個功能比較弱的AOP解決方案,AspectJ提供了Spring AOP所不能支援的許多型別的切點,例如構造器切點等

1.本地安裝好AspectJ環境(具體安裝自行百度)

2.在IDEA中配置AspectJ環境(具體可百度)
AspectJ IDEA配置
AspectJ IDEA配置

3.編寫Aspect Demo使用AspectJ實現AOP功能

3.1.建立學生類

public class Student {
    public Student() {
        System.out.println("構造方法執行了");
    }

    public void doHomeWork(){
        System.out.println("學生開始做功課啦~~~");
    }

    public static void main(String[] args) {
        Student student = new Student();
        student.doHomeWork();
    }
}

3.2.建立AspectJ切面

public aspect StudentAspect {
    // 建立構造器切點
    pointcut constructPointCut():call(roberto.growth.process.aop.Student.new());

    before():constructPointCut(){
        System.out.println("建立學生物件前呼叫");
    }

    after():constructPointCut(){
        System.out.println("建立學生物件後呼叫");
    }

    // 建立學生做功課方法切點
    pointcut doHomeWorkPointCut():execution(public * roberto.growth.process.aop.Student.doHomeWork(..));

    before():doHomeWorkPointCut(){
        System.out.println("學生做功課前");
    }

    after():doHomeWorkPointCut(){
        System.out.println("學生做功課後");
    }
}

3.3.執行學生類的main方法,檢視輸出結果

建立學生物件前呼叫
構造方法執行了
建立學生物件後呼叫
學生做功課前
學生開始做功課啦~~~
學生做功課後

注意:由於AspectJ是在編譯時期進行織入,所以在執行main方法前最好先手動編譯一下

切入點相關

切入點表示式

本篇文章不介紹切入點表示式語法,與需要的讀者可以參考切入點表示式可參考: 切入點表示式

切入點執行順序

在配置切面和通知的時候,可以指定order引數來區分切入執行先後順序,order的值越小說明越先被執行

XML方式:<aop:aspect id="helloWorldAspect" ref="helloWorldBusinessAspect" order="0">

Aspect類添加註解:org.springframework.core.annotation.Order 
Aspect類實現介面:org.springframework.core.Ordered實現Ordered介面的getOrder()方法即可

基於註解方式引入新功能

Demo:為程式設計師新增歌手的屬性,讓程式設計師成為斜槓青年

1.編寫程式設計師類

public interface Programmer {
    public void coding();
}

@Component
public class ProgrammerImpl implements Programmer{
    @Override
    public void coding() {
        System.out.println("寫最好的程式碼");
    }
}

2.編寫歌手類

public interface Singer {
    void sing();
}

public class DefaultSinger implements Singer{
    @Override
    public void sing() {
        System.out.println("唱最動聽的歌");
    }
}

3.編寫歌手引入切面 為程式設計師新增歌手特性

@Aspect
@Component
public class SingerAspect {
    @DeclareParents(value = "roberto.growth.process.aop.Programmer+", defaultImpl = DefaultSinger.class)
    public static Singer singer;
}

4.編寫配置類(使用EnableAspectJAutoProxy註解啟用自動代理功能)

@Configuration
@EnableAspectJAutoProxy
@ComponentScan(basePackages = "roberto.growth.process")
public class ApplicationConfig {

}

5.測試輸出結果如下:(程式設計師即可以寫程式碼也可以唱歌)

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = ApplicationConfig.class)
public class StudentTest {
    @Autowired
    private Programmer programmer;

    @Test
    public void testSayHelloWorld() {
        System.out.println("這個程式設計師是個歌手嗎:" + (programmer instanceof Singer));
        programmer.coding();
        ((Singer) programmer).sing();
    }
}

控制檯輸出:
這個程式設計師是個歌手嗎:true
寫最好的程式碼
唱最動聽的歌

基於XML方式引入新功能

Demo:為程式設計師新增歌手的屬性,讓程式設計師成為斜槓青年

1.編寫程式設計師類

public interface Programmer {
    public void coding();
}

public class ProgrammerImpl implements Programmer{
    @Override
    public void coding() {
        System.out.println("寫最好的程式碼");
    }
}

2.編寫歌手類

public interface Singer {
    void sing();
}

public class DefaultSinger implements Singer{
    @Override
    public void sing() {
        System.out.println("唱最動聽的歌");
    }
}

3.編寫配置檔案引入切面 為程式設計師新增歌手特性

<?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-4.3.xsd">


    <!-- 配置程式設計師類 -->
    <bean id="programmer" class="roberto.growth.process.aop.ProgrammerImpl" />

    <aop:config>
        <aop:aspect>
            <aop:declare-parents types-matching="(roberto.growth.process.aop.Programmer+)" implement-interface="roberto.growth.process.aop.Singer" default-impl="roberto.growth.process.aop.DefaultSinger" />
        </aop:aspect>
    </aop:config>
</beans>

4.測試輸出結果如下:(程式設計師即可以寫程式碼也可以唱歌)

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class StudentTest {
    @Autowired
    private Programmer programmer;

    @Test
    public void testSayHelloWorld() {
        System.out.println("這個程式設計師是個歌手嗎:" + (programmer instanceof Singer));
        programmer.coding();
        ((Singer) programmer).sing();
    }
}

控制檯輸出:
這個程式設計師是個歌手嗎:true
寫最好的程式碼
唱最動聽的歌