Spring學習總結(二)——Spring AOP、靜態代理、JDK與CGLIB動態代理

AOP(Aspect Oriented Programming)意爲:面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點,也是Spring框架中的一個重要內容,是函數式編程的一種衍生範型。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程序的可重用性,同時提高了開發的效率。

一、爲什麼需要代理模式

假設需實現一個計算的類Math、完成加、減、乘、除功能,如下所示:

package cn.liuw.aop;

public class Math {
    //加
    public int add(int n1,int n2){
        int result=n1+n2;
        System.out.println(n1+"+"+n2+"="+result);
        return result;
    }
    
    
    //減
    public int sub(int n1,int n2){
        int result=n1-n2;
        System.out.println(n1+"-"+n2+"="+result);
        return result;
    }
    
    //乘
    public int mut(int n1,int n2){
        int result=n1*n2;
        System.out.println(n1+"X"+n2+"="+result);
        return result;
    }
    
    //除
    public int div(int n1,int n2){
        int result=n1/n2;
        System.out.println(n1+"/"+n2+"="+result);
        return result;
    }
}

現在需求發生了變化,要求項目中所有的類在執行方法時輸出執行耗時。最直接的辦法是修改源代碼,如下所示:

package cn.liuw.aop;

import java.util.Random;

public class Math {
    //加
    public int add(int n1,int n2){
        //開始時間
        long start=System.currentTimeMillis();
        lazy();
        int result=n1+n2;
        System.out.println(n1+"+"+n2+"="+result);
        Long span= System.currentTimeMillis()-start;
        System.out.println("共用時:"+span);
        return result;
    }
    
    //減
    public int sub(int n1,int n2){
        //開始時間
        long start=System.currentTimeMillis();
        lazy();
        int result=n1-n2;
        System.out.println(n1+"-"+n2+"="+result);
        Long span= System.currentTimeMillis()-start;
        System.out.println("共用時:"+span);
        return result;
    }
    
    //乘
    public int mut(int n1,int n2){
        //開始時間
        long start=System.currentTimeMillis();
        lazy();
        int result=n1*n2;
        System.out.println(n1+"X"+n2+"="+result);
        Long span= System.currentTimeMillis()-start;
        System.out.println("共用時:"+span);
        return result;
    }
    
    //除
    public int div(int n1,int n2){
        //開始時間
        long start=System.currentTimeMillis();
        lazy();
        int result=n1/n2;
        System.out.println(n1+"/"+n2+"="+result);
        Long span= System.currentTimeMillis()-start;
        System.out.println("共用時:"+span);
        return result;
    }
    
    //模擬延時
    public void lazy()
    {
        try {
            int n=(int)new Random().nextInt(500);
            Thread.sleep(n);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

測試運行:

package cn.liuw.aop;

public class Test {
    
    @org.junit.Test
    public void test01()
    {
        Math math=new Math();
        int n1=100,n2=5;
        math.add(n1, n2);
        math.sub(n1, n2);
        math.mut(n1, n2);
        math.div(n1, n2);
    }
}

運行結果:

缺點:

1、工作量特別大,如果項目中有多個類,多個方法,則要修改多次。

2、違背了設計原則:開閉原則(OCP),對擴展開放,對修改關閉,而爲了增加功能把每個方法都修改了,也不便於維護。

3、違背了設計原則:單一職責(SRP),每個方法除了要完成自己本身的功能,還要計算耗時、延時;每一個方法引起它變化的原因就有多種。

4、違背了設計原則:依賴倒轉(DIP),抽象不應該依賴細節,兩者都應該依賴抽象。而在Test類中,Test與Math都是細節。

使用靜態代理可以解決部分問題。

二、靜態代理

 1、定義抽象主題接口

package cn.liuw.aop;

/**
 * 接口
 * 抽象主題
 */
public interface IMath {
    //加
    int add(int n1, int n2);

    //減
    int sub(int n1, int n2);

    //乘
    int mut(int n1, int n2);

    //除
    int div(int n1, int n2);

}

2、主題類,算術類,實現抽象接口

package cn.liuw.aop;

/**
 * 被代理的目標對象
 *真實主題
 */
public class Math implements IMath {
    //加
    public int add(int n1,int n2){
        int result=n1+n2;
        System.out.println(n1+"+"+n2+"="+result);
        return result;
    }
    
    //減
    public int sub(int n1,int n2){
        int result=n1-n2;
        System.out.println(n1+"-"+n2+"="+result);
        return result;
    }
    
    //乘
    public int mut(int n1,int n2){
        int result=n1*n2;
        System.out.println(n1+"X"+n2+"="+result);
        return result;
    }
    
    //除
    public int div(int n1,int n2){
        int result=n1/n2;
        System.out.println(n1+"/"+n2+"="+result);
        return result;
    }
}

3、代理類

package cn.liuw.aop;

import java.util.Random;

/**
 * 靜態代理類
 */
public class MathProxy implements IMath {

    //被代理的對象
    IMath math=new Math();
    
    //加
    public int add(int n1, int n2) {
        //開始時間
        long start=System.currentTimeMillis();
        lazy();
        int result=math.add(n1, n2);
        Long span= System.currentTimeMillis()-start;
        System.out.println("共用時:"+span);
        return result;
    }

    //減法
    public int sub(int n1, int n2) {
        //開始時間
        long start=System.currentTimeMillis();
        lazy();
        int result=math.sub(n1, n2);
        Long span= System.currentTimeMillis()-start;
        System.out.println("共用時:"+span);
        return result;
    }

    //乘
    public int mut(int n1, int n2) {
        //開始時間
        long start=System.currentTimeMillis();
        lazy();
        int result=math.mut(n1, n2);
        Long span= System.currentTimeMillis()-start;
        System.out.println("共用時:"+span);
        return result;
    }
    
    //除
    public int div(int n1, int n2) {
        //開始時間
        long start=System.currentTimeMillis();
        lazy();
        int result=math.div(n1, n2);
        Long span= System.currentTimeMillis()-start;
        System.out.println("共用時:"+span);
        return result;
    }

    //模擬延時
    public void lazy()
    {
        try {
            int n=(int)new Random().nextInt(500);
            Thread.sleep(n);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

4、測試運行

package cn.liuw.aop;

public class Test {
    
    IMath math=new MathProxy();
    @org.junit.Test
    public void test01()
    {
        int n1=100,n2=5;
        math.add(n1, n2);
        math.sub(n1, n2);
        math.mut(n1, n2);
        math.div(n1, n2);
    }
}

5、運行結果

6、小結

通過靜態代理,是否完全解決了上述的4個問題:

已解決:

1、解決了“開閉原則(OCP)”的問題,因爲並沒有修改Math類,而擴展出了MathProxy類。

2、解決了“依賴倒轉(DIP)”的問題,通過引入接口。

3、解決了“單一職責(SRP)”的問題,Math類不再需要去計算耗時與延時操作,但從某些方面講MathProxy還是存在該問題。

未解決:

4、如果項目中有多個類,則需要編寫多個代理類,工作量大,不好修改,不好維護,不能應對變化。

如果要解決上面的問題,可以使用動態代理。

三、動態代理,使用JDK內置的Proxy實現

只需要一個代理類,而不是針對每個類編寫代理類。

在上一個示例中修改代理類MathProxy如下:

package cn.liuw.aop;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Random;

/**
 * 動態代理類
 */
public class DynamicProxy implements InvocationHandler {

    //被代理的對象
    Object targetObject;
    
    /**
     * 獲得被代理後的對象
     * @param object 被代理的對象
     * @return 代理後的對象
     */
    public Object getProxyObject(Object object){
        this.targetObject=object;
        return Proxy.newProxyInstance(
                targetObject.getClass().getClassLoader(), //類加載器
                targetObject.getClass().getInterfaces(),  //獲得被代理對象的所有接口
                this);  //InvocationHandler對象
        //loader:一個ClassLoader對象,定義了由哪個ClassLoader對象來生成代理對象進行加載
        //interfaces:一個Interface對象的數組,表示的是我將要給我需要代理的對象提供一組什麼接口,如果我提供了一組接口給它,那麼這個代理對象就宣稱實現了該接口(多態),這樣我就能調用這組接口中的方法了
        //h:一個InvocationHandler對象,表示的是當我這個動態代理對象在調用方法的時候,會關聯到哪一個InvocationHandler對象上,間接通過invoke來執行
    }
    
    
    /**
     * 當用戶調用對象中的每個方法時都通過下面的方法執行,方法必須在接口
     * proxy 被代理後的對象
     * method 將要被執行的方法信息(反射)
     * args 執行方法時需要的參數
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //被織入的內容,開始時間
        long start=System.currentTimeMillis();
        lazy();
        
        //使用反射在目標對象上調用方法並傳入參數
        Object result=method.invoke(targetObject, args);
        
        //被織入的內容,結束時間
        Long span= System.currentTimeMillis()-start;
        System.out.println("共用時:"+span);
        
        return result;
    }
    
    //模擬延時
    public void lazy()
    {
        try {
            int n=(int)new Random().nextInt(500);
            Thread.sleep(n);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

測試運行:

package cn.liuw.aop;

public class Test {
    
    //實例化一個MathProxy代理對象
    //通過getProxyObject方法獲得被代理後的對象
    IMath math=(IMath)new DynamicProxy().getProxyObject(new Math());
    @org.junit.Test
    public void test01()
    {
        int n1=100,n2=5;
        math.add(n1, n2);
        math.sub(n1, n2);
        math.mut(n1, n2);
        math.div(n1, n2);
    }
    
    IMessage message=(IMessage) new DynamicProxy().getProxyObject(new Message());
    @org.junit.Test
    public void test02()
    {
        message.message();
    }
}

小結:

 JDK內置的Proxy動態代理可以在運行時動態生成字節碼,而沒必要針對每個類編寫代理類。中間主要使用到了一個接口InvocationHandler與Proxy.newProxyInstance靜態方法,參數說明如下:使用內置的Proxy實現動態代理有一個問題:被代理的類必須實現接口,未實現接口則沒辦法完成動態代理。如果項目中有些類沒有實現接口,則不應該爲了實現動態代理而刻意去抽出一些沒有實例意義的接口,通過cglib可以解決該問題。

、動態代理,使用cglib實現

CGLIB(Code Generation Library)是一個開源項目,是一個強大的,高性能,高質量的Code生成類庫,它可以在運行期擴展Java類與實現Java接口,通俗說cglib可以在運行時動態生成字節碼。

4.1、引用cglib,通過maven

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.2.3</version>
</dependency>

4.2、使用cglib完成動態代理

大概的原理是:cglib繼承被代理的類,重寫方法,織入通知,動態生成字節碼並運行,因爲是繼承所以final類是沒有辦法動態代理的。具體實現如下:

package cn.liuw.aop;

import java.lang.reflect.Method;
import java.util.Random;

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

/*
 * 動態代理類
 * 實現了一個方法攔截器接口
 */
public class DynamicProxy implements MethodInterceptor {

    // 被代理對象
    Object targetObject;

    //Generate a new class if necessary and uses the specified callbacks (if any) to create a new object instance. 
    //Uses the no-arg constructor of the superclass.
    //動態生成一個新的類,使用父類的無參構造方法創建一個指定了特定回調的代理實例
    public Object getProxyObject(Object object) {
        this.targetObject = object;
        //增強器,動態代碼生成器
        Enhancer enhancer=new Enhancer();
        //回調方法
        enhancer.setCallback(this);
        //設置生成類的父類類型
        enhancer.setSuperclass(targetObject.getClass());
        //動態生成字節碼並返回代理對象
        return enhancer.create();
    }

    // 攔截方法
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 被織入的橫切內容,開始時間 before
        long start = System.currentTimeMillis();
        lazy();

        // 調用方法
        Object result = methodProxy.invoke(targetObject, args);

        // 被織入的橫切內容,結束時間
        Long span = System.currentTimeMillis() - start;
        System.out.println("共用時:" + span);
        
        return result;
    }

    // 模擬延時
    public void lazy() {
        try {
            int n = (int) new Random().nextInt(500);
            Thread.sleep(n);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

參數:Object爲由CGLib動態生成的代理類實例,Method爲上文中實體類所調用的被代理的方法引用,Object[]爲參數值列表,MethodProxy爲生成的代理類對方法的代理引用。

返回:從代理實例的方法調用返回的值。

測試運行:

package cn.liuw.aop;

public class Test {
    //實例化一個DynamicProxy代理對象
    //通過getProxyObject方法獲得被代理後的對象
    Math math=(Math)new DynamicProxy().getProxyObject(new Math());
    @org.junit.Test
    public void test01()
    {
        int n1=100,n2=5;
        math.add(n1, n2);
        math.sub(n1, n2);
        math.mut(n1, n2);
        math.div(n1, n2);
    }
    //另一個被代理的對象,不再需要重新編輯代理代碼
    Message message=(Message) new DynamicProxy().getProxyObject(new Message());
    @org.junit.Test
    public void test02()
    {
        message.message();
    }
}

4.3、小結

使用cglib可以實現動態代理,即使被代理的類沒有實現接口,但被代理的類必須不是final類。

五、使用Spring實現AOP

橫切關注點:跨越應用程序多個模塊的方法或功能。(軟件系統,可以看做由一組關注點即業務或功能或方法組成。其中,直接的業務關注點是直切關注點,而爲直切關注點服務的,就是橫切關注點。)即是,與我們業務邏輯無關的,但是我們需要關注的部分,就是橫切關注點。

切面(ASPECT):橫切關注點被模塊化的特殊對象。即,它是一個類。

通知(Advice):切面必須要完成的工作。即,它是類中的一個方法。

目標(Target):被通知對象。

代理(Proxy):向目標對象應用通知之後創建的對象。

切入點(PointCut):切面通知執行的“地點”的定義。

連接點(JointPoint):與切入點匹配的執行點。

下面示意圖:

SpringAOP中,通過Advice定義橫切邏輯,Spring中支持5種類型的Advice:

5.1、定義通知(Advice)

 前置通知

package cn.liuw.aspect;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;

/**
 * @ClassName BeforeAdvice
 * @Description 前置通知
 **/
public class BeforeAdvice implements MethodBeforeAdvice{

    /**
     * @param method:方法
     * @param objects:參數
     * @param target:被代理的目標對象
     * @throws Throwable
     */
    @Override
    public void before(Method method, Object[] objects, Object target) throws Throwable {
        System.out.println("-----前置通知-----");
    }
}

後置通知

package cn.liuw.aspect;

import org.springframework.aop.AfterReturningAdvice;
import org.springframework.aop.MethodBeforeAdvice;
import java.lang.reflect.Method;

/**
 * @ClassName AfterAdvice
 * @Description 前置通知
 **/
public class AfterAdvice implements AfterReturningAdvice{

    /**
     * @param returnValue:返回值
     * @param method:被調用的方法
     * @param objects:方法參數
     * @param target:被代理對象
     * @throws Throwable
     */
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] objects, Object target) throws Throwable {
        System.out.println("-----後置通知-----");
    }
}

環繞通知

package cn.liuw.aspect;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.AfterReturningAdvice;

import java.lang.reflect.Method;

/**
 * @ClassName AfterAdvice
 * @Description 前置通知
 **/
public class SurroundAdvice implements MethodInterceptor{


    @Override
    public Object invoke(MethodInvocation method) throws Throwable {
        //前置橫切邏輯
        System.out.println("方法"+method.getMethod()+"被調用在對象"+method.getThis()+"上,參數"+method.getArguments());
        //方法調用
        Object result = method.proceed();
        //後置橫切邏輯
        System.out.println("返回值:"+result);
        return result;
    }
}

5.2、創建代理工廠、設置被代理對象、添加通知

import cn.liuw.aspect.AfterAdvice;
import cn.liuw.aspect.BeforeAdvice;
import cn.liuw.aspect.SurroundAdvice;
import org.junit.Test;
import org.springframework.aop.framework.ProxyFactory;
import cn.liuw.aspect.Math;

public class Test05 {
    @Test
    public void test01(){
        //實例化Spring代理工廠
        ProxyFactory factory = new ProxyFactory();
        //設置被代理的對象
        factory.setTarget(new Math());
        //添加通知,橫切邏輯
        factory.addAdvice(new BeforeAdvice());
        factory.addAdvice(new AfterAdvice());
        factory.addAdvice(new SurroundAdvice());
        //從代理工廠中獲得代理對象
        Math math = (Math) factory.getProxy();
        int n1 = 100,n2 = 5;
        math.add(n1,n2);
        math.sub(n1,n2);
        math.mut(n1,n2);
        math.div(n1,n2);
    }
}

運行結果:

-----前置通知-----
方法public int cn.liuw.aspect.Math.add(int,int)被調用在對象cn.liuw.aspect.Math@31a75098上,參數[Ljava.lang.Object;@5f9c449f
100+5=105
返回值:105
-----後置通知-----
-----前置通知-----
方法public int cn.liuw.aspect.Math.sub(int,int)被調用在對象cn.liuw.aspect.Math@31a75098上,參數[Ljava.lang.Object;@2af590c6
100-5=95
返回值:95
-----後置通知-----
-----前置通知-----
方法public int cn.liuw.aspect.Math.mut(int,int)被調用在對象cn.liuw.aspect.Math@31a75098上,參數[Ljava.lang.Object;@5ff3cd32
100X5=500
返回值:500
-----後置通知-----
-----前置通知-----
方法public int cn.liuw.aspect.Math.div(int,int)被調用在對象cn.liuw.aspect.Math@31a75098上,參數[Ljava.lang.Object;@7f24bed4
100/5=20
返回值:20
-----後置通知-----

Process finished with exit code 0

5.3、封裝代理創建邏輯

 在上面的示例中如果要代理不同的對象需要反覆創建ProxyFactory對象,代碼會冗餘。同樣以實現方法耗時爲示例代碼如下:

1、創建一個環繞通知:

package cn.liuw.aspect;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import java.util.Random;

/**
 * 用於完成計算方法執行時長的環繞通知
 **/
public class TimeSpanAdvice implements MethodInterceptor{
    @Override
    public Object invoke(MethodInvocation method) throws Throwable {
        //被織入的橫切內容,開始時間before
        long start = System.currentTimeMillis();
        //延遲
        lazy();
        //方法調用
        Object result = method.proceed();
        //被織入的橫切內容,結束時間
        long span = System.currentTimeMillis() - start;
        System.out.println("總共用時:"+span);
        return result;
    }

    //延遲
    public void lazy(){
        try {
            int n = new Random().nextInt(500);
            Thread.sleep(n);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

2、封裝動態代理類

package cn.liuw.aspect;
import org.springframework.aop.framework.ProxyFactory;

/**
 * 動態代理類
 **/
public class DynamicProxy {
    public static Object getProxy(Object obj){
        //實例化Spring代理工廠
        ProxyFactory factory = new ProxyFactory();
        //設置被代理對象
        factory.setTarget(obj);
        //添加通知,橫切邏輯
        factory.addAdvice(new TimeSpanAdvice());
        return factory.getProxy();
    }
}

3、測試運行

import cn.liuw.aspect.*;
import cn.liuw.aspect.Math;
import org.junit.Test;
import org.springframework.aop.framework.ProxyFactory;

public class Test05 {
    
    @Test
    public void test02(){
        //實例化Spring代理工廠
        Math math = (Math) DynamicProxy.getProxy(new Math());
        int n1 = 100,n2 = 5;
        math.add(n1,n2);
        math.sub(n1,n2);
        math.mut(n1,n2);
        math.div(n1,n2);
    }
}

4、運行結果

100+5=105
總共用時:165
100-5=95
總共用時:86
100X5=500
總共用時:49
100/5=20
總共用時:185

Process finished with exit code 0

封裝動態代理類:

package cn.liuw.aop;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.aop.framework.ProxyFactory;

public class SpringProxy<T> implements MethodInterceptor {

    /**獲得代理後的對象*/
    public T getProxyObject(Object target){
        //代理工廠
        ProxyFactory proxy=new ProxyFactory();
        //添加被代理的對象
        proxy.setTarget(target);
        //添加環繞通知
        proxy.addAdvice(this);
        //獲得代理後的對象
        return (T) proxy.getProxy();
    }

    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        before();
        //調用方法獲得結果
        Object result=methodInvocation.proceed();
        after(result);
        return result;
    }

    public void before(){
        System.out.println("調用方法前");
    }
    public void after(Object result){
        System.out.println("調用方法後"+result);
    }
}

六、使用XML配置Spring AOP切面

6.1、添加引用

需要引用一個新的jar包:aspectjweaver,該包是AspectJ的組成部分。可以去http://search.maven.org搜索後下載或直接在maven項目中添加依賴。

<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjweaver</artifactId>
  <version>1.8.9</version>
</dependency>

6.2、定義通知

該通知不再需要實現任何接口或繼承抽象類,一個普通的bean即可,方法可以帶一個JoinPoint連接點參數,用於獲得連接點信息,如方法名,參數,代理對象等。

package cn.liuw.aspect;

import org.aspectj.lang.JoinPoint;

/**
 * @ClassName Advices
 * @Description 通知
 **/
public class Advices {
    //前置通知
    public void before(JoinPoint joinPoint){
        System.out.println("-----before-----");
        System.out.println("方法名:"+joinPoint+" ,參數:"+joinPoint.getArgs().length+" ,代理對象:"+joinPoint.getTarget());
    }
    //後置通知
    public void after(JoinPoint joinPoint){
        System.out.println("-----after-----");
    }
}

通知的類型有多種,有些參數會不一樣,特別是環繞通知,通知類型如下:

//前置通知
public void beforeMethod(JoinPoint joinPoint)

//後置通知
public void afterMethod(JoinPoint joinPoint)

//返回值通知
public void afterReturning(JoinPoint joinPoint, Object result)

//拋出異常通知
//在方法出現異常時會執行的代碼可以訪問到異常對象,可以指定在出現特定異常時在執行通知代碼
public void afterThrowing(JoinPoint joinPoint, Exception ex)

//環繞通知
//環繞通知需要攜帶ProceedingJoinPoint類型的參數
//環繞通知類似於動態代理的全過程:ProceedingJoinPoint類型的參數可以決定是否執行目標方法。
//而且環繞通知必須有返回值,返回值即爲目標方法的返回值
public Object aroundMethod(ProceedingJoinPoint pjd)

6.3、配置IOC容器依賴的XML文件applicationContext.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 id="advice" class="cn.liuw.aspect.Advices" />
    <bean id="math" class="cn.liuw.aspect.Math" />
    <!-- AOP -->
    <aop:config proxy-target-class="true">
        <!-- 切面配置 -->
        <!-- ref表示通知對象的引用 -->
        <aop:aspect ref="advice">
            <!-- 配置切入點 -->
            <aop:pointcut id="pointcut" expression="execution(* cn.liuw.aspect.Math.*(..))" />
            <!-- 聲明通知,method指定通知類型,pointcut指定切點,就是該通知應該注入哪些方法中 -->
            <aop:before method="before" pointcut-ref="pointcut" />
            <aop:after method="after" pointcut-ref="pointcut" />
        </aop:aspect>
    </aop:config>
</beans>
  • <bean id="advice" class="com.zhangguo.Spring041.aop08.Advices" />表示通知bean,也就是橫切邏輯bean。
  • <aop:config proxy-target-class="true">用於AOP配置,proxy-target-class屬性表示被代理的類是否爲一個沒有實現接口的類,Spring會依據實現了接口則使用JDK內置的動態代理,如果未實現接口則使用cblib;在Bean配置文件中,所有的Spring AOP配置都必須定義在<aop:config>元素內部。
  • 對於每個切面而言,都要創建一個<aop:aspect>元素來爲具體的切面實現引用後端Bean實例。因此,切面Bean必須有一個標識符,供<aop:aspect>元素引用。
    • aop:aspect表示切面配置, ref表示通知對象的引用;aop:pointcut是配置切入點,就是橫切邏輯將注入的精確位置,那些包,類,方法需要攔截注入橫切邏輯。
    • aop:before用於聲明通知,method指定通知類型,pointcut指定切點,就是該通知應該注入那些方法中。在aop Schema中,每種通知類型都對應一個特定地XML元素。通知元素需要pointcut-ref屬性來引用切入點,或者用pointcut屬性直接嵌入切入點表達式。

method屬性指定切面類中通知方法的名稱。有如下幾種:

<!-- 前置通知 -->
<aop:before method="before" pointcut-ref="pointcut"/>
<!-- 後置通知 -->
<aop:after method="after" pointcut-ref="pointcut"/>
<!--環繞通知 -->
<aop:around method="around" pointcut="execution(* cn.liuw.aspect.Math.s*(..))"/>
<!--異常通知 -->
<aop:after-throwing method="afterThrowing" pointcut="execution(* cn.liuw.aspect.Math.d*(..))"  throwing="exp"/>
<!-- 返回值通知 -->
<aop:after-returning method="afterReturning" pointcut="execution(* cn.liuw.aspect.Math.m*(..))" returning="result"/>

6.3.1 表達式類型

標準的Aspectj Aop的pointcut的表達式類型是很豐富的,但是Spring Aop只支持其中的9種,外加Spring Aop自己擴充的一種一共是10種類型的表達式,分別如下。

  • execution:一般用於指定方法的執行,用的最多。
  • within:指定某些類型的全部方法執行,也可用來指定一個包。
  • this:Spring Aop是基於代理的,生成的bean也是一個代理對象,this就是這個代理對象,當這個對象可以轉換爲指定的類型時,對應的切入點就是它了,Spring Aop將生效。
  • target:當被代理的對象可以轉換爲指定的類型時,對應的切入點就是它了,Spring Aop將生效。
  • args:當執行的方法的參數是指定類型時生效。
  • @target:當代理的目標對象上擁有指定的註解時生效。
  • @args:當執行的方法參數類型上擁有指定的註解時生效。
  • @within:與@target類似,看官方文檔和網上的說法都是@within只需要目標對象的類或者父類上有指定的註解,則@within會生效,而@target則是必須是目標對象的類上有指定的註解。而根據筆者的測試這兩者都是隻要目標類或父類上有指定的註解即可。
  • @annotation:當執行的方法上擁有指定的註解時生效。
  • bean:當調用的方法是指定的bean的方法時生效。

6.3.2 使用示例

(1) execution

execution是使用的最多的一種Pointcut表達式,表示某個方法的執行,其標準語法如下。

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)

modifiers-pattern表示方法的訪問類型,public等;

ret-type-pattern表示方法的返回值類型,如String表示返回類型是String,“*”表示所有的返回類型;

declaring-type-pattern表示方法的聲明類,如“com.elim..*”表示com.elim包及其子包下面的所有類型;

name-pattern表示方法的名稱,如“add*”表示所有以add開頭的方法名;

param-pattern表示方法參數的類型,name-pattern(param-pattern)其實是一起的表示的方法集對應的參數類型,如“add()”表示不帶參數的add方法,“add(*)”表示帶一個任意類型的參數的add方法,“add(*,String)”則表示帶兩個參數,且第二個參數是String類型的add方法;

throws-pattern表示異常類型;其中以問號結束的部分都是可以省略的。

  • 1、“execution(* add())”匹配所有的不帶參數的add()方法。
  • 2、“execution(public * com..*.add*(..))”匹配所有com包及其子包下所有類的以add開頭的所有public方法。
  • 3、“execution(* *(..) throws Exception)”匹配所有拋出Exception的方法。

(2) within

within是用來指定類型的,指定類型中的所有方法將被攔截。

  • 1、“within(com.spring.aop.service.UserServiceImpl)”匹配UserServiceImpl類對應對象的所有方法外部調用,而且這個對象只能是UserServiceImpl類型,不能是其子類型。
  • 2、“within(com.elim..*)”匹配com.elim包及其子包下面所有的類的所有方法的外部調用。

(3) this

Spring Aop是基於代理的,this就表示代理對象。this類型的Pointcut表達式的語法是this(type),當生成的代理對象可以轉換爲type指定的類型時則表示匹配。基於JDK接口的代理和基於CGLIB的代理生成的代理對象是不一樣的。

  • 1、“this(com.spring.aop.service.IUserService)”匹配生成的代理對象是IUserService類型的所有方法的外部調用。

(4) target

Spring Aop是基於代理的,target則表示被代理的目標對象。當被代理的目標對象可以被轉換爲指定的類型時則表示匹配。

  • 1、“target(com.spring.aop.service.IUserService)”則匹配所有被代理的目標對象能夠轉換爲IUserService類型的所有方法的外部調用。

(5) args

args用來匹配方法參數的。

  • 1、“args()”匹配任何不帶參數的方法。
  • 2、“args(java.lang.String)”匹配任何只帶一個參數,而且這個參數的類型是String的方法。
  • 3、“args(..)”帶任意參數的方法。
  • 4、“args(java.lang.String,..)”匹配帶任意個參數,但是第一個參數的類型是String的方法。
  • 5、“args(..,java.lang.String)”匹配帶任意個參數,但是最後一個參數的類型是String的方法。

(6) @target

@target匹配當被代理的目標對象對應的類型及其父類型上擁有指定的註解時。

  • 1、“@target(com.spring.support.MyAnnotation)”匹配被代理的目標對象對應的類型上擁有MyAnnotation註解時。

(7) @args

@args匹配被調用的方法上含有參數,且對應的參數類型上擁有指定的註解的情況。

  • 1、“@args(com.spring.support.MyAnnotation)”匹配方法參數類型上擁有MyAnnotation註解的方法調用。如我們有一個方法add(MyParam param)接收一個MyParam類型的參數,而MyParam這個類是擁有註解MyAnnotation的,則它可以被Pointcut表達式“@args(com.elim.spring.support.MyAnnotation)”匹配上。

(8) @within

@within用於匹配被代理的目標對象對應的類型或其父類型擁有指定的註解的情況,但只有在調用擁有指定註解的類上的方法時才匹配。

  • 1、“@within(com.spring.support.MyAnnotation)”匹配被調用的方法聲明的類上擁有MyAnnotation註解的情況。比如有一個ClassA上使用了註解MyAnnotation標註,並且定義了一個方法a(),那麼在調用ClassA.a()方法時將匹配該Pointcut;如果有一個ClassB上沒有MyAnnotation註解,但是它繼承自ClassA,同時它上面定義了一個方法b(),那麼在調用ClassB().b()方法時不會匹配該Pointcut,但是在調用ClassB().a()時將匹配該方法調用,因爲a()是定義在父類型ClassA上的,且ClassA上使用了MyAnnotation註解。但是如果子類ClassB覆寫了父類ClassA的a()方法,則調用ClassB.a()方法時也不匹配該Pointcut。

(9) @annotation

@annotation用於匹配方法上擁有指定註解的情況。

  • 1、“@annotation(com.spring.support.MyAnnotation)”匹配所有的方法上擁有MyAnnotation註解的方法外部調用。

(10) bean

bean用於匹配當調用的是指定的Spring的某個bean的方法時。

  • 1、“bean(abc)”匹配Spring Bean容器中id或name爲abc的bean的方法調用。
  • 2、“bean(user*)”匹配所有id或name爲以user開頭的bean的方法調用。

6.3.4 表達式組合

表達式的組合其實就是對應的表達式的邏輯運算,與、或、非。可以通過它們把多個表達式組合在一起。

  • 1、“bean(userService) && args()”匹配id或name爲userService的bean的所有無參方法。
  • 2、“bean(userService) || @annotation(MyAnnotation)”匹配id或name爲userService的bean的方法調用,或者是方法上使用了MyAnnotation註解的方法調用。
  • 3、“bean(userService) && !args()”匹配id或name爲userService的bean的所有有參方法調用。

七、  基於Aspectj註解的Pointcut表達式應用

在使用基於Aspectj註解的Spring Aop時,我們可以把通過@Pointcut註解定義Pointcut,指定其表達式,然後在需要使用Pointcut表達式的時候直接指定Pointcut。

1、AOP註解方式使用

配置applicationContext.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">

    <!-- 自動掃描所有註解 -->
    <context:component-scan base-package="cn.liuw.*" />
    
    <!-- 開啓aop註解自動掃描 -->
    <aop:aspectj-autoproxy />
</beans>

使用註解定義切面類:

package cn.liuw.aspect;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class MyAspect {
    @Pointcut("execution(* cn.liuw.aspect.Math.add(..))")
    private void beforeAdd(){}

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

測試類:

@Test
public void test03(){
	//容器
	ApplicationContext ctx=new ClassPathXmlApplicationContext("applicationContext.xml");
	//從代理工廠中獲得代理對象
	Math math=(Math)ctx.getBean("math");
	int n1 = 100,n2 = 5;
	math.add(n1,n2);
	math.sub(n1,n2);
	math.mut(n1,n2);
	math.div(n1,n2);
}

運行結果:

--- before ---
100+5=105
100-5=95
100X5=500
100/5=20

2、零配置實現Spring IoC與AOP

package cn.liuw.aop;

import cn.liuw.po.User;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

//用於表示當前類爲容器的配置類,類似<beans/>
@Configuration
//掃描的範圍,相當於xml配置的結點<context:component-scan/>
@ComponentScan(basePackages="cn.liuw")
//自動代理,相當於<aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class ApplicationCfg {
    //在配置中聲明一個bean,相當於<bean id="getUser" class="cn.liuw.po.User"/>
    @Bean
    public User getUser(){
        return new User();
    }
}

本文參考自:https://www.cnblogs.com/best/p/5679656.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章