5.3 Spring5源碼–Spring AOP使用接口方式實現

Spring 提供了很多的實現AOP的方式:Spring 接口方式,schema配置方式和註解.

本文重點介紹Spring使用接口方式實現AOP. 使用接口方式實現AOP以了解為目的. 更好地理解動態代理. 通常我們使用的更多的是使用註解的方式實現AOP

下面來看看如何實現接口方式的AOP

一. 環境準備

要在項目中使用Spring AOP 則需要在項目中導入除了spring jar包之外, 還需要引入aspectjrt.jar,aspectjweaver.jar,aopalliance.jar ,spring-aop-3.2.0.M2.jar和cglib.jar 

二、Spring接口方式實現AOP步驟

使用Spring aop接口方式實現aop, 可以通過自定義通知來供Spring AOP識別. 常見的自己定義通知有:前置通知, 後置通知, 返回通知, 異常通知, 環繞通知. 對應實現的接口是:

  • 前置通知: MethodBeforeAdvice
  • 後置通知: AfterAdvice
  • 返回通知:AfterReturningAdvice
  • 異常通知:ThrowsAdvice
  • 環繞通知:MethodInterceptor

實現步驟如下:

1. 業務接口實現

package com.lxl.www.aop.interfaceAop;

/**
 * 使用接口方式實現AOP, 默認通過JDK的動態代理來實現. 非接口方式, 使用的是cglib實現動態代理
 *
 * 業務接口類-- 計算器接口類
 *
 * 定義三個業務邏輯方法
 */
public interface IBaseCalculate {

    int add(int numA, int numB);

    int sub(int numA, int numB);

    int div(int numA, int numB);

    int multi(int numA, int numB);

    int mod(int numA, int numB);

}

 

2. 業務類

package com.lxl.www.aop.interfaceAop;//業務類,也是目標對象

import com.lxl.www.aop.Calculate;

import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;

/**
 * 業務實現類 -- 基礎計算器
 */
@Service
public class BaseCalculate implements IBaseCalculate {

    @Override
    public int add(int numA, int numB) {
        System.out.println("執行目標方法: add");
        return numA + numB;
    }

    @Override
    public int sub(int numA, int numB) {
        System.out.println("執行目標方法: sub");
        return numA - numB;
    }

    @Override
    public int multi(int numA, int numB) {
        System.out.println("執行目標方法: multi");
        return numA * numB;
    }

    @Override
    public int div(int numA, int numB) {
        System.out.println("執行目標方法: div");
        return numA / numB;
    }

    @Override
    public int mod(int numA, int numB) {
        System.out.println("執行目標方法: mod");

        int retVal = ((Calculate) AopContext.currentProxy()).add(numA, numB);
        return retVal % numA;
    }
}

 

3. 通知類

前置通知

package com.lxl.www.aop.interfaceAop;

import org.springframework.aop.MethodBeforeAdvice;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 定義前置通知
 */
@Component
public class BaseBeforeAdvice implements MethodBeforeAdvice {

    /**
     *
     * @param method 切入的方法
     * @param args 切入方法的參數
     * @param target 目標對象
     * @throws Throwable
     */
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("===========進入beforeAdvice()============");

        System.out.println("目標對象:" + target);
        System.out.println("方法名: "+method);

        System.out.println("即將進入切入點方法");
    }

}

 

後置通知

package com.lxl.www.aop.interfaceAop;

import org.aspectj.lang.annotation.AfterReturning;
import org.springframework.aop.AfterAdvice;
import org.springframework.aop.AfterReturningAdvice;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

public class BaseAfterReturnAdvice implements AfterReturningAdvice {

    /**
     *
     * @param returnValue 切入點執行完方法的返回值,但不能修改
     * @param method 切入點方法
     * @param args 切入點方法的參數數組
     * @param target 目標對象
     * @throws Throwable
     */
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("==========進入afterReturning()=========== \n");
        System.out.println("切入點方法執行完成");

        System.out.println("後置通知--目標對象:" + target);
        System.out.println("後置通知--方法名: "+method);
        System.out.println("後置通知--方法入參: "+ args.toString());
        System.out.println("後置通知--方法返回值: "+ returnValue);
    }

}

 

異常通知

package com.lxl.www.aop.interfaceAop;

import org.springframework.aop.ThrowsAdvice;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
@Component
public class BaseAfterThrowsAdvice implements ThrowsAdvice {

    /**
     * @param method    可選:切入的方法
     * @param args      可選:切入的方法的參數
     * @param target    可選:目標對象
     * @param throwable 必填 : 異常子類,出現這個異常類的子類,則會進入這個通知。
     */
    public void afterThrowing(Method method, Object[] args, Object target, Throwable throwable) {
        System.out.println("出錯啦");
    }
}

 

環繞通知

package com.lxl.www.aop.interfaceAop;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 環繞通知
 */
@Component
public class BaseAroundAdvice implements MethodInterceptor {

    /**
     * invocation :連接點
     */
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("===========around環繞通知方法 開始===========");

        // 調用目標方法之前執行的動作
        System.out.println("環繞通知--調用方法之前: 執行");

        // 調用方法的參數
        Object[] args = invocation.getArguments();
        // 調用的方法
        Method method = invocation.getMethod();
        // 獲取目標對象
        Object target = invocation.getThis();
        System.out.println("輸入參數:" + args[0] + ";" + method + ";" + target);

        // 執行完方法的返回值:調用proceed()方法,就會觸發切入點方法執行
        Object returnValue = invocation.proceed();

        System.out.println("環繞通知--調用方法之後: 執行");
      
        System.out.println("輸出參數:" + args[0] + ";" + method + ";" + target + ";" + returnValue);

        System.out.println("===========around環繞通知方法  結束===========");

        return returnValue;
    }

}

 

4. 自定義切點

package com.lxl.www.aop.interfaceAop;

/**
 * 切點
 *
 * 繼承NameMatchMethodPointcut類,來用方法名匹配
 */
import org.springframework.aop.support.NameMatchMethodPointcut;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Component
public class Pointcut extends NameMatchMethodPointcut {

    private static final long serialVersionUID = 3990456017285944475L;

    @SuppressWarnings("rawtypes")
    @Override
    public boolean matches(Method method, Class targetClass) {
        // 設置單個方法匹配
        this.setMappedName("add");
        // 設置多個方法匹配
        String[] methods = { "add", "div" };
      
        //也可以用「 * 」 來做匹配符號
        // this.setMappedName("get*");
      
        this.setMappedNames(methods);

        return super.matches(method, targetClass);
    }

}

 

5. 配置xml文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="//www.springframework.org/schema/beans"
       xmlns:xsi="//www.w3.org/2001/XMLSchema-instance"
       xmlns:p="//www.springframework.org/schema/p"
       xmlns:context="//www.springframework.org/schema/context"
       xmlns:aop="//www.springframework.org/schema/aop"
       xsi:schemaLocation="
          http://www.springframework.org/schema/beans
          http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

          http://www.springframework.org/schema/context
          http://www.springframework.org/schema/context/spring-context-3.0.xsd

         http://www.springframework.org/schema/aop
          http://www.springframework.org/schema/aop/spring-aop-3.0.xsd"
>

    <!-- ==============================aop配置================================ -->
    <!-- 聲明一個業務類 -->
    <bean id="baseCalculate" class="com.lxl.www.aop.interfaceAop.BaseCalculate"/>

    <!-- 聲明通知類 -->
    <bean id="baseBefore" class="com.lxl.www.aop.interfaceAop.BaseBeforeAdvice"/>
    <bean id="baseAfterReturn" class="com.lxl.www.aop.interfaceAop.BaseAfterReturnAdvice"/>
    <bean id="baseAfterThrows" class="com.lxl.www.aop.interfaceAop.BaseAfterThrowsAdvice"/>
    <bean id="baseAround" class="com.lxl.www.aop.interfaceAop.BaseAroundAdvice"/>

    <!-- 指定切點匹配類 -->
    <bean id="pointcut" class="com.lxl.www.aop.interfaceAop.Pointcut"/>

    <!-- 包裝通知,指定切點 -->
    <bean id="matchBeforeAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="pointcut">
            <ref bean="pointcut"/>
        </property>
        <property name="advice">
            <ref bean="baseBefore"/>
        </property>
    </bean>

    <!-- 使用ProxyFactoryBean 產生代理對象 -->
    <bean id="businessProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <!-- 代理對象所實現的接口 ,如果有接口可以這樣設置 -->
        <property name="proxyInterfaces">
            <value>com.lxl.www.aop.interfaceAop.IBaseCalculate</value>
        </property>

        <!-- 設置目標對象 -->
        <property name="target">
            <ref bean="baseCalculate"/>
        </property>
        <!-- 代理對象所使用的攔截器 -->
        <property name="interceptorNames">
            <list>
                <value>matchBeforeAdvisor</value>
                <value>baseAfterReturn</value>
                <value>baseAround</value>
            </list>
        </property>
    </bean>
</beans>

 

6. 方法入口

package com.lxl.www.aop.interfaceAop;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class InterfaceMainClass{

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("aop/aop.xml");
        BaseCalculate calculate = (BaseCalculate) context.getBean("baseCalculate");
        calculate.add(1, 3);
    }

}

 

三. 分析

各種類型通知的執行順序: 前置方法會在切入點方法之前執行,後置會在切入點方法執行之後執行,環繞則會在切入點方法執行前執行同事方法結束也會執行對應的部分。主要是調用proceed()方法來執行切入點方法。來作為環繞通知前後方法的分水嶺

在xml 配置 businessProxy這個bean的時候,ProxyFactoryBean類中指定了,proxyInterfaces參數。這裡把他配置了IBaseCalculate接口。因為在項目開發過程中,往往業務類都會有對應的接口,以方便利用IOC解耦。但Spring AOP卻也能支持沒有接口的代理。這就是為什麼需要導入cglib.jar包了。看過spring的源碼,知道在目標切入對象如果有實現接口,spring會默認使用jdk動態代理來實現代理類。如果沒有接口,則會通過cglib來實現代理類。

  這個業務類現在有 前置通知,後置通知,環繞三個通知同時作用,可能以及更多的通知進行作用。那麼這些通知的執行順序是怎麼樣的?就這個例子而言,同時實現了三個通知。在例 子xml中,則顯示執行before通知,然後執行around的前處理,執行切點方法,再執行return處理。最後執行around的後處理。經過測 試,知道spring 處理順序是按照xml配置順序依次處理通知,以隊列的方式存放前通知,以壓棧的方式存放後通知。所以是前通知依次執行,後通知到切入點執行完之後,從棧里 在後進先出的形式把後通知執行。
  在實現過程中發現通知執行對應目標對象的整個類中的方法,如何精確到某個方法,則需要定義一個切點匹配的方式:spring提供了方法名匹配或正則方式來匹配。然後通過DefaultPointcutAdvisor來包裝通知,指定切點。

 使用接口方式配置起來,可見代碼還是非常的厚重的,定義一個切面就要定義一個切面類,然而切面類中,就一個通知方法,着實沒有必要。所以Spring提供了,依賴aspectj的schema配置和基於aspectj 註解方式。這兩種方式非常簡單方便使用,也是項目中普遍的使用方式。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

as