Spring: IOC容器的實現

上一篇中對Spring的IOC概念進行了介紹, 本篇將經過代碼來實現一個簡易版的IOC. 在Spring中, IOC是一個容器, 主要負責對託管至Spring的Bean進行建立及保存. Spring IOC建立Bean可分爲單例和原型兩種. 因爲篇幅所限, 本篇中的簡易版IOC只實現對單例Bean的管理.java

設計思路

定位Bean

項目中的代碼成千上萬, Spring並不能準確的知道哪些Bean是須要由IOC容器建立並管理. 所以須要經過配置的方式將須要被管理的Bean告知Spring.緩存

XML配置

早期的Spring, 被管理的Bean須要XML文件中進行聲明.bash

<bean id="userService" class="com.atd681.xc.ssm.ioc.demo.UserService"></bean>
複製代碼

註解配置

因爲XML配置過於繁瑣, 可讀性較差. 爲簡化配置Spring推出了基於註解的配置. 在代碼中對須要被管理的Bean添加指定註解便可.dom

package com.atd681.xc.ssm.ioc.demo;

@Component
public class UserService {
}
複製代碼

爲了提高性能, 須要告知Spring哪些目錄下有須要被加載的Bean, Spring會掃描這些目錄並將含有註解的Bean進行管理ide

<component-scan package="com.atd681.xc.ssm.ioc.demo" />
複製代碼

解析Bean

肯定須要被管理的Bean後, 就要對Bean進行解析. 因爲有有XML和註解兩種配置方式, 所以IOC容器須要分別解析XML配置及註解配置的Bean. 主要針對如下幾項進行解析:性能

  • 類型: 後續經過反射建立Bean時使用
  • 名稱: 保存時做爲Bean的別名使用
  • 屬性: 依賴注入(下篇文字中實現)時使用

註冊Bean

將解析獲得的Bean描述信息註冊到指定容器中.測試

建立Bean

將已註冊到容器中的Bean依次實例化, 並統一保存. 根據Bean描述信息中的類型(Class)經過反射建立Bean的實例.ui

獲取Bean

對外提供獲取Bean的接口, 若是Bean不存在, 自動建立保存後返回.this

接口與組件

BeanDefinition

BEAN描述類, 用來保存BEAN的基本信息, 包括名稱, 類型, 屬性等.spa

// BEAN描述信息
public class BeanDefinition {

    // 名稱
    private String name;

    // CLASS
    private Class<?> clazz;

    // 經過名稱和CLASS實例化, 默認使用CLASS名做爲BEAN的名稱
    public BeanDefinition(String name, Class<?> clazz) {
        this.clazz = clazz;
        this.name = BeanUtil.isEmpty(name) ? BeanUtil.getName(clazz) : name;
    }

    // Getter & Setter
    // ...

}

複製代碼

BeanFactory

BEAN工廠, IOC容器的核心類. 負責統一建立及管理BEAN(包括描述信息和實例), 對外提供獲取BEAN的接口. 由IOC容器管理的BEAN的全部操做都由BeanFactory完成.

// BEAN工廠, 提供BEAN的建立及獲取
public class BeanFactory {

    // 保存全部BEAN的信息. K: BEAN名稱, V: BEAN描述信息
    private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>();

    // 保存全部BEAN的實例化對象. K: BEAN名稱, V: BEAN實例化對象
    private final Map<String, Object> beanObjectMap = new ConcurrentHashMap<String, Object>();

    // 註冊BEAN
    public void registerBean(BeanDefinition bean) {
        beanDefinitionMap.put(bean.getName(), bean);
    }

    // 獲取全部已註冊BEAN的名稱
    public Set<String> getBeanNames() {
        return this.beanDefinitionMap.keySet();
    }

    // 根據名稱獲取BEAN的類型
    public Class<?> getBeanType(String name) {
        return this.beanDefinitionMap.get(name).getClazz();
    }

    // 根據名稱獲取BEAN的實例
    @SuppressWarnings("unchecked")
    public <T> T getBean(String name) throws Exception {
        return null;
    }

    // 實例化BEAN
    public void instanceBean() throws Exception {
    }

}

複製代碼

ElementParser

配置文件節點解析器接口

// 節點解析器接口
public interface ElementParser {

    // 解析節點
    public void parse(Element ele, BeanFactory factory) throws Exception;

}
複製代碼

BeanElementParser

解析XML配置文件中的節點, 將解析到的BEAN信息封裝成BeanDefinition, 註冊至BeanFactory中.

// Bean節點解析器,解析XML配置文件中的<bean>節點
public class BeanElementParser implements ElementParser {

    // 解析<bean>節點
    @SuppressWarnings("unchecked")
    @Override
    public void parse(Element ele, BeanFactory factory) throws Exception {
        // 解析<bean>節點, 將Bean描述信息封裝
        BeanDefinition bd = null;
        // 向BEAN工廠註冊Bean
        factory.registerBean(bd);
    }

}
複製代碼

ComponentScanElementParser

解析XML配置文件中的節點, 獲取package屬性中的包目錄, 掃描目錄下的類並解析, 將須要被管理的BEAN信息封裝成BeanDefinition, 註冊至BeanFactory中.

// <component-scan>節點解析器
public class ComponentScanElementParser implements ElementParser {

    // 解析<component-scan>節點
    @Override
    public void parse(Element ele, BeanFactory factory) throws Exception {
        // 掃描package屬性中定義的目錄
        String basePackage = ele.getAttributeValue("package");
        // 解析目錄下的Bean並註冊至BEAN工廠
        BeanDefinition bd = null;
        factory.registerBean(bd);
    }

}
複製代碼

Component

若是BEAN須要被Spring管理, 在類中添加該註解. 含有該註解的類在被ComponentScanElementParser掃描後會交由IOC容器管理.

// 託管Bean聲明註解
@Documented
@Target({ ElementType.TYPE })
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface Component {
    String value() default "";
}
複製代碼

ApplicationContext

應用程序上下文, 提供IOC容器初始化入口及統一獲取BEAN的接口.

package com.atd681.xc.ssm.ioc.framework;

import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.servlet.ServletContext;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.input.SAXBuilder;

import com.atd681.xc.ssm.ioc.framework.parser.BeanElementParser;
import com.atd681.xc.ssm.ioc.framework.parser.ComponentScanElementParser;
import com.atd681.xc.ssm.ioc.framework.parser.ElementParser;

// 應用程序上下文
public class ApplicationContext {

    // 配置文件路徑
    private String configLocation;

    // BEAN工廠
    private BeanFactory beanFactory;

    // 節點解析器容器
    private Map<String, ElementParser> parserMap = new HashMap<String, ElementParser>();

    // 無參構造
    public ApplicationContext() {
    }

    // 根據配置文件路徑實例化上下文
    public ApplicationContext(String configLocation) {
        this.setConfigLocation(configLocation);
    }

    // 設置配置文件路徑
    public void setConfigLocation(String configLocation) {
        this.configLocation = configLocation;
    }

    // 初始化
    public void init() throws Exception {
        this.init(null);
    }

    // 根據Servlet上下文初始化
    public void init(ServletContext context) throws Exception {
        // 建立BEAN工廠
        // 初始化配置文件節點解析器
        // 解析配置文件中定義的BEAN
        // 通知BEAN工廠實例化已註冊的BEAN
    }

    // 獲取名稱獲取BEAN實例
    public <T> T getBean(String beanName) throws Exception {
        return this.beanFactory.getBean(beanName);
    }

    // 獲取BEAN工廠
    public BeanFactory getBeanFactory() {
        return beanFactory;
    }

}
複製代碼

流程

  1. 實例化ApplicationContext並設置配置文件路徑
  2. 調用init方法初始化IOC容器
  3. 建立BeanFactory
  4. 初始化, 節點解析器
  5. 讀取配置文件並解析配置文件中定義的BEAN
  6. 各節點解析器解析配置文件並向BeanFactory中註冊Bean
  7. 調用BeanFactory的instanceBean方法實例化已註冊的Bean
  8. IOC容器初始化完成

具體實現

IOC容器初始化

ApplicationContext做爲IOC容器的初始化入口, init方法中須要初始化基礎組件(節點解析器, BEAN工廠)並完成BEAN的註冊及實例化.

  • 初始化操做基礎流程
// 根據Servlet上下文初始化
public void init(ServletContext context) throws Exception {
    
    // 建立BEAN工廠
    createBeanFactory();
    // 初始化配置文件節點解析器
    initElementParser();
    // 解析配置文件中定義的BEAN
    parseBean();
    // 通知BEAN工廠實例化已註冊的BEAN
    this.beanFactory.instanceBean();

}
複製代碼
  • 建立BEAN工廠
// 建立BEAN工廠
private void createBeanFactory() {
    this.beanFactory = new BeanFactory();
}
複製代碼
  • 初始化配置文件節點解析器
// 初始化配置文件節點解析器, KEY爲節點的名稱
// 解析文件時根據節點的名稱就能夠找到對應的解析器
private void initElementParser() {
    parserMap.put("bean", new BeanElementParser());
    parserMap.put("component-scan", new ComponentScanElementParser());
}
複製代碼
  • 解析配置文件中定義的BEAN
// 解析配置文件中定義的BEAN
@SuppressWarnings("unchecked")
private void parseBean() throws Exception {

    // 開始加載配置文件(JDom解析XML)
    String classpath = getClass().getClassLoader().getResource("").getPath();
    Document doc = new SAXBuilder().build(new File(classpath, this.configLocation));

    // 獲取根節點(<beans>)下全部子節點並依次解析
    List<Element> elementList = doc.getRootElement().getChildren();
    for (Element ele : elementList) {

        // 節點名稱
        String eleName = ele.getName();
        // 無對應的節點解析器
        if (!this.parserMap.containsKey(eleName)) {
            throw new RuntimeException("節點[" + eleName + "]配置錯誤,沒法解析");
        }

        // 根據節點名稱找到對應的節點解析器解析節點
        this.parserMap.get(eleName).parse(ele, this.beanFactory);

    }

}
複製代碼

節點解析器解析節點

  • 節點解析器
// Bean節點解析器,解析XML配置文件中的<bean>節點
public class BeanElementParser implements ElementParser {

    // 解析<bean>節點
    @SuppressWarnings("unchecked")
    @Override
    public void parse(Element ele, BeanFactory factory) throws Exception {
        
        // <bean>節點中的id和class屬性
        String cls = ele.getAttributeValue("class");
        String id = ele.getAttributeValue("id");
        
        // 類型
        Class<?> clazz = Class.forName(cls);
        
        // 封裝成類描述信息
        BeanDefinition bd = new BeanDefinition(id, clazz);

        // 向BEAN工廠註冊Bean
        factory.registerBean(bd);

    }

}
複製代碼
  • 節點解析器
// <component-scan>節點解析器
public class ComponentScanElementParser implements ElementParser {

    // 解析<component-scan>節點
    @Override
    public void parse(Element ele, BeanFactory factory) throws Exception {

        // package屬性(掃描目錄)
        String basePackage = ele.getAttributeValue("package");
        if (basePackage == null) {
            throw new RuntimeException("<component-scan>必須配置package屬性");
        }

        // 獲取掃描目錄絕對路徑
        String baseDir = getClass().getClassLoader().getResource(basePackage.replace('.', '/')).getPath();

        // 掃描目錄,獲取目錄下的全部類文件
        for (File file : new File(baseDir).listFiles()) {

            // 獲取CLASS的路徑(包目錄+類名)並加載CLASS
            String classPath = basePackage + "." + file.getName().replaceAll("\\.class", "");
            Class<?> clazz = Class.forName(classPath);

            // 只處理含有@Component的BEAN
            if (!clazz.isAnnotationPresent(Component.class)) {
                continue;
            }

            // 獲取類的@Component註解
            Component c = clazz.getAnnotation(Component.class);
            // 封裝成類描述信息
            BeanDefinition bd = new BeanDefinition(c.value(), clazz);

            // 向BEAN工廠註冊Bean
            factory.registerBean(bd);

        }

    }

}
複製代碼

BEAN工廠註冊BEAN並實例化

// BEAN工廠, 提供BEAN的建立及獲取
public class BeanFactory {

    // BEAN描述信息容器, 保存全部BEAN的信息. K: BEAN名稱, V: BEAN描述信息
    private final Map<String, BeanDefinition> beanDefinitionMap = new ConcurrentHashMap<String, BeanDefinition>();

    // BEAN實例容器, 保存全部BEAN的實例化對象. K: BEAN名稱, V: BEAN實例化對象
    private final Map<String, Object> beanObjectMap = new ConcurrentHashMap<String, Object>();

    // 註冊BEAN
    public void registerBean(BeanDefinition bean) {
        beanDefinitionMap.put(bean.getName(), bean);
    }

    // 獲取全部已註冊BEAN的名稱
    public Set<String> getBeanNames() {
        return this.beanDefinitionMap.keySet();
    }

    // 根據名稱獲取BEAN的類型
    public Class<?> getBeanType(String name) {
        return this.beanDefinitionMap.get(name).getClazz();
    }

    // 根據名稱獲取BEAN的實例
    @SuppressWarnings("unchecked")
    public <T> T getBean(String name) throws Exception {

        // 根據名稱從容器獲取BEAN
        Object bean = this.beanObjectMap.get(name);

        // 容器中存在直接返回
        if (bean != null) {
            return (T) bean;
        }

        // 未獲取到時自動建立
        // 查看緩存中是否有BEAN描述
        if (!this.beanDefinitionMap.containsKey(name)) {
            throw new RuntimeException("未定義BEAN[" + name + "]");
        }

        // 存在BEAN描述時根據描述信息實例化BEAN
        BeanDefinition beanDef = this.beanDefinitionMap.get(name);
        bean = beanDef.getClazz().newInstance();

        // 將BEAN實例化保存至容器
        this.beanObjectMap.put(name, bean);

        // 返回新建立BEAN
        return (T) bean;

    }

    // 實例化BEAN
    public void instanceBean() throws Exception {

        // 根據緩存的BEAN描述信息依次建立BEAN
        for (String beanName : this.beanDefinitionMap.keySet()) {
            getBean(beanName);
        }

    }

}
複製代碼

測試

  • 建立BEAN
package com.atd681.xc.ssm.ioc.demo.service;

import com.atd681.xc.ssm.ioc.framework.annotation.Component;

// 經過註解聲明BEAN
@Component
public class ServiceX {

    public void test() {
        System.out.println("ServiceX.test start...");
    }

}
複製代碼
// 經過配置文件配置BEAN
public class ManagerX {

    public void test() {
        System.out.println("ManagerX.test start...");
    }

}
複製代碼
  • 建立XML配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans>

    <!-- 配置BEAN所在目錄, IOC容器會掃描該目錄, 加載含有@Component註解的Bean -->
	<component-scan package="com.atd681.xc.ssm.ioc.demo.service" />
	
    <!-- 配置BEAN -->
	<bean id="managerX" class="com.atd681.xc.ssm.ioc.demo.ManagerX"></bean>
	
</beans>
複製代碼
  • 建立測試類
// IOC測試類
public class Test {

    // 測試IOC容器
    public static void main(String[] args) throws Exception {

        // 實例化應用上下文並設置配置文件路徑
        ApplicationContext context = new ApplicationContext("context.xml");
        // 初始化上下文(IOC容器)
        context.init();

        // 從IOC容器中獲取BEAN並執行
        ServiceX serviceX = context.getBean("serviceX");
        ManagerX managerx = context.getBean("managerX");
        serviceX.test();
        managerx.test();

    }

}
複製代碼
  • 運行

從IOC容器中獲取BEAN並執行後輸出以下結果: BEAN的實例對象已經保存在IOC容器中.

ServiceX.test start...
ManagerX.test start...
複製代碼

IOC容器未找到對應的BEAN(未配置或配置錯誤)時會拋出異常: BEAN的實例對象沒有在IOC容器中.

Exception in thread "main" java.lang.RuntimeException: 未定義BEAN[managerX1]
	at com.atd681.xc.ssm.ioc.framework.BeanFactory.getBean(BeanFactory.java:54)
	at com.atd681.xc.ssm.ioc.framework.ApplicationContext.getBean(ApplicationContext.java:117)
	at com.atd681.xc.ssm.ioc.demo.Test.main(Test.java:19)
複製代碼

總結

Spring IOC實現對BEAN控制權的反轉, 將BEAN統一將由IOC容器建立及管理. 只有IOC容器統一管理BEAN後才能完成對各BEAN依賴屬性的自動注入.

Spring的IOC容器經過配置文件獲取到須要被管理的BEAN後, 將BEAN的信息解析封裝並註冊至BEAN工廠(BEAN工廠緩存BEAN描述信息). 全部BEAN註冊完成後依次對BEAN進行實例化並保存在BEAN工廠的容器中, 以此來實現對BEAN的統一管理. 當須要獲取BEAN時, 統一從BEAN工廠容器中獲取.

下一篇將實現依賴注入的功能.

相關文章
相關標籤/搜索