Spring學(xué)習(xí)筆記(三): Spring 面向切面

面向切面編程是 Spring 最為重要的功能之一,在數(shù)據(jù)庫事務(wù)管理中廣泛應(yīng)用。

全部章節(jié)傳送門:
Spring學(xué)習(xí)筆記(一):Spring IoC 容器
Spring學(xué)習(xí)筆記(二):Spring Bean 裝配
Spring學(xué)習(xí)筆記(三): Spring 面向切面
Spring學(xué)習(xí)筆記(四): Spring 數(shù)據(jù)庫編程
Spring學(xué)習(xí)筆記(五): Spring 事務(wù)管理

AOP(Aspect Oriented Programming),即面向切面編程,可以說是OOP(Object Oriented Programming,面向?qū)ο缶幊蹋┑难a(bǔ)充和完善。

使用"橫切"技術(shù),AOP把軟件系統(tǒng)分為兩個部分:核心關(guān)注點和橫切關(guān)注點。業(yè)務(wù)處理的主要流程是核心關(guān)注點,與之關(guān)系不大的部分是橫切關(guān)注點。橫切關(guān)注點的一個特點是,他們經(jīng)常發(fā)生在核心關(guān)注點的多處,而各處基本相似,比如權(quán)限認(rèn)證、日志、事物。AOP的作用在于分離系統(tǒng)中的各種關(guān)注點,將核心關(guān)注點和橫切關(guān)注點分離開來。

AOP 常用術(shù)語

spring-aop.jpg
  • 切面(Aspect): 通常是一個類,里面可以定義切入點和通知。
  • 通知(Advice): 通知是切面開啟后,切面的方法,它根據(jù)在代理對象真實方法調(diào)用前、后的順序和邏輯區(qū)分,一共五類:
    • 前置通知(before): 在動態(tài)代理反射原有對象或者執(zhí)行環(huán)繞通知前執(zhí)行的通知功能。
    • 后置通知(after): 在動態(tài)代理反射原有對象或者執(zhí)行環(huán)繞通知后執(zhí)行的通知功能,無論是否拋出異常,它都會被執(zhí)行。
    • 返回通知(afterReturning): 在動態(tài)代理反射原有對象或者執(zhí)行環(huán)繞通知后正常返回(無異常)執(zhí)行的通知功能。
    • 異常通知(afterThrowing): 在動態(tài)代理反射原有對象或者執(zhí)行環(huán)繞通知產(chǎn)生異常后執(zhí)行的通知功能。
    • 環(huán)繞通知(around): 在動態(tài)代理中,它可以取代當(dāng)前被攔截對象的方法,提供回調(diào)原有被攔截對象的方法。
  • 引入(Introduction): 在不修改代碼的前提下,引入可以在運(yùn)行期為類動態(tài)地添加一些方法或字段。
  • 連接點(join point): 被攔截到的點。
  • 切點(Pointcut): 對連接點進(jìn)行攔截的定義。
  • 織入(Weaving): 將切面應(yīng)用到目標(biāo)對象并導(dǎo)致代理對象創(chuàng)建的過程。

Spring 對 AOP 的支持

Spring AOP 是一種基于方法攔截的 AOP,它只支持方法攔截。在 Spring 中有4種方法去實現(xiàn) AOP 的攔截功能。

  • 使用 ProxyFactoryBean 和對應(yīng)的接口實現(xiàn) AOP。
  • 使用 XML 配置 AOP。
  • 使用 @AspectJ 注解驅(qū)動切面。
  • 使用 AspectJ 注入切面。
    其中,真正常用的是 @AspectJ 注解,有時候 XML 配置也有一定的輔助作用,另外2種很少使用。

使用 @AspectJ 注解開發(fā) Spring AOP

使用 @AspectJ 注解的方式是當(dāng)下的主流方式。

創(chuàng)建一個 maven 項目, 引入 Spring 相關(guān)依賴。

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-support</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aop</artifactId>
        <version>${spring.version}</version>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>${spring.version}</version>
    </dependency>
</dependencies>

選擇連接點

創(chuàng)建一個接口,使用其中的方法作為連接點。

package com.wyk.aopdemo.service;

import com.wyk.aopdemo.domain.Role;

public interface RoleService {
    public void printRole(Role role);
}

然后編寫它的實現(xiàn)類。

package com.wyk.aopdemo.service.impl;

import com.wyk.aopdemo.domain.Role;
import com.wyk.aopdemo.service.RoleService;
import org.springframework.stereotype.Component;

@Component
public class RoleServiceImpl implements RoleService {
    public void printRole(Role role) {
        System.out.println("{id: " + role.getId() + ", role_name: "
            + role.getRoleName() + ", note: " + role.getNote() + "}");
    }
}

其中的 Role 是一個實體類。

package com.wyk.aopdemo.domain;

public class Role {
    private Long id;
    private String roleName;
    private String note;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getRoleName() {
        return roleName;
    }

    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }

    public String getNote() {
        return note;
    }

    public void setNote(String note) {
        this.note = note;
    }
}

創(chuàng)建切面和切點

在類上添加 @Aspect 注解即可將其變成一個切面。

package com.wyk.aopdemo.aspect;

import org.aspectj.lang.annotation.*;

@Aspect
public class RoleAspect {
    @Pointcut("execution(* com.wyk.aopdemo.service.impl.RoleServiceImpl.printRole(..))")
    public void print() {

    }
    @Before("print()")
    public void before() {
        System.out.println("before ...");
    }

    @After("print()")
    public void after() {
        System.out.println("after ...");
    }

    @AfterReturning("print()")
    public void afterReturning() {
        System.out.println("afterReturn ...");
    }

    @AfterThrowing("print()")
    public void afterThrowing() {
        System.out.println("afterThrowing ...");
    }
}

其中的 @Pointcut 注解用來定義切點,其他注解代表通知,很好理解。如過不定義切點則需要在通知上直接寫正則表達(dá)式,比較麻煩。

對切點中的正則表達(dá)式簡單分析一下。

execution(* com.wyk.aopdemo.service.impl.RoleServiceImpl.printRole(..))
  • execution: 代表執(zhí)行方法的時候會觸發(fā)。
  • *:代表任意返回類型的方法。
  • com.wyk.aopdemo.service.impl.RoleServiceImpl.printRole: 代表類的全限定名。
  • (..): 任意的參數(shù)。

生成 AOP 實例

最后需要添加方法配置 Spring 的 Bean 。

package com.wyk.aopdemo.config;

import com.wyk.aopdemo.aspect.RoleAspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.wyk.aopdemo")
public class AppConfig {
    @Bean
    public RoleAspect getRoleAspect() {
        return new RoleAspect();
    }
}

其中 @EnableAspectJAutoProxy 注解用來啟用 AspectJ 框架的自動代理,這個時候 Spring 會生成動態(tài)代理對象,進(jìn)而可以使用 AOP , 而其中的方法用來生成一個切面實例。

進(jìn)行測試

編寫一個測試類進(jìn)行測試。

package com.wyk.aopdemo;

import com.wyk.aopdemo.config.AppConfig;
import com.wyk.aopdemo.domain.Role;
import com.wyk.aopdemo.service.RoleService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class Main {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        RoleService roleService = (RoleService)ctx.getBean(RoleService.class);
        Role role = new Role();
        role.setId(1L);
        role.setRoleName("wyk");
        role.setNote("haha");
        roleService.printRole(role);
        System.out.println("############################");
        //測試異常通知
        role = null;
        roleService.printRole(role);
    }
}

運(yùn)行程序,在控制臺查看結(jié)果。很顯然切面的通知已經(jīng)通過 AOP 織入約定的流程當(dāng)中。

before ...
{id: 1, role_name: wyk, note: haha}
after ...
afterReturn ...
############################
before ...
after ...
afterThrowing ...

環(huán)繞通知

環(huán)繞通知是 Spring AOP 當(dāng)中最強(qiáng)大的 AOP ,它可以同時實現(xiàn)前置通知和后置通知。它保留了調(diào)度被代理對象原有方法的功能,所以它既強(qiáng)大又靈活,但是可控性不強(qiáng),如果不需要大量改變業(yè)務(wù)邏輯,一般而言并不需要使用它。

在 RoleAspect 類中添加環(huán)繞通知方法。

@Around("print()")
public void around(ProceedingJoinPoint jp) {
    System.out.println("around before ...");
    try {
        jp.proceed();
    } catch (Throwable e) {
        e.printStackTrace();
    }
    System.out.println("around after ...");
}

在一個切面里通過 @Around 注解加入環(huán)繞通知,其中方法中有一個 ProceedingJoinPoint 參數(shù),它由 Spring 提供,可以反射連接點方法。

運(yùn)行程序,查看結(jié)果。

around before ...
before ...
{id: 1, role_name: wyk, note: haha}
around after ...
after ...
afterReturn ...
############################
around before ...
before ...
...(異常堆棧信息)
around after ...
after ...
afterReturn ...

根據(jù)控制臺輸出,可以看到執(zhí)行順序。

織入

織入是生成代理對象并把切面內(nèi)容放入約定流程的過程,上述示例中連接點所在的類都是擁有接口的類。事實上即使沒有接口,Spring 也能提供 AOP 功能,但是在 Spring 中建議使用接口編程,因為這樣的好處是使定義和實現(xiàn)分離,有利于變化和替換,更加靈活。

給通知傳遞參數(shù)

如果需要給通知傳遞參數(shù)的話,則需要修改通知方法。首先修改連接點為一個多參數(shù)的方法。

public void printRole(Role role, int sort) {
    System.out.println("{id: " + role.getId() + ", role_name: "
        + role.getRoleName() + ", note: " + role.getNote() + "}");
    System.out.println(sort);
}

這樣在通知中應(yīng)該如下定義方法。

@Before("execution(* com.wyk.aopdemo.service.impl.RoleServiceImpl.printRole(..)) && args(role, sort)")
public void before(Role role, Sort sort) {
    System.out.println("before ...");
}

引入

有時候,我們希望在流程中引入其它類的方法來得到更好的實現(xiàn),這時需要使用 Spring 的引入技術(shù)。

首先新建一個接口,用來檢查角色是否為空。

package com.wyk.aopdemo.service;

import com.wyk.aopdemo.domain.Role;

public interface RoleVerifier {
    public boolean verify(Role role);
}

編寫其實現(xiàn)類。

package com.wyk.aopdemo.service.impl;

import com.wyk.aopdemo.domain.Role;
import com.wyk.aopdemo.service.RoleVerifier;
import org.springframework.stereotype.Component;

public class RoleVerifierImpl implements RoleVerifier {
    public boolean verify(Role role) {
        return role != null;
    }
}

在切面 RoleAspect 類中添加一個新屬性,用來將 RoleVerifier 加入到切面之中。

@DeclareParents(value = "com.wyk.aopdemo.service.impl.RoleServiceImpl+",
        defaultImpl = RoleVerifierImpl.class)
public RoleVerifier roleVerifier;

注解 @DeclareParents 用來引入類。

  • value = "com.wyk.aopdemo.service.impl.RoleServiceImpl+" 表示對 RoleServiceImpl 類進(jìn)行增強(qiáng),也就是在 RoleServiceImpl 中添加一個新的接口。
  • defaultImpl 代表默認(rèn)的實現(xiàn)類。

修改測試方法,進(jìn)行測試。

public class Main {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
        RoleService roleService = (RoleService)ctx.getBean(RoleService.class);
        RoleVerifier roleVerifier = (RoleVerifier)roleService;
        Role role = new Role();
        role.setId(1L);
        role.setRoleName("wyk");
        role.setNote("haha");
        if(roleVerifier.verify(role)) {
            roleService.printRole(role);
        }
    }
}

此方法的原理是讓代理對象掛到 RoleService 和 RoleVerifier 兩個接口之下,這樣就可以在它們之間進(jìn)行強(qiáng)制轉(zhuǎn)換。

使用 XML 配置開發(fā) Spring AOP

使用 XML 方式開發(fā) AOP 和使用注解的原理相同。

接口和接口實現(xiàn)類和上面的相同,這里不在描述。切面類和前面的例子也相同,只是不需要添加注解。

package com.wyk.xmlaop.aspect;

import org.aspectj.lang.ProceedingJoinPoint;

public class XmlAspect {
    public void before() {
        System.out.println("before ...");
    }

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

    public void afterThrowing() {
        System.out.println("after-throwing ...");
    }

    public void afterReturning() {
        System.out.println("after-returning ...");
    }

    public void around(ProceedingJoinPoint jp) {
        System.out.println("around before ...");
        try {
            jp.proceed();
        } catch (Throwable e) {
            new RuntimeException("回調(diào)原有流程,產(chǎn)生異常 ...");
        }
        System.out.println("around after ...");
    }
}

添加通知

在類路徑下面添加 XML 配置文件 spring-cfg.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-4.0.xsd
           http://www.springframework.org/schema/context
           http://www.springframework.org/schema/context/spring-context-4.0.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop-4.0.xsd
           ">
    <bean id="xmlAspect" class="com.wyk.xmlaop.aspect.XmlAspect" />
    <bean id="roleService" class="com.wyk.xmlaop.service.impl.RoleServiceImpl" />
    <aop:config>
        <!-- 引用xmlAspect作為切面 -->
        <aop:aspect ref="xmlAspect">
            <!--定義切點-->
            <aop:pointcut id="printRole" expression="execution(* com.wyk.xmlaop.service.impl.RoleServiceImpl.printRole(..))" />
            <!--定義通知,引入切點-->
            <aop:before method="before" pointcut-ref="printRole" />
            <aop:after method="after" pointcut-ref="printRole" />
            <aop:after-throwing method="afterThrowing" pointcut-ref="printRole" />
            <aop:after-returning method="afterReturning" pointcut-ref="printRole" />
            <aop:around method="around" pointcut-ref="printRole" />
        </aop:aspect>
    </aop:config>
</beans>

在文件中通過<aop:config>定義 AOP 的內(nèi)容信息。

  • <aop:aspect> 定義切面類。
  • <aop:before> 定義前置通知。
  • <aop:after> 定義后置通知。
  • <aop:after-throwing> 定義異常通知。
  • <aop:after-returning> 定義返回通知。
  • <aop:around> 定義環(huán)繞通知。

進(jìn)行測試

創(chuàng)建測試類進(jìn)行測試。

public class Main {
    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-cfg.xml");
        RoleService roleService = ctx.getBean(RoleService.class);
        Role role = new Role();
        role.setId(1L);
        role.setRoleName("wyk");
        role.setNote("haha");
        roleService.printRole(role);
    }
}

運(yùn)行程序,查看結(jié)果。

before ...
around before ...
{id: 1, role_name: wyk, note: haha}
around after ...
after-returning ...
after ...

這里的順序貌似和注解版不一樣,不知道為啥。。。

給通知添加參數(shù)

修改 before 方法。

public void before(Role role) {
    System.out.println("before ...");
}

然后 XML 中的配置也要修改。

<aop:before method="before" pointcut="execution(* com.wyk.xmlaop.service.impl.RoleServiceImpl.printRole(..)) and aegs(role)" />

和注解版不同的是這里使用 and 而不是 && ,這是因為 & 是 xml 保留字符。

引入

引入方法和上面的例子相同,這里不再贅述,其中 XML 需要添加如下配置。

<aop:declare-parents type-matching="com.wyk.xmlaop.service.impl.RoleServiceImpl+"
    implement-interface="com.wyk.xmlaop.service.RoleVerifier"
    default-impl="com.wyk.xmlaop.service.RoleVerifierImpl" />

多個切面

Spring 中也能支持一個連接點包含多個切面,但多個切面之間的順序是隨機(jī)的,可以在切面類上使用 @Order注解進(jìn)行排序。如果是 XML 配置文件,也可以在配置標(biāo)簽上添加 order 屬性。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容