Spring系列22:Spring AOP 概念與快速入門篇


本文內容

  1. Spring AOP含義和目標
  2. AOP相關概念
  3. 聲明式AOP快速入門
  4. 編程式創建代理對象

Spring AOP含義和目標

OOP: Object-oriented Programming 面向對象編程,大家再熟悉不過了

AOP:Aspect-oriented Programming 面向切面編程

面向切面編程通過提供另一種思考程序結構的方式來補充面向對象編程。OOP 中模塊化的關鍵單元是類,而 AOP 中模塊化的單元是切面。

Spring 的關鍵組件之一是 AOP 框架。Spring IoC 容器不依賴 AOP,AOP 對 Spring IoC 的補充提供了非常強大的中間件解決方案。主要用於下面2方面:

  • 提供聲明式服務。最重要的此類服務是聲明式事務管理。
  • 讓用戶實現自定義切面,用 AOP 補充他們對 OOP 的使用。

Spring AOP 的能力和目標

Spring AOP 是用純 Java 實現的。不需要特殊的編譯過程。 Spring AOP 不需要控制類加載器層次結構,因此適用於 servlet 容器或應用程序服務器。

Spring AOP 當前僅支持方法執行連接點(建議在 Spring bean 上執行方法)。字段攔截未實現。

Spring AOP 的 AOP 方法不同於大多數其他 AOP 框架。盡管 Spring AOP 非常強,其目的不是提供最完整的 AOP 實現,相反,其目的是提供 AOP 實現和 Spring IoC 之間的緊密集成,以幫助解決企業應用程序中的常見問題。因此,Spring Framework 的 AOP 功能通常與 Spring IoC 容器結合使用。切面是通過使用普通的 bean 定義語法來配置的(盡管這允許強大的“自動代理”功能),這是與其他 AOP 實現的關鍵區別。

Spring AOP 從不努力與 AspectJ 競爭以提供全面的 AOP 解決方案。Spring AOP 等基於代理的框架和 AspectJ 等成熟框架都很有價值,它們是互補的,而不是競爭的。Spring 將 Spring AOP 和 IoC 與 AspectJ 無縫集成,以在一致的基於 Spring 的應用程序架構中實現 AOP 的所有使用。此集成不會影響 Spring AOP API 或 AOP Alliance API。 Spring AOP 保持向后兼容。

AOP相關概念

先了解一下核心 AOP 概念和術語,方便后面深入使用。

切面 Aspect

跨多個類的關注點的模塊化。事務管理是企業 Java 應用程序中橫切關注點的一個很好的例子。

連接點 Join point

程序執行過程中的一個點,例如方法的執行或異常的處理。在 Spring AOP 中,一個連接點總是代表一個方法執行。

通知 Advice

切面在特定連接點采取的操作。不同類型如前置通知,環繞通知等。 Spring將通知建模為攔截器,並在連接點周圍維護一系列攔截器。

切點 Pointcut

匹配連接點的謂詞。 Advice 與切入點表達式相關聯,並在與切入點匹配的任何連接點處運行(例如執行具有特定名稱的方法)。切入點表達式匹配的連接點的概念是 AOP 的核心,Spring 默認使用 AspectJ 切入點表達式語言。

引介 Introduction

在類上聲明其他方法或字段。 Spring AOP 允許向任何被增強的對象引入新接口和相應的實現。例如,可以使用 Introduction 使 bean 實現 IsModified 接口,以簡化緩存。

目標對象 Target object

由一個或多個切面增強的對象。

代理 AOP proxy

由 AOP 框架創建的一個對象,用於實現切面邏輯如增強方法執行等。在 Spring Framework 中,AOP 代理是 JDK 動態代理或 CGLIB 代理。

織入 Weaving

將切面與其他應用程序類型或對象鏈接以創建增強對象。這可以在編譯時如使用 AspectJ 編譯器、加載時或運行時完成。 Spring AOP 與其他純 Java AOP 框架一樣,在運行時執行編織。

結合網上的一張圖理解下。

切入點匹配的連接點的概念是 AOP 的關鍵,這將它與僅提供攔截的舊技術區分開來。切入點使增強Advice的目標獨立於面向對象的層次結構。如可以將提供聲明性事務管理的環繞通知應用到一組跨越多個對象(例如服務層中的所有業務操作)的方法。

12

快速入門

通過注解,聲明式的Spring AOP 的使用比較簡單,主要步驟如下:

  1. 通過 @EnableAspectJAutoProxy 啟用自動生成代理;
  2. 通過@Aspect 定義切面並注入到Spring容器中;
  3. 切面中可以通過@Pointcut定義切點;
  4. 切面通過@Before等通知注解來定義通知
    • @Around 環繞通知
    • @Before 前置通知
    • @After 最終通知
    • @AfterReturning 返回通知
    • @AfterThrowing 異常拋出后通知
  5. 從容器中獲取bean使用

以非入侵的方式記錄類方法開始執行的日志為例,完整看一個AOP的例子。

  1. 引入aspectjweaver依賴

    <!--Aop需要的庫-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
    </dependency>
    
  2. 啟用 @AspectJ 注解支持

    @Configuration
    @ComponentScan
    @EnableAspectJAutoProxy()
    public class AppConfig {}
    
  3. 定義目標對象

    @Service
    public class UserService {
        public void add(String name) {
            System.out.println("UserService add " + name);
        }
    }
    
  4. 聲明切面、切點和通知

    /**
     * 切面定義
     * 包含切點 通知 引入等
     *
     * @author zfd
     * @version v1.0
     * @date 2022/1/29 13:33
     * @關於我 請關注公眾號 螃蟹的Java筆記 獲取更多技術系列
     */
    @Component
    @Aspect // 切面
    public class MyAspect {
    
        /**
         * 聲明切入點  這里表達式指:攔截UserService類所有方法執行
         */
        @Pointcut("execution(* com.crab.spring.aop.demo01.UserService.*(..))")
        public void pc(){}
    
        /**
         * 前置通知,指定切入點
         * @param joinPoint
         */
        @Before("pc()")
        public void before(JoinPoint joinPoint) {
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            System.out.println("我是前置通知!開始執行方法:" + signature.getMethod().getName()); // 方法執行前記錄日志
        }
    }
    
  5. 執行目標方法

    public static void main(String[] args) {
            AnnotationConfigApplicationContext context =
                    new AnnotationConfigApplicationContext(AppConfig.class);
            IService bean = context.getBean(IService.class);
            System.out.println("bean的類型:" + bean.getClass());
            bean.add("xxx");
            context.close();
    }
    
  6. 觀察輸出結果

    bean的類型:class com.sun.proxy.$Proxy19
    我是前置通知!開始執行方法:add
    UserService add xxx
    

    從結果看成功攔截了,代理對象是通過JDK動態代理生成的。

  7. 類比不采用AOP的方式

    上面的效果類似於下面的硬編碼的寫法

    @Service
    public class UserService {
        public void add(String name) {
            System.out.println("我是前置通知!開始執行方法:" + "add");
            System.out.println("UserService add " + name);
        }
    }
    

編程式創建代理

上面的快速入門式通過注解聲明式自動創建代理的,好處是簡單方便,缺點是使用者不清楚的創建過程和細節。為了深入了解AOP中代理式如何創建,我們看下編程式如何創建代理對象,主要類圖如下。

image-20220206165026199

設計的接口或是基類是代理配置類AdvisedSupport、創建代理的工廠類AopProxyFactory和AopProxy ,總共4種手動創建代理對象的方式。

方式1:AdvisedSupport + AopProxyFactory 方式

這種方式最原始最基礎的,其它方式也是在此基礎上做封裝和簡化創建的。創建的代理對象主要考慮3個方面:

  • 目標對象
  • 代理方式的配置
  • 如何創建代理對象

直接上案例。

/**
 * 方式1
 * 使用 AdvisedSupport + AopProxyFactory
 */
@Test
public void test1() {
    // 1、目標對象
    UserService target = new UserService();
    // 2 代理配置信息
    AdvisedSupport advisedSupport = new AdvisedSupport();
    advisedSupport.setTarget(target); // 目標對象
    advisedSupport.addInterface(IService.class);// 代理的接口
    advisedSupport.setProxyTargetClass(true);// 、強制cglib代理
    advisedSupport.addAdvice(new MethodBeforeAdvice() {
        @Override
        public void before(Method method, Object[] args, Object target) throws Throwable {
            System.out.println("前置通知,開始執行方法: " + method.getName());
        }
    });

    // 3 創建代理對象的工廠
    DefaultAopProxyFactory proxyFactory = new DefaultAopProxyFactory();
    AopProxy aopProxy = proxyFactory.createAopProxy(advisedSupport);

    // 4 獲取代理對象
    Object proxy = aopProxy.getProxy();

    // 5 查看代理的信息
    System.out.println("代理對象的類型:"+proxy.getClass());
    System.out.println("代理對象的父類:"+proxy.getClass().getSuperclass());
    System.out.println("代理對象實現的接口如下:");
    for (Class<?> itf : proxy.getClass().getInterfaces()) {
        System.out.println(itf);
    }

}

代碼注釋比較清晰,來看下輸出結果。

代理對象的類型:class com.crab.spring.aop.demo01.UserService$$EnhancerBySpringCGLIB$$87584fdb
代理對象的父類:class com.crab.spring.aop.demo01.UserService
代理對象實現的接口如下:
interface com.crab.spring.aop.demo01.IService
interface org.springframework.aop.SpringProxy
interface org.springframework.aop.framework.Advised
interface org.springframework.cglib.proxy.Factory

結果看:

  1. 強制采用了CGLIB代理類的方式
  2. 默認實現了3個額外的接口SpringProxy、 Advised、Factory,后面2篇AOP源碼解析會分析如何來的。

方式2:ProxyFactory

原始的方式需要同時操作代理的配置和代理工廠創建類,相對還是比較繁雜的,ProxyFactory 中引用了AopProxyFactory,一定程度簡化了創建過程。直接上案例。

/**
 * 方式2
 * 使用 ProxyFactory 簡化, ProxyFactory中組合了AopProxyFactory
 */
@Test
public void test2() {
    // 1、目標對象
    UserService target = new UserService();
    // 2 創建代理對象的工廠,同時代理配置信息
    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.setTarget(target);// 目標對象
    proxyFactory.addInterface(IService.class);// 實現接口
    // 添加通知
    proxyFactory.addAdvice(new MethodBeforeAdvice() {
        @Override
        public void before(Method method, Object[] args, Object target) throws Throwable {
            System.out.println("前置通知,開始執行方法: " + method.getName());
        }
    });
    // 3 獲取代理對象
    Object proxy = proxyFactory.getProxy();

    // 5 調用方法
    IService service = (IService) proxy;
    service.hello("xx");
}

代理信息的配置可以直接通過ProxyFactory設置。看下結果。

前置通知,開始執行方法: hello
hello xx

方式3:AspectJProxyFactory

AspectJProxyFactory 可以結合@Aspect的聲明的切面來創建代理對象的。理解這種方式對理解@Aspect聲明式使用AOP的方式很有幫助,詳細見我們的單獨的源碼分析的文章。

直接上案例。

切面定義,含切點和通知。

/**
 * @author zfd
 * @version v1.0
 * @date 2022/2/6 17:08
 * @關於我 請關注公眾號 螃蟹的Java筆記 獲取更多技術系列
 */
@Aspect
public class MyAspect {

    /**
     * 聲明切入點  這里表達式指:攔截UserService類所有方法執行
     */
    @Pointcut("execution(* com.crab.spring.aop.demo01.UserService.*(..))")
    public void pc(){}

    /**
     * 前置通知,指定切入點
     * @param joinPoint
     */
    @Before("pc()")
    public void before(JoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        System.out.println("我是前置通知!開始執行方法:" + signature.getMethod().getName());
    }
}

使用如下

/**
 * 方式3 使用AspectProxyFactory結合@Aspect切面方式
 */
@Test
public void test3(){
    // 1、目標對象
    UserService target = new UserService();
    // 2 創建代理對象的工廠,同時代理配置信息
    AspectJProxyFactory proxyFactory = new AspectJProxyFactory();
    proxyFactory.setTarget(target);
    proxyFactory.setInterfaces(IService.class);
    // 設置切面 含通知和切點
    proxyFactory.addAspect(MyAspect.class);

    // 3 創建代理對象
    IService proxy = proxyFactory.getProxy();

    // 4 執行目標方法
    proxy.hello("xx");
}

結果如下

前置通知: execution(void com.crab.spring.aop.demo02.IService.hello(String))
hello xx

方式4:ProxyFactoryBean

ProxyFactoryBean用來在spring環境中給指定的bean創建代理對象,用到的不是太多,了解即可。直接上案例。

不同於前面的方式,這種方式的目標對象和通知的設置方式是通過指定容器中的bean名稱來設置的。

package com.crab.spring.aop.demo01;

import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.aop.framework.ProxyFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.lang.reflect.Method;

/**
 * ProxyFactoryBean 方式創建代理
 * @author zfd
 * @version v1.0
 * @date 2022/2/6 17:20
 * @關於我 請關注公眾號 螃蟹的Java筆記 獲取更多技術系列
 */
@Configuration
public class AopProxyFactoryBeanConfig {

    // 1 注冊目標對象
    @Bean("userService")
    public UserService userService() {
        return new UserService();
    }

    // 2 注冊通知
    @Bean("beforeAdvice")
    public MethodBeforeAdvice beforeAdvice() {
        return new MethodBeforeAdvice() {
            @Override
            public void before(Method method, Object[] args, Object target) throws Throwable {
                System.out.println("前置通知: " + method);
            }
        };
    }

    // 3 注冊ProxyFactoryBean
    @Bean("userServiceProxy")
    public ProxyFactoryBean userServiceProxy() {
        ProxyFactoryBean proxyFactoryBean = new ProxyFactoryBean();
        // 設置目標對象的bean名稱
        proxyFactoryBean.setTargetName("userService");
        // 設置攔截器的bean名稱
        proxyFactoryBean.setInterceptorNames("beforeAdvice");
        // 代理方式
//        proxyFactoryBean.setProxyTargetClass(true);
        return proxyFactoryBean;
    }

}

測試程序和結果

    /**
     * 方式4 使用ProxyFactoryBean
     */
    @Test
    public void test04() {
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(AopProxyFactoryBeanConfig.class);

        // 面向接口,支持Jdk或是CGLIB
        IService userService = (IService) context.getBean("userServiceProxy");

        // 面向類,只支持CGLIB,  proxyFactoryBean.setProxyTargetClass(true)
//        UserService userService = context.getBean("userServiceProxy", UserService.class);
        userService.hello("xxxx");

    }
// 結果
前置通知: public abstract void com.crab.spring.aop.demo01.IService.hello(java.lang.String)
hello xxxx

總結

本文主要是介紹了Spring AOP 的相關概念、聲明式AOP的入門使用,以及編程式創建代理的4種方式。

本篇源碼地址:https://github.com/kongxubihai/pdf-spring-series/tree/main/spring-series-aop/src/main/java/com/crab/spring/aop/demo01

知識分享,轉載請注明出處。學無先后,達者為先!


免責聲明!

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



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