自己動手實現的 Spring IOC 和 AOP - 上篇
1. 背景
我在大四實習的時候開始接觸 J2EE 方面的開發工作,也是在同時期接觸並學習 Spring 框架,到現在也有快有兩年的時間了。不過之前沒有仿寫過 Spring IOC 和 AOP,只是巨集觀上對 Spring IOC 和 AOP 原理有一定的認識。所以為了更進一步理解 Spring IOC 和 AOP 原理。在工作之餘,參考了一些資料和程式碼,動手實現了一個簡單的 IOC 和 AOP,並實現瞭如下功能:
- 根據 xml 配置檔案載入相關 bean
- 對 BeanPostProcessor 型別的 bean 提供支援
- 對 BeanFactoryAware 型別的 bean 提供支援
- 實現了基於 JDK 動態代理的 AOP
- 整合了 IOC 和 AOP,使得二者可很好的協同工作
在實現自己的 IOC 和 AOP 前,我的想法比較簡單,就是實現一個非常簡單的 IOC 和 AOP,哪怕是幾十行程式碼實現的都行。後來實現後,感覺還很有意思的。不過那個實現太過於簡單,和 Spring IOC,AOP 相去甚遠。後來想了一下,不能僅滿足那個簡單的實現,於是就有了這個仿寫專案。相對來說仿寫的程式碼要複雜了一些,功能也多了一點,看起來也有點樣子的。儘管仿寫出的專案仍然是玩具級,不過寫仿寫的過程中,還是學到了一些東西。總體上來說,收穫還是很大的。在接下來文章中,我也將從易到難,實現不同版本的 IOC 和 AOP。好了,不多說了,開始幹活。
2. 簡單的 IOC 和 AOP 實現
2.1 簡單的 IOC
先從簡單的 IOC 容器實現開始,最簡單的 IOC 容器只需4步即可實現,如下:
- 載入 xml 配置檔案,遍歷其中的標籤
- 獲取標籤中的 id 和 class 屬性,載入 class 屬性對應的類,並建立 bean
- 遍歷標籤中的標籤,獲取屬性值,並將屬性值填充到 bean 中
- 將 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步即可完成:
- 定義一個包含切面邏輯的物件,這裡假設叫 logMethodInvocation
- 定義一個 Advice 物件(實現了 InvocationHandler 介面),並將上面的 logMethodInvocation 和 目標物件傳入
- 將上面的 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 原理探祕 - 容器的建立過程 |