Spring學習筆記(1)----Spring IOC學習和理解

1. 基本思想和概念

1.1 Spring IOC思想

Spring IOC(Inversion of Control,控制反轉)是Spring的一個核心思想,其包括兩層含義:html

  • 控制:控制是指當前對象對內部依賴對象的控制權,好比建立依賴對象。
  • 反轉:這種控制權不禁當前對象來管理,由第三方(好比容器)來管理。(反轉也能夠理解爲獲取依賴對象的方式從主動變成了被動)

在Spring中,控制反轉也能夠簡單地理解爲將對象的建立和管理交由IOC容器來完成。java

1.2 依賴注入(Dependency Injection)

IOC將依賴對象的控制權轉移到外部,但當前對象仍是須要依賴對象的,這時候就須要使用依賴注入將所須要的依賴對象從外部注入進來。spring

1.3 控制反轉與依賴注入的關係

控制反轉是一種思想,而依賴注入是一種實現方式。segmentfault

  • 有些文章有提到,實現控制反轉的方式還有依賴查找。

1.4 Spring IOC的優勢

爲何須要使用Spring IOC?從IOC思想來看,主要有兩大優勢:數組

  • 資源集中管理,實現資源可配置和易管理;
  • 經過IOC實現當前對象和依賴對象之間的鬆耦合

從Spring IOC的實際實現來看,還有以下好處:bash

  • 享受單例的好處,效率高,節約空間;
  • 便於單元測試,方便切換mock組件;
  • 便於進行AOP操做;

1.4.1 Spring IOC實現鬆耦合理解

沒有IOC的狀況下,使用依賴的對象須要手動new一個對象出來,根據構造器是否須要參數,能夠分爲有參對象和無參對象。而Spring只須要在xml文件中集中配置一次,或者使用註解就能夠實現依賴注入,不須要手動new對象出來。app

接下來考慮幾種須要修改具體對象實現的狀況下代碼重構的成本,來理解Spring IOC如何實現鬆耦合。工具

(1)無參對象,須要修改類名

好比須要將類名從Message修改成News,那麼此時能夠藉助開發工具的重構功能實現代碼重構。
重構難度:1星。post

(2)無參對象,須要修改對象類型

好比須要將對象類型名從Student修改成Teacher,那麼此時能夠藉助查找替換功能實現代碼重構。 無重寫方法狀況下,重構難度:2星; 有重寫方法狀況下,重構難度:3星;單元測試

(3)有參對象,須要修改參數;

好比須要在建立Student對象時,增長年齡參數,此時藉助查找功能也須要一個個手動修改代碼,增長參數。
重構難度:5星。

(4)Spring IOC重構

若是使用Spring IOC,即使對難度最大的第三種狀況,也只須要在xml文件中修改下注入的參數,或者在對應對象中增長一個屬性,並使用註解自動注入便可。
重構難度:1星。

2. Spring IOC容器簡介

IOC容器是Spring提供的一個工廠,用於管理全部的bean、以及bean之間的關係。

2.1 Spring IOC容器原理

  • IOC容器底層原理是Java反射機制,經過反射機制獲取類的全部信息(屬性、類名等);
  • IOC容器再經過讀取xml文件、或者註解來獲取bean之間依賴關係。

2.2 常見IOC容器

Spring中主要提供兩類IOC容器,分別是BeanFactoryApplicationContext

  • BeanFactory: 最基礎的IOC容器,提供完整的IOC服務支持。
  • ApplicationContext: ApplicationContext是在 BeanFactory的基礎之上構建的,是相對比較高級的容器實現。

二者間繼承關係以下圖,可見ApplicationContext間接繼承自BeanFactory。

2.2.1 BeanFactory

BeanFactory是Spring提供的最基礎的IOC容器,用於幫助完成bean的註冊和bean之間關係的綁定。
特色:

  • 默認採用延加載策略。當容器中bean被訪問時,才進行bean的初始化和依賴注入工做。

2.2.1.1 實現類

BeanFactory最基礎的實現類是DefaultListableBeanFactory
舊版本中BeanFactory最多見實現類是XmlBeanFactory(現已被廢棄),新版中使用XmlBeanDefinitionReader + DefaultListableBeanFactory替代XmlBeanFactory

2.2.1.2 代碼示例

(1)創建一個工程,引入響應jar包

出於方便,可直接創建了一個SpringBoot工程。

(2)創建xml文件,複製基本結構
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
    
</beans>
複製代碼
(3)建立對象類

這裏使用@Data註解省略了getter和setter方法。

@Data
public class User {
    private Integer age;
    private Nation nation;

    public User() {
    }
    public User(Integer age) {
        this.age = age;
    }
    public User(Nation nation) {
        this.nation = nation;
    }
}
複製代碼
(4)在配置文件中註冊一個bean
<bean id="user" class="com.example.springdemo.xml.model.User">
    <constructor-arg name="age" type="java.lang.Integer" value="26"></constructor-arg>
</bean>
複製代碼
(5)實例化容器、加載配置文件、獲取bean
public static void main(String[] args) {
    // 1. XmlBeanFactory方式,已廢棄
    // XmlBeanFactory factory = new XmlBeanFactory(new ClassPathResource("application.xml"));

    // 2. XmlBeanDefinitionReader + DefaultListableBeanFactory 替代XmlBeanFactory
    Resource resource = new ClassPathResource("application.xml");
    BeanFactory factory = new DefaultListableBeanFactory();
    BeanDefinitionReader bdr = new XmlBeanDefinitionReader((BeanDefinitionRegistry) factory);
    bdr.loadBeanDefinitions(resource);
    // 根據bean名從容器中獲取bean
    User user1 = (User) factory.getBean("user");
    System.out.println(user1.getAge());
}
複製代碼

2.2.2 ApplicationContext

ApplicationContext是在BeanFactory的基礎上構建的,是相對比較高級的容器實現,除了擁有 BeanFactory的全部支持,ApplicationContext還提供了其餘高級特性,好比:

  • 統一資源加載策略
  • 國際化信息支持
  • 容器內部事件發佈機制

2.2.2.1 實現類

ApplicationContext常見實習類有:FileSystemXmlApplicationContext、ClassPathXmlApplicationContext和XmlWebApplicationContext。

2.2.2.2 代碼示例

仍然使用BeanFactory示例中的配置文件,同時使用ClassPathXmlApplicationContext來讀取xml文件。

// 3. ApplicationContext擁有比BeanFactory更高級的特性
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");
User user3 = (User) applicationContext.getBean("user");
System.out.println(user3.getAge());
複製代碼

3. Spring IOC容器裝配Bean詳解

3.1 依賴注入方式

依賴注入方式主要包括:

  • 構造器注入
  • 屬性注入(setter方法注入)

其中經常使用的兩種方式是構造器注入屬性注入。雖然官方文檔建議能用構造器注入就用構造器注入,由於這樣可使得依賴關係明確,而且若是缺乏依賴的話在初始化階段就能夠發現問題。但屬性注入更加靈活,而且構造器注入方式沒法解決循環依賴問題,因此通常使用屬性注入強制依賴建議使用構造器方式注入。

3.1.1 構造器注入

基於構造器的依賴注入是指,在bean的構造器中指明依賴的bean,並在初始化時完成注入。

代碼示例

(1)bean類

User類見2.2.1.2中,存在以下構造器:

public User(Integer age) {
    this.age = age;
}
複製代碼
(2)在xml中註冊bean,並經過構造器方式進行依賴注入
<!-- 構造器方式注入 -->
<bean id="user" class="com.example.springdemo.xml.model.User">
    <constructor-arg name="age" type="java.lang.Integer" value="26"></constructor-arg>
</bean>
複製代碼

3.1.2 屬性注入

基於屬性的依賴注入是指,容器經過無參構造器初始化bean,再經過調用setter方法來完成注入。

  • 不必定是無參構造器,只要後續經過setter方法進行注入就行。
(1)bean類

User類如上,有屬性age(經過@Data註解省略了getter/setter方法):

private Integer age;
複製代碼

(2)註冊bean,並經過屬性方式進行依賴注入

<!-- 屬性注入 -->
<bean id="user2" class="com.example.springdemo.xml.model.User">
    <property name="age" value="27"></property>
</bean>
複製代碼

(3)獲取bean,並輸出其依賴

// user使用了構造器注入
User user1 = (User) factory.getBean("user");
System.out.println("DI by constructor:" + user1.getAge());
// user2使用了屬性注入
User user2 = (User) factory.getBean("user2");
System.out.println("DI by setter:" + user2.getAge());
複製代碼

輸出:

3.2 自動裝配

3.2.1 手動注入依賴bean

在3.1小節中,User bean注入了int型的age屬性,一樣能夠注入其它由IOC容器管理的bean。

(1)bean類

User類還存在以下構造器,表示User bean依賴Nation bean。

public User(Nation nation) {
    this.nation = nation;
}
複製代碼

Nation類:

public class Nation {
    public String name;
    public Nation(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
複製代碼

(2)註冊bean,並聲明依賴關係

註冊Nation bean,同時註冊User bean,並使用構造器方式對User bean注入Nation bean。

<bean id="nation" class="com.example.springdemo.xml.model.Nation">
    <constructor-arg name="name" type="java.lang.String" value="China"></constructor-arg>
</bean>

<!-- 手動注入依賴bean -->
<bean id="user3" class="com.example.springdemo.xml.model.User">
    <constructor-arg name="nation" type="com.example.springdemo.xml.model.Nation" ref="nation"></constructor-arg>
</bean>
複製代碼

(3)獲取bean,並輸出其依賴bean的屬性

// 注入IOC容器管理的bean,user注入時須要nation。
// 這時候也能明顯體現Spring Ioc解耦的優勢,若是不使用依賴注入,將對象的建立交由bean來作的話,代碼以下。
// 若是須要修改傳入參數,項目中全部地方都須要修改
Nation nation = new Nation("China");
User user4 = new User(nation);
System.out.println("Name of nation in user3 by code: " + user4.getNation().getName());
// 而使用依賴注入的話,只須要在xml中一個地方集中配置管理,在配置文件中注入取值或者內部bean,而不須要每一個建立對象的地方都要修改
User user5 = (User) factory.getBean("user3");
System.out.println("Name of nation in user3 by IOC: " + user5.getNation().getName());
複製代碼

輸出:

3.2.2 自動裝配

在實際項目中,通常對於Dao、Service都須要單例,不會建立過多的bean,那麼不少時候這些bean並不存在name、type的衝突,這時候是否是能夠根據特定的規則,來簡化bean的裝配。
Spring IOC容器還提供了自動裝配,能夠根據bean的type、name進行自動裝配,而不須要顯示地聲明bean間依賴關係。

(1)xml中聲明自動裝配

<!-- 自動裝配bean,這裏是byType方式。自動裝配時會將知足條件的bean注入到構造器和setter方法中 -->
<!-- 自動裝配缺點:(1)不適用構造器重寫的狀況;(2)不能裝配基本類型和字符串 -->
<bean id="user4" class="com.example.springdemo.xml.model.User" autowire="byType">
    <constructor-arg name="age" type="java.lang.Integer" value="28"></constructor-arg>
</bean>
複製代碼

(2)獲取bean,並輸出其依賴bean

// 自動裝配bean
User user6 = (User) factory.getBean("user4");
System.out.println("自動裝配:" + user6.getAge());
System.out.println("自動裝配:" + user6.getNation().getName());
複製代碼

輸出:

3.3 讀取依賴關係方式

Spring IOC容器讀取依賴關係方式有:

  • (1)xml配置文件方式
  • (2)註解方式
  • (3)JavaConfig:屬於註解方式中特殊方式,在一個java類集中使用註解來註冊bean、聲明bean間依賴關係,能夠理解爲將一個的xml配置文件變成了一個Java配置類,而且註解形式來聲明。

相比較而言,xml配置文件方式擁有集中配置的優勢,而註解方式則擁有簡化配置的優勢。

3.3.1 xml配置文件

Spring IOC容器能夠經過加載xml配置文件,來讀取依賴關係。

  • 3.2示例的代碼都是經過此種方式來讀取bean間依賴關係。

3.3.2 註解

因爲xml配置文件擁有繁雜、不易配置的缺點,Spring IOC容器還同時支持註解來簡化配置。
使用註解來讀取依賴關係的步驟以下:

(1)聲明註解

在bean類上使用註解@Component聲明bean,使用註解@Autowired來聲明依賴關係:

@Component
@Data
public class User {
    private Integer age;
    
    @Autowired
    private Nation nation;
}
複製代碼

(2)實例化容器、設置要掃描註解類

建立ApplicationContext類型的IOC容器(BeanFactory實測不支持),並設置要掃描註解的包路徑。

// 能夠傳入要掃描的包路徑動態數組,也能夠傳入Class動態數組
ApplicationContext applicationContext = new AnnotationConfigApplicationContext("com.example.springdemo.annation");

User user1 = (User) applicationContext.getBean("user");
System.out.println(user1.getNation().getName());
複製代碼

3.3.3 xml配置文件和註解混用

Spring IOC容器其實還支持xml配置文件和註解混用的方式來讀取bean間依賴關係。

(1)在User類的nation的屬性上添加@Autowired

@Autowired
private Nation nation;
複製代碼

(2)配置文件中註冊bean

使用構造器方式注入age屬性,但沒有注入nation字段

<!-- 註解與xml混用方式,這裏並無注入nation,也沒有使用自動裝配 -->
<bean id="user5" class="com.example.springdemo.xml.model.User">
    <constructor-arg name="age" type="java.lang.Integer" value="30"></constructor-arg>
</bean>
複製代碼

(3)獲取bean

ApplicationContext applicationContext = new ClassPathXmlApplicationContext("application.xml");
// xml與註解混用,經過@Autowired注入nation bean(實際測試BeanFactory不支持註解,須要使用ApplicationContext)
User user7 = (User) applicationContext.getBean("user5");
System.out.println(user7.getAge());
System.out.println(user7.getNation().getName());
複製代碼

從輸出能夠發現age和nation屬性都被注入了。

3.4 註解

在3.3.2中介紹了註解的簡單使用,因爲註解已經成爲如今主流的使用方式,接下來詳細介紹些經常使用註解知識。

3.4.1 bean註冊

@Bean@Component等註解用於將對象註冊爲bean,交由Spring容器管理。

  • @Component註解在上使用,將類的定義與bean的聲明綁定,@Bean則將二者分離。通常@Component更適合聲明service、dao等單例對象,而@Bean更適合用於在不一樣場景下建立不一樣對象的狀況。
  • @Controller、@Service和@Repository屬於@Component的一種,通常分別用於controller層、service層和持久化層。我的理解@Service註解和@Component沒有什麼區別。
  • @Bean註解使用在方法上。

(1)@Bean

  • 可以使用@Bean註解註冊同一類的多個bean,該註解使用在方法上。
  • 可在被@Componet@Configuration註解聲明的類裏面使用聲明裏面使用;
  • 默認bean名爲首字母小寫的方法名,也可使用name字段設置bean名。
@Bean
// @Primary表示默認bean,當有多個相同類型的bean時,可使用@Primary來表示默認bean,其它地方注入依賴時就不須要都指明具體bean名。
@Primary
public Nation nation1() {
    return new Nation("China");
}

@Bean
public Nation nation2() {
    return new Nation("England");
}

// 使用name屬性爲bean命名
@Bean(name = "nation3")
public Nation nation() {
    return new Nation("America");
}
複製代碼

(2)@Primary

當有多個相同類型的bean時,可使用@Primary來表示默認bean,其它地方注入默認bean時就不須要指明具體bean名。

(3)@Value和@ConfigurationProperties

可使用@Value註解來注入外部屬性,而@ConfigurationProperties則可用來注入一整個對象的全部屬性,或者說批量注入外部屬性。

@Configuration
@PropertySource("classpath:application.properties")
public class JavaConfiguration {}
複製代碼
@Bean
// 使用@Value來注入外部屬性,須要使用@PropertySource引入資源文件。還可使用@ConfigurationProperties來批量注入外部屬性
public Nation nation4(@Value("${nation4.name:#{null}}") String nation4name) {
    return new Nation(nation4name);
}
複製代碼

3.4.2 bean注入

@Autowired@Required用來注入依賴的bean。

(1)@Autowired

  • @Autowired註解能夠在字段上使用,也能夠在setter方法上使用,同時能夠在構造器上使用。
  • @Autowired默認採用了自動裝配,且優先根據類型自動裝配,同類型則優先注入primary bean,而後再依據bean名(與屬性同名)進行注入。
  • @Autowired還能夠結合@Qualifier來指定要注入的bean名。例如:@Qualifier("nation2")。

(2)@Required

@Required註解也能夠用來注入依賴,但已經被廢棄。

(3)@Resource

Spring同時支持JSR-250的註解,一樣可使用@Resource來注入依賴(字段或者setter方法上),並使用name字段來指定bean名。例如:@Resource(name = "nation2")。

(4)@Inject

Spring3.0還支持JSR-330註解,可使用@Inject來注入依賴。

3.4.3 Java config

@Configuration註解代表該類是一個Java配置類,用於聲明bean,在該類中可使用@Bean註解來聲明bean。通常狀況下會將同一功能或用途的bean在一個配置累中聲明。

3.5 Bean做用域(scope)

Spring IOC中Bean有多種不一樣做用域,主要有:

  • singleton
  • prototype

接下來主要介紹下singleton和prototype做用域。

3.5.1 singleton模式

singleton模式(單例模式)是默認模式,該模式的bean有如下特色:

  • 容器中該bean只存在一個實例;
  • 實例存活時間:從容器啓動,到第一次因請求而實例化,只要容器不銷燬或退出,該實例一直存在。
代碼示例

(1)註冊bean 註冊一個bean名爲nation1的單例bean。

@Bean
public Nation nation1() {
    return new Nation("China");
}
複製代碼

(2)從容器獲取bean 兩次從容器中獲取同一個bean,是同一對象地址相同。

// 建立的nation1和nation2對象是相同bean
Nation nation1 = (Nation) applicationContext.getBean("nation1");
Nation nation2 = (Nation) applicationContext.getBean("nation1");
System.out.println("nation1 == nation2: " + (nation1 == nation2));
複製代碼

輸出:

3.5.2 prototype模式

prototype模式(原型模式、多例模式)的bean有如下特色:

  • 每次從容器中請求bean都會從新生成一個實例;
代碼示例

(1)註冊bean 註冊一個名爲nation7的多例bean。

@Bean
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public Nation nation7() {
    return new Nation();
}
複製代碼

(2)從容器獲取bean 兩次從容器獲取同一個bean,是不一樣的對象,地址不一樣。

// nation3和nation4是不一樣bean。
// 此處須要注意,若是bean類上使用了lombok註解,不要被輸出所迷惑,由於lombok重寫了toString(),使得二者看起來像是一個對象,實際應該比較對象地址
Nation nation3 = (Nation) applicationContext.getBean("nation7");
Nation nation4 = (Nation) applicationContext.getBean("nation7");
System.out.println("nation3 == nation4: " + (nation3 == nation4));
System.out.println("nation3:" + nation3);
System.out.println("nation4:" + nation4);
複製代碼

輸出:

3.6 循環依賴

未完待續。。。。。

小結

Spring IOC能夠說是Spring全家桶的基石,AOP等都依賴IOC的實現,要理解學習Spring應該先掌握Spring IOC的知識。
本文先是簡單介紹了Spring IOC的思想和一些基本概念,而後針對Spring IOC容器實現依賴注入相關的知識點,而且分別給出了xml形式和註解形式依賴注入代碼實現。

參考網址

https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html
https://juejin.im/post/5bf51d4c5188256d9832b0d3
https://segmentfault.com/a/1190000013700859
https://segmentfault.com/a/1190000014979704
https://juejin.im/post/5b399eb1e51d4553156c0525
複製代碼
相關文章
相關標籤/搜索