1. 程式人生 > >自己動手實現的 Spring IOC 和 AOP - 上篇

自己動手實現的 Spring IOC 和 AOP - 上篇

1. 背景

我在大四實習的時候開始接觸 J2EE 方面的開發工作,也是在同時期接觸並學習 Spring 框架,到現在也有快有兩年的時間了。不過之前沒有仿寫過 Spring IOC 和 AOP,只是巨集觀上對 Spring IOC 和 AOP 原理有一定的認識。所以為了更進一步理解 Spring IOC 和 AOP 原理。在工作之餘,參考了一些資料和程式碼,動手實現了一個簡單的 IOC 和 AOP,並實現瞭如下功能:

  1. 根據 xml 配置檔案載入相關 bean
  2. 對 BeanPostProcessor 型別的 bean 提供支援
  3. 對 BeanFactoryAware 型別的 bean 提供支援
  4. 實現了基於 JDK 動態代理的 AOP
  5. 整合了 IOC 和 AOP,使得二者可很好的協同工作

在實現自己的 IOC 和 AOP 前,我的想法比較簡單,就是實現一個非常簡單的 IOC 和 AOP,哪怕是幾十行程式碼實現的都行。後來實現後,感覺還很有意思的。不過那個實現太過於簡單,和 Spring IOC,AOP 相去甚遠。後來想了一下,不能僅滿足那個簡單的實現,於是就有了這個仿寫專案。相對來說仿寫的程式碼要複雜了一些,功能也多了一點,看起來也有點樣子的。儘管仿寫出的專案仍然是玩具級,不過寫仿寫的過程中,還是學到了一些東西。總體上來說,收穫還是很大的。在接下來文章中,我也將從易到難,實現不同版本的 IOC 和 AOP。好了,不多說了,開始幹活。

 2. 簡單的 IOC 和 AOP 實現

 2.1 簡單的 IOC

先從簡單的 IOC 容器實現開始,最簡單的 IOC 容器只需4步即可實現,如下:

  1. 載入 xml 配置檔案,遍歷其中的標籤
  2. 獲取標籤中的 id 和 class 屬性,載入 class 屬性對應的類,並建立 bean
  3. 遍歷標籤中的標籤,獲取屬性值,並將屬性值填充到 bean 中
  4. 將 bean 註冊到 bean 容器中

如上所示,僅需4步即可,是不是覺得很簡單。好了,Talk is cheap, Show me the code. 接下來要上程式碼了。不過客官別急,上程式碼前,容我對程式碼結構做一下簡單介紹:

1
2
3
4
5
SimpleIOC     // IOC 的實現類,實現了上面所說的4個步驟
SimpleIOCTest    // IOC 的測試類
Car           // IOC 測試使用的 bean
Wheel         // 同上 
ioc.xml       // bean 配置檔案

容器實現類 SimpleIOC 的程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public class SimpleIOC {

    private Map<String, Object> beanMap = new HashMap<>();

    public SimpleIOC(String location) throws Exception {
        loadBeans(location);
    }

    public Object getBean(String name) {
        Object bean = beanMap.get(name);
        if (bean == null) {
            throw new IllegalArgumentException("there is no bean with name " + name);
        }

        return bean;
    }

    private void loadBeans(String location) throws Exception {
        // 載入 xml 配置檔案
        InputStream inputStream = new FileInputStream(location);
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder docBuilder = factory.newDocumentBuilder();
        Document doc = docBuilder.parse(inputStream);
        Element root = doc.getDocumentElement();
        NodeList nodes = root.getChildNodes();

        // 遍歷 <bean> 標籤
        for (int i = 0; i < nodes.getLength(); i++) {
            Node node = nodes.item(i);
            if (node instanceof Element) {
                Element ele = (Element) node;
                String id = ele.getAttribute("id");
                String className = ele.getAttribute("class");
                
                // 載入 beanClass
                Class beanClass = null;
                try {
                    beanClass = Class.forName(className);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                    return;
                }

                   // 建立 bean
                Object bean = beanClass.newInstance();

                // 遍歷 <property> 標籤
                NodeList propertyNodes = ele.getElementsByTagName("property");
                for (int j = 0; j < propertyNodes.getLength(); j++) {
                    Node propertyNode = propertyNodes.item(j);
                    if (propertyNode instanceof Element) {
                        Element propertyElement = (Element) propertyNode;
                        String name = propertyElement.getAttribute("name");
                        String value = propertyElement.getAttribute("value");

                            // 利用反射將 bean 相關欄位訪問許可權設為可訪問
                        Field declaredField = bean.getClass().getDeclaredField(name);
                        declaredField.setAccessible(true);

                        if (value != null && value.length() > 0) {
                            // 將屬性值填充到相關欄位中
                            declaredField.set(bean, value);
                        } else {
                            String ref = propertyElement.getAttribute("ref");
                            if (ref == null || ref.length() == 0) {
                                throw new IllegalArgumentException("ref config error");
                            }
                            
                            // 將引用填充到相關欄位中
                            declaredField.set(bean, getBean(ref));
                        }

                        // 將 bean 註冊到 bean 容器中
                        registerBean(id, bean);
                    }
                }
            }
        }
    }

    private void registerBean(String id, Object bean) {
        beanMap.put(id, bean);
    }
}

容器測試使用的 bean 程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Car {
    private String name;
    private String length;
    private String width;
    private String height;
    private Wheel wheel;
    
    // 省略其他不重要程式碼
}

public class Wheel {
    private String brand;
    private String specification ;
    
    // 省略其他不重要程式碼
}

bean 配置檔案 ioc.xml 內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<beans>
    <bean id="wheel" class="com.titizz.simulation.toyspring.Wheel">
        <property name="brand" value="Michelin" />
        <property name="specification" value="265/60 R18" />
    </bean>

    <bean id="car" class="com.titizz.simulation.toyspring.Car">
        <property name="name" value="Mercedes Benz G 500"/>
        <property name="length" value="4717mm"/>
        <property name="width" value="1855mm"/>
        <property name="height" value="1949mm"/>
        <property name="wheel" ref="wheel"/>
    </bean>
</beans>

IOC 測試類 SimpleIOCTest:

1
2
3
4
5
6
7
8
9
10
11
public class SimpleIOCTest {
    @Test
    public void getBean() throws Exception {
        String location = SimpleIOC.class.getClassLoader().getResource("spring-test.xml").getFile();
        SimpleIOC bf = new SimpleIOC(location);
        Wheel wheel = (Wheel) bf.getBean("wheel");
        System.out.println(wheel);
        Car car = (Car) bf.getBean("car");
        System.out.println(car);
    }
}

測試結果:

以上是簡單 IOC 實現的全部內容,難度不大,程式碼也不難看懂,這裡不再多說了。下面說說簡單 AOP 的實現。

 2.2 簡單的 AOP 實現

AOP 的實現是基於代理模式的,這一點相信大家應該都知道。代理模式是AOP實現的基礎,代理模式不難理解,這裡就不花篇幅介紹了。在介紹 AOP 的實現步驟之前,先引入 Spring AOP 中的一些概念,接下來我們會用到這些概念。

通知(Advice)

1
2
3
4
5
6
7
通知定義了要織入目標物件的邏輯,以及執行時機。
Spring 中對應了 5 種不同型別的通知:
· 前置通知(Before):在目標方法執行前,執行通知
· 後置通知(After):在目標方法執行後,執行通知,此時不關係目標方法返回的結果是什麼
· 返回通知(After-returning):在目標方法執行後,執行通知
· 異常通知(After-throwing):在目標方法丟擲異常後執行通知
· 環繞通知(Around): 目標方法被通知包裹,通知在目標方法執行前和執行後都被會呼叫

切點(Pointcut)

1
2
如果說通知定義了在何時執行通知,那麼切點就定義了在何處執行通知。所以切點的作用就是
通過匹配規則查詢合適的連線點(Joinpoint),AOP 會在這些連線點上織入通知。

切面(Aspect)

1
切面包含了通知和切點,通知和切點共同定義了切面是什麼,在何時,何處執行切面邏輯。

說完概念,接下來我們來說說簡單 AOP 實現的步驟。這裡 AOP 是基於 JDK 動態代理實現的,只需3步即可完成:

  1. 定義一個包含切面邏輯的物件,這裡假設叫 logMethodInvocation
  2. 定義一個 Advice 物件(實現了 InvocationHandler 介面),並將上面的 logMethodInvocation 和 目標物件傳入
  3. 將上面的 Adivce 物件和目標物件傳給 JDK 動態代理方法,為目標物件生成代理

上面步驟比較簡單,不過在實現過程中,還是有一些難度的,這裡要引入一些輔助接口才能實現。接下來就來介紹一下簡單 AOP 的程式碼結構:

1
2
3
4
5
6
7
MethodInvocation 介面  // 實現類包含了切面邏輯,如上面的 logMethodInvocation
Advice 介面        // 繼承了 InvocationHandler 介面
BeforeAdvice 類    // 實現了 Advice 介面,是一個前置通知
SimpleAOP 類       // 生成代理類
SimpleAOPTest      // SimpleAOP 從測試類
HelloService 介面   // 目標物件介面
HelloServiceImpl   // 目標物件

MethodInvocation 介面程式碼:

1
2
3
public interface MethodInvocation {
    void invoke();
}

Advice 介面程式碼:

1
public interface Advice extends InvocationHandler {}

BeforeAdvice 實現程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BeforeAdvice implements Advice {
    private Object bean;
    private MethodInvocation methodInvocation;

    public BeforeAdvice(Object bean, MethodInvocation methodInvocation) {
        this.bean = bean;
        this.methodInvocation = methodInvocation;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 在目標方法執行前呼叫通知
        methodInvocation.invoke();
        return method.invoke(bean, args);
    }
}

SimpleAOP 實現程式碼:

1
2
3
4
5
6
public class SimpleAOP {
    public static Object getProxy(Object bean, Advice advice) {
        return Proxy.newProxyInstance(SimpleAOP.class.getClassLoader(), 
                bean.getClass().getInterfaces(), advice);
    }
}

HelloService 介面,及其實現類程式碼:

1
2
3
4
5
6
7
8
9
10
public interface HelloService {
    void sayHelloWorld();
}

public class HelloServiceImpl implements HelloService {
    @Override
    public void sayHelloWorld() {
        System.out.println("hello world!");
    }
}

SimpleAOPTest 程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SimpleAOPTest {
    @Test
    public void getProxy() throws Exception {
        // 1. 建立一個 MethodInvocation 實現類
        MethodInvocation logTask = () -> System.out.println("log task start");
        HelloServiceImpl helloServiceImpl = new HelloServiceImpl();
        
        // 2. 建立一個 Advice
        Advice beforeAdvice = new BeforeAdvice(helloServiceImpl, logTask);
        
        // 3. 為目標物件生成代理
        HelloService helloServiceImplProxy = (HelloService) SimpleAOP.getProxy(helloServiceImpl,beforeAdvice);
        
        helloServiceImplProxy.sayHelloWorld();
    }
}

輸出結果:

以上實現了簡單的 IOC 和 AOP,不過實現的 IOC 和 AOP 還很簡單,且只能獨立執行。在下一篇文章中,我將實現一個較為複雜的 IOC 和 AOP,大家如果有興趣可以去看看。好了,本篇文章到此結束。

 附錄:Spring 原始碼分析文章列表

 Ⅰ. IOC

更新時間 標題
2018-05-30 Spring IOC 容器原始碼分析系列文章導讀
2018-06-01 Spring IOC 容器原始碼分析 - 獲取單例 bean
2018-06-04 Spring IOC 容器原始碼分析 - 建立單例 bean 的過程
2018-06-06 Spring IOC 容器原始碼分析 - 建立原始 bean 物件
2018-06-08 Spring IOC 容器原始碼分析 - 迴圈依賴的解決辦法
2018-06-11 Spring IOC 容器原始碼分析 - 填充屬性到 bean 原始物件
2018-06-11 Spring IOC 容器原始碼分析 - 餘下的初始化工作

 Ⅱ. AOP

更新時間 標題
2018-06-17 Spring AOP 原始碼分析系列文章導讀
2018-06-20 Spring AOP 原始碼分析 - 篩選合適的通知器
2018-06-20 Spring AOP 原始碼分析 - 建立代理物件
2018-06-22 Spring AOP 原始碼分析 - 攔截器鏈的執行過程

 Ⅲ. MVC

更新時間 標題
2018-06-29 Spring MVC 原理探祕 - 一個請求的旅行過程
2018-06-30 Spring MVC 原理探祕 - 容器的建立過程

from:http://www.tianxiaobo.com/2018/01/18/%E8%87%AA%E5%B7%B1%E5%8A%A8%E6%89%8B%E5%AE%9E%E7%8E%B0%E7%9A%84-Spring-IOC-%E5%92%8C-AOP-%E4%B8%8A%E7%AF%87/