五分鐘,手擼一個Spring容器!

三分惡發表於2022-03-01

大家好,我是老三,Spring是我們最常用的開源框架,經過多年發展,Spring已經發展成枝繁葉茂的大樹,讓我們難以窺其全貌。

這節,我們迴歸Spring的本質,五分鐘手擼一個Spring容器,揭開Spring神祕的面紗!

從什麼是IOC開始?

Spring——春天,Java程式設計世界的春天是由一位音樂家——Rod Johnson帶來的。

Rod Johnson先後編寫了兩本鉅著《Expert One-on-One J2EE Design and Development》、《Expert One-on-One J2EE Development without EJB》,拉起了挑戰正統Java EE框架EJB的大旗。

Rod Johnson兩大著作-來自百度百科

Rod Johnson不僅是一名旗手,更是開發了Spring這一輕量級框架,像一名勇敢的龍騎兵一樣,對EJB發動了衝鋒,並最終戰勝了EJB,讓Spring成為Java EE事實上的標準。

Spring Logo

Spring的兩大核心分別是IOC和AOP,其中最最核心的是IOC。

所謂的IOC(控制反轉):就是由容器來負責控制物件的生命週期和物件間的關係。以前是我們想要什麼,就自己建立什麼,現在是我們需要什麼,容器就給我們送來什麼。

引入IOC之前和引入IOC之後

也就是說,控制物件生命週期的不再是引用它的物件,而是容器。對具體物件,以前是它控制其它物件,現在所有物件都被容器控制,所以這就叫控制反轉

控制反轉示意圖

也許你還聽到另外一個概念DI(依賴注入),它指的是容器在例項化物件的時候把它依賴的類注入給它,我們也可以認為,DI是IOC的補充和實現。

工廠和Spring容器

Spring是一個成熟的框架,為了滿足擴充套件性、實現各種功能,所以它的實現如同枝節交錯的大樹一樣,現在讓我們把視線從Spring本身移開,來看看一個萌芽版的Spring容器怎麼實現。

Spring的IOC本質就是一個大工廠,我們想想一個工廠是怎麼執行的呢?

工廠執行

  • 生產產品:一個工廠最核心的功能就是生產產品。在Spring裡,不用Bean自己來例項化,而是交給Spring,應該怎麼實現呢?——答案毫無疑問,反射

    那麼這個廠子的生產管理是怎麼做的?你應該也知道——工廠模式

  • 庫存產品:工廠一般都是有庫房的,用來庫存產品,畢竟生產的產品不能立馬就拉走。Spring我們都知道是一個容器,這個容器裡存的就是物件,不能每次來取物件,都得現場來反射建立物件,得把建立出的物件存起來。

  • 訂單處理:還有最重要的一點,工廠根據什麼來提供產品呢?訂單。這些訂單可能五花八門,有線上籤籤的、有到工廠籤的、還有工廠銷售上門籤的……最後經過處理,指導工廠的出貨。

    在Spring裡,也有這樣的訂單,它就是我們bean的定義和依賴關係,可以是xml形式,也可以是我們最熟悉的註解形式。

那對應我們的萌芽版的Spring容器是什麼樣的呢?

mini版本Spring IOC

訂單:Bean定義

Bean可以通過一個配置檔案定義,我們會把它解析成一個型別。

Bean定義

  • beans.properties

    為了偷懶,這裡直接用了最方便解析的properties,用一個<key,value>型別的配置來代表Bean的定義,其中key是beanName,value是class

    userDao:cn.fighter3.bean.UserDao
    
  • BeanDefinition.java

    bean定義類,配置檔案中bean定義對應的實體

    public class BeanDefinition {
    
        private String beanName;
    
        private Class beanClass;
         //省略getter、setter  
     }   
    

獲取訂單:資源載入

接下訂單之後,就要由銷售向生產部門交接,讓生產部門知道商品的規格、數量之類。

資源載入器,就是來完成這個工作的,由它來完成配置檔案中配置的載入。

public class ResourceLoader {

    public static Map<String, BeanDefinition> getResource() {
        Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>(16);
        Properties properties = new Properties();
        try {
            InputStream inputStream = ResourceLoader.class.getResourceAsStream("/beans.properties");
            properties.load(inputStream);
            Iterator<String> it = properties.stringPropertyNames().iterator();
            while (it.hasNext()) {
                String key = it.next();
                String className = properties.getProperty(key);
                BeanDefinition beanDefinition = new BeanDefinition();
                beanDefinition.setBeanName(key);
                Class clazz = Class.forName(className);
                beanDefinition.setBeanClass(clazz);
                beanDefinitionMap.put(key, beanDefinition);
            }
            inputStream.close();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return beanDefinitionMap;
    }

}

訂單分配:Bean註冊

物件註冊器,這裡用於單例bean的快取,我們大幅簡化,預設所有bean都是單例的。可以看到所謂單例註冊,也很簡單,不過是往HashMap裡存物件。

public class BeanRegister {

    //單例Bean快取
    private Map<String, Object> singletonMap = new HashMap<>(32);

    /**
     * 獲取單例Bean
     *
     * @param beanName bean名稱
     * @return
     */
    public Object getSingletonBean(String beanName) {
        return singletonMap.get(beanName);
    }

    /**
     * 註冊單例bean
     *
     * @param beanName
     * @param bean
     */
    public void registerSingletonBean(String beanName, Object bean) {
        if (singletonMap.containsKey(beanName)) {
            return;
        }
        singletonMap.put(beanName, bean);
    }

}

生產車間:物件工廠

好了,到了我們最關鍵的生產部門了,在工廠裡,生產產品的是車間,在IOC容器裡,生產物件的是BeanFactory。

BeanFactory

  • 物件工廠,我們最核心的一個類,在它初始化的時候,建立了bean註冊器,完成了資源的載入。

  • 獲取bean的時候,先從單例快取中取,如果沒有取到,就建立並註冊一個bean

    public class BeanFactory {
    
        private Map<String, BeanDefinition> beanDefinitionMap = new HashMap<>();
    
        private BeanRegister beanRegister;
    
        public BeanFactory() {
            //建立bean註冊器
            beanRegister = new BeanRegister();
            //載入資源
            this.beanDefinitionMap = new ResourceLoader().getResource();
        }
    
        /**
         * 獲取bean
         *
         * @param beanName bean名稱
         * @return
         */
        public Object getBean(String beanName) {
            //從bean快取中取
            Object bean = beanRegister.getSingletonBean(beanName);
            if (bean != null) {
                return bean;
            }
            //根據bean定義,建立bean
            return createBean(beanDefinitionMap.get(beanName));
        }
    
        /**
         * 建立Bean
         *
         * @param beanDefinition bean定義
         * @return
         */
        private Object createBean(BeanDefinition beanDefinition) {
            try {
                Object bean = beanDefinition.getBeanClass().newInstance();
                //快取bean
                beanRegister.registerSingletonBean(beanDefinition.getBeanName(), bean);
                return bean;
            } catch (InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    

生產銷售:測試

  • UserDao.java

    我們的Bean類,很簡單

    public class UserDao {
    
        public void queryUserInfo(){
            System.out.println("A good man.");
        }
    }
    
  • 單元測試

    public class ApiTest {
        @Test
        public void test_BeanFactory() {
            //1.建立bean工廠(同時完成了載入資源、建立註冊單例bean註冊器的操作)
            BeanFactory beanFactory = new BeanFactory();
    
            //2.第一次獲取bean(通過反射建立bean,快取bean)
            UserDao userDao1 = (UserDao) beanFactory.getBean("userDao");
            userDao1.queryUserInfo();
    
            //3.第二次獲取bean(從快取中獲取bean)
            UserDao userDao2 = (UserDao) beanFactory.getBean("userDao");
            userDao2.queryUserInfo();
        }
    }
    
  • 執行結果

    A good man.
    A good man.
    

至此,我們一個萌芽版的Spring容器就完成了。

考慮一下,它有哪些不足呢?是否還可以抽象、擴充套件、解耦……

細細想想這些東西,你是不是對真正的Spring IOC容器為何如此複雜,有所理解了呢?




參考:

[1]. 《Spring揭祕》

[2].小傅哥 《手擼Spring》

[3].《精通Spring4.X企業應用開發實戰》

相關文章