中級17 - Spring IoC容器原理與手寫簡單實(shí)現(xiàn)

一切開始之前挽拂,先了解下 JavaBean 是什么,它是一種標(biāo)準(zhǔn)和約定人灼。
一個 JavaBean:

  • 所有的屬性都是 private(通過 getter/setter 進(jìn)行讀寫)
  • 有一個 public 無參數(shù)的構(gòu)造器
  • 實(shí)現(xiàn)了 Serializable(序列化围段,注意 Serializable 接口沒有方法也沒有字段,更多的是一種規(guī)范和約定)

1. Spring 是什么

Srping 是自動化管理 Java 對象和其中依賴關(guān)系的容器投放。

  • Java世界中Web容器的事實(shí)標(biāo)準(zhǔn)
  • Spring 容器 - 一個 IoC 容器
    • Spring MVC - 基于 Spring 和 Servlet 的 Web 應(yīng)用框架
      • Spring Boot - 集成度和自動化程度更高(內(nèi)嵌了 Servlet 容器)

-let 詞根奈泪,“小”的意思,servlet 小服務(wù)器(service + let)。
Servlet 將網(wǎng)絡(luò)請求封裝成對象涝桅,交給上層 WebApp(spring容器拜姿、mvc、boot)冯遂。
Servlet <-> HttpServletRequest/HttpServletResponse <-> WebApp蕊肥。
常見的 servlet 有:

  • tomcat
  • jetty

沒有 Spring 怎么辦?
**

  • 一個 main 程序走天下:輕量簡單蛤肌,但是規(guī)模大了后難以維護(hù)壁却。
  • 拆分并且手動管理:優(yōu)點(diǎn)方便測試、共同開發(fā)裸准、維護(hù)展东,但缺點(diǎn)還是規(guī)模更大后,依賴關(guān)系太復(fù)雜炒俱。

Spring 的出現(xiàn)解放了我們的雙手盐肃。

2. Spring 最簡單的用法

通過配置一種“上古”的 xml 來使用。
xml 配置文件中聲明了兩個 Bean向胡, 而這兩個 Bean 之間存在依賴關(guān)系恼蓬,因此為 OrderService 中的成員屬性 OrderDao 添加了注解惊完,用于對 OrderDao 自動裝配僵芹。

在這里,容器就是 BeanFactory小槐。有了容器之后拇派,向容器索要 Bean。

// maven 依賴
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.6.RELEASE</version>
</dependency>
package spring.demo;

public class OrderDao {
    public void select() {
        System.out.println("select!");
    }
}
package spring.demo;

import org.springframework.beans.factory.annotation.Autowired;

public class OrderService {

    @Autowired // 加了這個注解才會自動裝配 OrderDao凿跳,否則 OrderService 對象中的 orderDao 是 null
    private OrderDao orderDao;

    public void doSomething() {
        orderDao.select();
    }
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config /> <!-- 這句不加的話件豌,即使寫了上面的注解也不生效 -->
    <!-- bean definitions here -->
    <bean id="orderDao" class="spring.demo.OrderDao"/>
    <bean id="orderService" class="spring.demo.OrderService"/>
</beans>
package spring.demo;

import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class SpringMain {
    public static void main(String[] args) {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("classpath:spring/config.xml");

        OrderService orderService = (OrderService)beanFactory.getBean("orderService");
        OrderDao orderDao = (OrderDao) beanFactory.getBean("orderDao");

        System.out.println(orderService);
        orderService.doSomething();
        System.out.println(orderDao);
    }
}

現(xiàn)在沒有寫 new,卻拿到了 OrderService 對象(一個Bean)控嗜,默認(rèn)是單例模式茧彤,也就是 OrderService 對象中的 OrderDao 和再次通過 getBean 得到的 OrderDao 對象是同一個對象。


image.png
image.png

假如沒有使用 Spring疆栏,那么可能要自己手動創(chuàng)建曾掂,涉及到的各種對象的各種 new。

3. Spring 容器核心概念

  • Bean

容器中的最小工作單元壁顶,通常為一個 Java 對象

  • BeanFactory/ApplicationContext

容器本身對應(yīng)的 Java 對象

  • 依賴注入(DI)

容器負(fù)責(zé)注入所有的依賴

  • 控制反轉(zhuǎn)(IoC)

用戶將控制權(quán)交給了 Spring 容器來進(jìn)行自動裝配

4. 手寫一個簡單的 IoC 容器

  • 定義 Bean
  • 加載 Bean 的定義
  • 實(shí)例化 Bean
  • 查找依賴珠洗,實(shí)現(xiàn)依賴注入
  • 要什么 Bean 就設(shè)置成什么 Bean

目錄結(jié)構(gòu):


image.png
image.png

使用方法是在字段是聲明注解(部分代碼不再羅列,這里僅供舉例):

import org.springframework.beans.factory.annotation.Autowired;

public class OrderService {
    @Autowired private OrderDao orderDao;
    @Autowired private UserService userService;

    public void createOrder() {
        orderDao.createOrder(userService.getCurrentLoginUser());
    }
}

具體實(shí)現(xiàn):

Java 從初代就支持了 .properties 格式的配置文件若专,該文件用來存儲簡單的基于 key-value pairs 的參數(shù)许蓖。該配置文件處于被編譯的代碼之外。

  1. 先寫一個簡單的 beans.properties 配置文件,定義了 Bean 的名字和對應(yīng)的實(shí)現(xiàn)類:
# Bean 名字 和 Bean 的全限定類名
orderDao=com.github.hcsp.ioc.OrderDao
userDao=com.github.hcsp.ioc.UserDao
userService=com.github.hcsp.ioc.UserService
orderService=com.github.hcsp.ioc.OrderService
  1. MyIoCContainer 容器:
import org.springframework.beans.factory.annotation.Autowired;

import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class MyIoCContainer {
    // 實(shí)現(xiàn)一個簡單的IoC容器膊爪,使得:
    // 1. 從beans.properties里加載bean定義
    // 2. 自動掃描bean中的@Autowired注解并完成依賴注入

    // 定義一個容器! 存放 bean 的名字到 bean 實(shí)例對象的映射
    private Map<String, Object> beans = new HashMap<>();

    /**
     * 依賴注入
     *
     * @param beanInstance bean 的 實(shí)例
     */
    private void dependencyInject(Object beanInstance) {
        // 拿到帶有 @AutoWired 注解的 fields
        List<Field> fieldsToBeAutoWired = Stream.of(beanInstance.getClass().getDeclaredFields())
                .filter(field -> field.getAnnotation(Autowired.class) != null)
                .collect(Collectors.toList());
        // 為當(dāng)前 bean 對象的需要依賴的字段注入依賴(設(shè)置字段值)
        fieldsToBeAutoWired.forEach(field -> {
            String fieldName = field.getName(); // 加了 @AutoWired 的字段名即是所要依賴的 bean 的名字
            Object dependencyBeanInstance = beans.get(fieldName); // 所依賴的 bean 實(shí)例
            try {
                field.setAccessible(true); // 設(shè)置為 true 用來壓制針對被反射對象的訪問檢查
                field.set(beanInstance, dependencyBeanInstance); // 從而可以在這里設(shè)置當(dāng)前 bean 的私有字段
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        });
    }

    /**
     * 啟動該容器
     */
    public void start() {
        // bean 的初始化

        Properties properties = new Properties();

        // 從 InputStream 中讀取屬性列表(鍵值對)
        try {
            properties.load(MyIoCContainer.class.getResourceAsStream("/beans.properties"));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        System.out.println(properties);

        properties.forEach((beanName, beanClassName) -> {
            try {
                // 通過反射拿到 bean 的實(shí)例并放入容器中
                Class<?> klass = Class.forName((String) beanClassName);
                Object beanInstance = klass.getConstructor().newInstance();
                beans.put((String) beanName, beanInstance);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        });

        // 使用反射自阱,處理依賴關(guān)系,注入依賴
        beans.forEach((beanName, beanInstance) -> dependencyInject(beanInstance));
    }

    /**
     * 從容器中獲取一個bean
     *
     * @param beanName bean 的名字
     * @return 返回 bean 的實(shí)例
     */
    public Object getBean(String beanName) {
        return beans.get(beanName);
    }

    public static void main(String[] args) {

        MyIoCContainer container = new MyIoCContainer();
        container.start();
        OrderService orderService = (OrderService) container.getBean("orderService");
        orderService.createOrder();
    }
}

即使是兩個依賴字段存在循環(huán)依賴也沒關(guān)系蚁飒,因?yàn)樵趧?chuàng)造期間动壤,會先各自創(chuàng)建出實(shí)例對象。相關(guān)依賴字段此時(shí)是 null淮逻,不影響 bean 的創(chuàng)建琼懊。而創(chuàng)建完之后,再回頭對字段注入所依賴的 bean爬早。

當(dāng)然哼丈,以上只是個簡單的實(shí)現(xiàn),實(shí)際的 Spring 中還可以在構(gòu)造器上使用 @Autowired 注解筛严,而不推薦在私有字段上使用醉旦。這樣即使不是用 Spring,比如寫測試代碼的時(shí)候桨啃,還是可以方便的 new 一個實(shí)例车胡,否則,私有字段還要通過反射一頓操作才能創(chuàng)建實(shí)例進(jìn)行測試照瘾。

另外匈棘,實(shí)際應(yīng)用中 @Autowired 也不推薦使用了,更推薦 @Inject 注解析命,這樣除了 Spring 之外主卫,還可能受到其他類似框架的識別。

所謂的 Spring 只不過是在以上基礎(chǔ)上擴(kuò)充了無窮無盡的功能鹃愤,比如支持 xml 中配置依賴簇搅,支持構(gòu)造器注入(調(diào)用哪個構(gòu)造器,傳遞哪些參數(shù))等等软吐,從一個很簡單的思想不斷擴(kuò)充成龐大的體系瘩将。

5. Spring 啟動過程淺析

  • 在 xml 里中定義 Bean
  • BeanDefinition 的加載和解析
  • Bean 的實(shí)例化和依賴注入
  • 對外提供服務(wù)

建議 debug Spring 源碼的時(shí)候,要帶著目前來進(jìn)行凹耙,不要在無關(guān)細(xì)節(jié)里浪費(fèi)太多時(shí)間姿现。


image.png
image.png

image.png
image.png

image.png
image.png

6. 參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市使兔,隨后出現(xiàn)的幾起案子建钥,更是在濱河造成了極大的恐慌,老刑警劉巖虐沥,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件熊经,死亡現(xiàn)場離奇詭異泽艘,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)镐依,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門匹涮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人槐壳,你說我怎么就攤上這事然低。” “怎么了务唐?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵雳攘,是天一觀的道長。 經(jīng)常有香客問我枫笛,道長吨灭,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任刑巧,我火速辦了婚禮喧兄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘啊楚。我一直安慰自己吠冤,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布恭理。 她就那樣靜靜地躺著拯辙,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蚯斯。 梳的紋絲不亂的頭發(fā)上薄风,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天饵较,我揣著相機(jī)與錄音拍嵌,去河邊找鬼。 笑死循诉,一個胖子當(dāng)著我的面吹牛横辆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播茄猫,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼狈蚤,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了划纽?” 一聲冷哼從身側(cè)響起脆侮,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎勇劣,沒想到半個月后靖避,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體潭枣,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年幻捏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了盆犁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡篡九,死狀恐怖谐岁,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情榛臼,我是刑警寧澤伊佃,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站沛善,受9級特大地震影響锭魔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜路呜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一迷捧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧胀葱,春花似錦漠秋、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至轧葛,卻和暖如春搂抒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背尿扯。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工求晶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人衷笋。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓芳杏,卻偏偏與公主長得像,于是被迫代替她去往敵國和親辟宗。 傳聞我的和親對象是個殘疾皇子爵赵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評論 2 348