Spring 08: AOP面向切面編程 + 手寫AOP框架

核心解讀

  • AOP:Aspect Oriented Programming,面向切面編程
  • 核心1:將公共的,通用的,重複的代碼單獨開發,在需要時反織回去
  • 核心2:面向接口編程,即設置接口類型的變量,傳入接口類型的參數,返回接口類型的返回值,調用接口類型的變量自身的實現方法
  • 圖示

image

圖示分析:例如,在開發中普遍需要使用到日誌輸出功能,會將日誌輸出功能大量耦合到項目的不同位置,如上圖左側所示。
而日誌輸出功能與其實與項目本身的核心業務邏輯無關,我們只是為了不時的查看項目的運行狀態。
則可以將日誌功能單獨提出去開發,在需要的地方將日誌輸出功能(所謂:日誌功能切面)反織回去即可,如上圖右側所示。

手寫AOP框架

  • 下面將手寫5個版本的AOP框架,在版本的不斷優化中,逐步理解AOP面向切面編程的核心,最後一個版本最接近Spring中AOP的原生實現
  • 業務功能和切面功能用簡單的輸出語句來模擬,主要是為了簡潔直觀的演示AOP核心思想
  • 手寫的AOP框架的業務背景:圖書購買業務

項目結構

  • 5個AOP版本分別放在proxy01 ~ proxy05這5個包下,下圖左側為項目結構,右側為各版本用到的接口和實現類

image

AOP版本1

實體類

  • 整個版本1,只是一個BookService實體類,業務功能和切面功能嚴重耦合
package com.example.proxy01;

/**
 * 圖書購買功能和事務切面功能耦合在一個類中
 */
public class BookService {
    public void buy(){
        try{
            System.out.println("開啟事務....");
            System.out.println("圖書購買業務....");
            System.out.println("提交事務....");
        }catch (Exception e){
            System.out.println("回滾事務....");
        }
    }
}

測試

  • 調用BookService實體對象中的buy()方法即可

AOP版本2

優化原理

  • 通過子類代理來實現將業務功能和切面功能初步拆分解耦

實體類

  • 實體類BookService及其子類SubBookService

  • BookService實體類

package com.example.proxy02;

/**
 * 圖書購買功能
 */
public class BookService {
    public void buy(){
        System.out.println("圖書購買功能....");
    }
}
  • SubBookService子類
package com.example.proxy02;

/**
 * 子類代理:將圖書購買功能和事務切面劃分到不同類中
 */
public class SubBookService extends BookService{
    @Override
    public void buy() {
        try{
            System.out.println("開啟事務事務....");
            super.buy();
            System.out.println("提交事務....");
        }catch (Exception e){
            System.out.println("回滾事務....");
        }
    }
}

測試

  • 調用SubBookService實體類對象中的buy()方法即可

AOP版本3

優化原理

  • 通過靜態代理,可以進行受代理對象的靈活切換

接口

  • Service接口
package com.example.proxy03;

/**
 * 靜態代理接口
 */
public interface Service {
    //定義業務功能
    void buy();
}

實現類

  • BookServiceImpl實現類
package com.example.proxy03;

/**
 * 目標對象
 */
public class BookServiceImpl implements Service{
    @Override
    public void buy() {
        System.out.println("圖書購買業務....");
    }
}
  • ProductServiceImpl實現類
package com.example.proxy03;

/**
 * 另外一種業務功能的目標對象
 */
public class ProductServiceImpl implements Service{
    @Override
    public void buy() {
        System.out.println("產品購買業務....");
    }
}
  • 靜態代理對象
package com.example.proxy03;

/**
 * 靜態代理對象
 */
public class Agent implements Service{
    //接口類型的參數
    Service target;

    //傳入接口類型的參數,靈活調用多種Service接口的實現類
    public Agent(Service target){
        this.target = target;
    }

    @Override
    public void buy() {
        try{
            System.out.println("開啟事務....");
            target.buy();
            System.out.println("提交事務....");
        }catch (Exception e){
            System.out.println("關閉事務....");
        }
    }
}

測試

  • 面向接口編程,可靈活代理多種Service接口的實現類,靈活切換受代理對象
package com.example.test;

import com.example.proxy03.Agent;
import com.example.proxy03.ProductServiceImpl;
import com.example.proxy03.Service;
import org.junit.Test;

public class TestProxy03 {
    @Test
    public void testProxy03(){
        //可以靈活切換受代理對象,因為接口類型的參數都能接住
        //Service agent = new Agent(new BookServiceImpl());
        Service agent = new Agent(new ProductServiceImpl());
        agent.buy();
    }
}

AOP版本4

優化原理

  • AOP版本3中,雖然受代理對象可以靈活切換,但是不同的受代理對象被綁定到相同的切面功能,切面功能無法靈活切換

  • 可以將上述切面功能上升到接口層次,針對不同切面功能有不同實現類

  • 核心:就像Agent代理對象持有Service接口類型的變量一樣,若持有切面接口類型的變量,則可以接收不同切面接口的實現類,實現不同切面功能的靈活切換

  • 考慮到切面功能出現在業務功能的前後關係,以及異常處理等情況,可以根據切面出現的時機定義切面接口中的方法

  • 推導出需要定義切面接口以及如何定義接口中方法的思路圖示

image

接口

  • 業務接口:Service接口
package com.example.proxy04;

/**
 * 靜態代理接口
 */
public interface Service {
    //定義業務功能
    void buy();
}
  • 切面接口:Aop接口
package com.example.proxy04;

/**
 * 自定義Aop,切面接口
 */
public interface Aop {
    default void before(){}	//default關鍵字,jdk8的新特性,可以提供空實現,不強迫接口實現類實現所有接口中的方法
    default void after(){}      //實現類需要實現哪個方法就實現哪個方法
    default void exception(){}
}

實現類

  • 業務功能實現類:BookServiceImpl和ProductServiceImpl
package com.example.proxy04;

/**
 * 目標對象
 */
public class BookServiceImpl implements Service {
    @Override
    public void buy() {
        System.out.println("圖書購買業務....");
    }
}

package com.example.proxy04;

public class ProductServiceImpl implements Service {
    @Override
    public void buy() {
        System.out.println("產品生產業務....");
    }
}
  • 切面功能實現類:TransAopImpl和LogAopImpl
package com.example.proxy04;

public class TransAopImpl implements Aop{
    @Override
    public void before() {
        System.out.println("開啟事務....");
    }

    @Override
    public void after() {
        System.out.println("提交事務....");
    }

    @Override
    public void exception() {
        System.out.println("回滾事務....");
    }
}
package com.example.proxy04;

//切面接口中定義方法時使用了default,不必實現所有接口方法,按需實現方法即可
public class LogAopImpl implements Aop{
    @Override
    public void before() {
        System.out.println("前置日誌輸出....");
    }
}

測試

  • 可以實現業務功能和切面功能的靈活組合
  • 而且就像下面第2個測試一樣,因為代理對象也是Service的一個實現類,所以代理對象還可以再次被代理,一個業務功能被多個切面包圍,實現多切面
  • 此時的版本已經很靈活,也已經揭示出了AOP面向切面編程的核心
package com.example.test;

import com.example.proxy04.*;
import org.junit.Test;

public class TestProxy04 {

    //測試:單個業務功能 + 單個切面功能
    @Test
    public void testProxy04(){
        //分別傳入要完成的業務功能和要切入的功能,可以靈活組合,這裡就可以有業務功能和切面功能的4種組合:2 x 2
        //Service agent = new Agent(new ProductServiceImpl(), new LogAopImpl());
        //Service agent = new Agent(new ProductServiceImpl(), new TransAopImpl());
        //Service agent = new Agent(new BookServiceImpl(), new LogAopImpl());
        Service agent = new Agent(new BookServiceImpl(), new TransAopImpl());
        agent.buy();
    }

    //測試:單個業務功能 + 多個切面功能(本例為:日誌切面 + 事務切面)
    @Test
    public void testManyProxies(){
        Service agent = new Agent(new ProductServiceImpl(), new LogAopImpl());
        Service agent2 = new Agent(agent, new TransAopImpl());
        agent2.buy();
    }
}

AOP版本5

優化原理

  • 靜態代理可以做到受代理對象的靈活切換,但是不可以做到代理功能的靈活切換,就像我們用動態代理優化靜態代理一樣,還可以用jdk動態代理繼續優化上述AOP版本4
  • 在Spring原生的AOP框架中,底層就是使用的jdk動態代理,AOP版本5最接近Spring原生AOP框架

實體類

  • AOP版本5中除了用ProxyFactory代理工廠來動態獲取代理對象外(不再寫AOP版本4中的Agent類,4版本是靜態的,現在不用寫了),其他接口和實現類與AOP版本4完全一致,不再贅述

  • 新增ProxyFactory類,代替AOP版本4中的Agent類

  • 參數比較多,看起來有些亂(包涵 包涵),若對jdk動態代理不是很熟悉,可以參考mybatis博客集(mybatis 01 對jdk動態代理有詳細討論)

package com.example.proxy05;

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

/**
 * 代理工廠,獲取動態代理對象
 */
public class ProxyFactory {
    
    //靜態方法獲取jdk動態代理對象:傳入業務功能對象 + 切面功能對象
    public static Object getProxy(Service target, Aop aop){
        
        //該方法有三個參數,第三個參數是一個匿名內部類
        return Proxy.newProxyInstance(
                //參數1               
                target.getClass().getClassLoader(),
                //參數2
                target.getClass().getInterfaces(),
            
                //參數3:匿名內部類重寫的方法又有三個參數
                //其中method用來反射調用外部調用的那個方法,args是調用目標方法時要傳的參數
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object obj, Method method, Object[] args) throws Throwable {
                        //用來存放目標對象被調用的方法的返回值
                        Object res = null;
                        try{
                            //切面功能
                            aop.before();
                            //業務功能,根據外部調用的功能,動態代理目標對象被調用的方法
                            res = method.invoke(target, args);
                            //切面功能
                            aop.after();
                        }catch (Exception e){
                            //切面功能
                            aop.exception();
                        }
                        //返回目標對象被調用的方法的返回值給外部調用者
                        return res;
                    }
                }
        );
    }
}

測試

package com.example.test;

import com.example.proxy05.BookServiceImpl;
import com.example.proxy05.ProxyFactory;
import com.example.proxy05.Service;
import com.example.proxy05.TransAopImpl;
import org.junit.Test;

public class TestProxy05 {
    //測試:AOP版本5
    @Test
    public void testProxy05(){
        //獲取動態代理對象,傳入業務功能對象和切面功能對象,這裡傳入的業務對象和切面對象可以有多種組合
        Service agent = (Service) ProxyFactory.getProxy(new BookServiceImpl(), new TransAopImpl());
        //完成業務功能和切面功能的組合
        agent.buy();
    }
}

測試輸出

開啟事務....
圖書購買業務....
提交事務....

Process finished with exit code 0

擴展測試

  • 為了體現動態代理的優點,並測試有參數和有返回值的方法都可被代理,為Service接口擴展功能:order(預定圖書的功能)

接口

  • Service接口新增order方法
package com.example.proxy05;

/**
 * 靜態代理接口
 */
public interface Service {
    //定義業務功能
    void buy();
    
    //新擴展一個預定功能
    default String order(int orderNums){return null;}
    //default,不強制實現類都實現該方法,按需實現
}

實現類

  • 讓BookServiceImpl實現該新增的方法
package com.example.proxy05;

/**
 * 目標對象
 */
public class BookServiceImpl implements Service {
    @Override
    public String order(int orderNums) {
        System.out.println("新預定圖書: " + orderNums + " 冊");
        return "預定成功";
    }
    @Override
    public void buy() {
        System.out.println("圖書購買業務....");
    }


}

測試

package com.example.test;

import com.example.proxy05.BookServiceImpl;
import com.example.proxy05.ProxyFactory;
import com.example.proxy05.Service;
import com.example.proxy05.TransAopImpl;
import org.junit.Test;

public class TestProxy05 {
    @Test
    public void testProxy05(){
        //獲取動態代理對象,傳入業務功能對象和切面功能對象
        Service agent = (Service) ProxyFactory.getProxy(new BookServiceImpl(), new TransAopImpl());
        //完成業務功能和切面功能的組合
        String res = agent.order(10);
        System.out.println("返回結果: " + res);
    }
}

測試結果

開啟事務....
新預定圖書: 10 冊
提交事務....
返回結果: 預定成功

Process finished with exit code 0

小結

  • 在AOP版本3優化了業務功能(靜態代理)
  • 在AOP版本4優化了切面功能(AOP面向切面編程)
  • 在AOP版本5優化了代理功能(jdk動態代理)
  • 此時手寫的AOP版本5可以做到被代理對象的靈活切換,代理功能的靈活切換,業務功能和切面功能的靈活組合
  • AOP版本5最接近Spring中AOP的原生實現原理