Spring 核心 之 IOC

什麼是 IOC

IOC(Inversion of Control)的意思是控制反轉,什麼意思呢?

意思就是反轉資源的獲取方向,傳統的方式是組件主動的從容器中獲取所需要的資源,在這樣的模式
下開發人員往往需要知道在具體容器中特定資源的獲取方式,增加了學習成本,同時降低了開發效率。

而應用了 IOC 之後, 則是容器主動地將資源推送給它所管理的組件, 開發人員不需要知道容器是如何創建資源對象的,只需要提供接收資源的方式即可,極大的降低了學習成本,提高了開發的效率。

我的理解就是沒用 IOC 之前,我們需要主動的 new 一個對象或者通過工廠,而 IOC 的意思就是,Spring 事先就幫我們創建好對象並放到容器中,當我們需要的時候,容器自動給我們提供。

舉一個通俗簡單的例子:平時我們如果想做飯,就需要自己去菜市場買菜,而 IOC 呢就是我們需要什麼菜,不用我們自己去買,菜市場的人會爲我們自動送來,我們所要做的就是用什麼來盛蔬菜。或者說我們都看過少林寺的電影,如果僧人想要喝水,就需要自己跑到山下去挑水,後來方丈弄了一個抽水泵,水直接從山底送到了寺院中,而僧人要做的就是用容器來裝水,用碗也好,用桶也好,用水缸也好。
在這裏插入圖片描述

IOC 的前生

需求: 生成 HTML 或 PDF 格式的不同類型的報表。

  1. 分離接口與實現
    在這裏插入圖片描述
    在 ReportService 中不僅需要知道接口與具體的實現類,還需要知道實現類的獲取方式,耦合很高。比如:ReportGenerator reportGenerator = new PdfReportGenerator();ReportGenerator reportGenerator = new HtmlReportGenerator();

    舉個例子:比如在遠古社會,我們想要製作一把斧子,還要知道斧子的形狀,以及怎麼製作斧子。

  2. 採用工廠設計模式
    在這裏插入圖片描述
    採用工廠設計模式,我們只需要通過工廠就可以得到具體的實現類,不用關注實現類的具體實現細節,雖然這樣好很多,但是仍然要依賴工廠類,而且代碼比以前要複雜。

    到了封建社會,我們可以直接去鐵匠鋪,用銀子買一把斧子,不用知道製造斧子的流程。

  3. IOC 控制反轉
    在這裏插入圖片描述
    採用 IOC 之後,容器就會自動的將我們需要的實現類注入到 ReportService 中。

    而到了現代社會,你只要說我需要一把斧子,那麼就會有人自動把斧子送到你手上。

Spring 需要用到的 jar 包

  • spring-core
  • spring-beans:(依賴注入:XML 配置 bean)
  • spring-context:(註解方式配置 bean,掃描指定的類併爲添加註解的類創建對象放入容器中)
  • spring-aop:(aop 配置)
  • spring-expression:(解析 aop 的切面表達式)
  • commons-logging:(Spring 還需要依賴一個日誌包)

如何將 Bean 放入容器中

XML 配置

通過全類名(反射)

<bean id="helloWorld" class="com.spring.helloworld.HelloWorld">

</bean>

通過工廠方法(靜態工廠 or 實例工廠)

  1. 通過靜態工廠
    通過靜態工廠創建 bean,需要先創建一個工廠類,工廠類中寫一個靜態的方法,該方法返回一個對象。然後我們在配置文件中配置的時候,需要指明使用哪個工廠類的哪個方法來創建 bean。如果方法中需要參數,我們可以使用 <constrctor-arg> 元素爲該方法傳遞參數。

    <!-- 通過工廠方法的方式來配置 bean -->
    <!-- 1. 通過靜態工廠方法: 一個類中有一個靜態方法, 可以返回一個類的實例(瞭解) -->
    <!-- 在 class 中指定靜態工廠方法的全類名, 在 factory-method 中指定靜態工廠方法的方法名 -->
    <bean id="dateFormat" class="java.text.DateFormat" factory-method="getDateInstance">
        <!-- 可以通過 constructor-arg 子節點爲靜態工廠方法指定參數 -->
        <constructor-arg value="2"></constructor-arg>
    </bean>
    
  2. 通過實例工廠
    通過實例工廠創建 bean,和使用靜態工廠創建 bean 的唯一不同就是,需要先實例化該工廠,然後才能用該工廠創建 bean。

    <!-- 2. 實例工廠方法: 先需要創建工廠對象, 再調用工廠的非靜態方法返回實例(瞭解) -->
    <!-- ①. 創建工廠對應的 bean -->
    <bean id="simpleDateFormat" class="java.text.SimpleDateFormat">
    	<constructor-arg value="yyyy-MM-dd hh:mm:ss"></constructor-arg>
    </bean>
    
    <!-- ②. 有實例工廠方法來創建 bean 實例 -->
    <!-- factory-bean 指向工廠 bean, factory-method 指定工廠方法(瞭解) -->
    <bean id="datetime" factory-bean="simpleDateFormat" factory-method="parse">
    	<!-- 通過 constructor-arg 執行調用工廠方法需要傳入的參數 -->
    	<constructor-arg value="1990-12-12 12:12:12"></constructor-arg>
    </bean>
    

通過 FactoryBean

Factory Bean 創建的 bean,返回的是該工廠 getObject() 方法所返回的對象。我們要想使用 FactoryBean 創建 bean,必須要先創建一個類來實現 FactoryBean 接口。
在這裏插入圖片描述

public class UserBean implements FactoryBean<User>{
	/**
	 * 返回的 bean 的實例
	 */
	@Override
	public User getObject() throws Exception {
		User user = new User();
		user.setUserName("abc");
		user.setWifeName("ABC");
		
		List<Car> cars = new ArrayList<>();
		cars.add(new Car("ShangHai", "BuiKe", 180, 300000));
		cars.add(new Car("ShangHai", "CRUZE", 130, 150000));
		
		user.setCars(cars);
		return user;
	}

	/**
	 * 返回的 bean 的類型
	 */
	@Override
	public Class<?> getObjectType() {
		return User.class;
	}

	/**
	 * 返回的 bean 是否爲單例的
	 */
	@Override
	public boolean isSingleton() {
		return true;
	}
}
<!-- 配置通過 FactroyBean 的方式來創建 bean 的實例(瞭解) -->
<bean id="user" class="com.spring.ref.UserBean">

</bean>

當然 UserBean 也可以定義屬性,使用 標籤來爲屬性賦值。

註解方式

@Component:標識一個普通的組件。
@Controller:標識在 Controller 層。
@Service:標識在 Service 層。
@Respository:標識在 Dao 層。

默認情況下使用類名的首字母小寫作爲 id,也可以使用註解的 value 屬性,來指定 id。

使用了上述註解之後,還需要在配置文件中配置需要掃描的包,只有被掃描到的類纔會被注入到容器中。

使用 <context:component-scan> 標籤指定被掃描的包,前提是在配置文件中加入 context 命名空間

<context:component-scan base-package="com.vijay.component"/>

<context:component-scan> 詳細說明:
base-package:表示掃描該包及其子包下的所有類,如果要掃描多個包,中間用逗號分隔。
如果僅希望掃描特定的類而非所有類,則可以使用 resource-pattern 屬性來過濾:

<context:component-scan base-package="com.atguigu.component" resource-pattern="autowire/*.class"/>

還可以使用 <context:include-filter> 子標籤包含要掃描的目標類,和 <context:exclude-filter> 子標籤排除目標類。
<component-scan>下可以擁有若干個 include-filterexclude-filter 子節點。
注意: <context:include-filter>標籤要和 use-default-filters 屬性搭配使用,因爲 spring 使用的默認過濾器會掃描所有標有註解的類,我們需要將 use-default-filters 屬性設爲 false,<context:include-filter> 標籤纔會起作用。

過濾的表達式:

類別 示例 說明
annotation com.vijay.XxxAnnotation 過濾所有標註了XxxAnnotation的類。這個規則根據目標組件是否標註了指定類型的註解進行過濾。
assignable com.vijay.BaseXxx 過濾所有BaseXxx類的子類。這個規則根據目標組件是否是指定類型的子類的方式進行過濾。
aspectj com.vijay.*Service+ 所有類名是以Service結束的,或這樣的類的子類。這個規則根據AspectJ表達式進行過濾。
regex com.vijay.anno.* 所有com.atguigu.anno包下的類。這個規則根據正則表達式匹配到的類名進行過濾。
custom com.atguigu.XxxTypeFilter 使用XxxTypeFilter類通過編碼的方式自定義過濾規則。該類必須實現org.springframework.core.type.filter.TypeFilter接口

給 bean 的屬性賦值

如何給 bean 的屬性賦值

XML 配置

屬性注入

屬性注入就是利用 setter 方法注入。

<bean id="helloWorld" class="com.spring.helloworld.HelloWorld">
    <!-- 爲屬性賦值 -->
    <property name="user" value="Jerry"></property>
</bean>

其中 name 屬性的值對應的是 setUser() 方法中,set 之後且將一個字母小寫。比如如果是 setUserName(),則 name 對應的要寫 userName

構造器注入

構造器注入用的是 constructor-arg 標籤。

<bean id="helloWorld3" class="com.spring.helloworld.HelloWorld">
    <!-- 要求: 在 Bean 中必須有對應的構造器.  -->
    <constructor-arg value="Mike"></constructor-arg>
</bean>

默認對構造器中的參數是按順序注入,不過也可以不按順序,需要用 index 屬性指定參數的位置。

<bean id="car" class="com.spring.helloworld.Car">
    <constructor-arg value="KUGA" index="1"></constructor-arg>
    <constructor-arg value="ChangAnFord" index="0"></constructor-arg>
</bean>

如果有多個構造器,還可以利用 type 屬性規定參數的類型,以區分重載的構造器。

<bean id="book" class="com.atguigu.spring.bean.Book" >
    <constructor-arg value= "10010" index ="0" type="java.lang.Integer" />
    <constructor-arg value= "Book01" index ="1" type="java.lang.String" />
    <constructor-arg value= "Author01" index ="2" type="java.lang.String" />
    <constructor-arg value= "20.2" index ="3" type="java.lang.Double" />
</bean >

index 和 type 還可以混合使用:

<bean id="car" class="com.spring.helloworld.Car">
    <constructor-arg value="KUGA" index="1"></constructor-arg>
    <constructor-arg value="ChangAnFord" index="0"></constructor-arg>
    <constructor-arg value="250000" type="float"></constructor-arg>
</bean>

使用 p 命名空間

爲了簡化 bean 的配置,我們可以直接使用 p 屬性來爲屬性賦值,而不用 propertyconstructor-arg 標籤,不過使用 p 屬性,必須要引入 p 命名空間。

<bean id="studentSuper" class="com.helloworld.bean.Student" p:studentId="2002" p:stuName="Jerry2016" p:age="18" />

級聯屬性賦值

<bean id="action" class="com.spring.ref.Action">
    <property name="service" ref="service2"></property>
    <!-- 設置級聯屬性(瞭解) -->
    <property name="service.dao.dataSource" value="DBCP2"></property>
</bean>

級聯屬性什麼意思呢?就是我們不僅可以爲自己的屬性賦值,也可以爲依賴的其他 bean 的屬性賦值。但是需要注意的是,必須先要爲自己的屬性賦值,即初始化之後,才能爲依賴的 bean 屬性賦值。

以下情況會報異常:

<bean id="action" class="com.spring.ref.Action">
    <!-- 沒有爲 service 屬性賦值,直接爲級聯屬性賦值 -->
    <!-- 設置級聯屬性(瞭解) -->
    <property name="service.dao.dataSource" value="DBCP2"></property>
</bean>

註解方式

@Autowired

在指定了要掃描的包時,<context:component-scan> 元素會自動註冊一個 bean 的後置處理器:AutowiredAnnotationBeanPostProcessor 的實例。該後置處理器可以自動裝配標記了 @Autowired@Resource@Inject註解的屬性。

註解 說明
@Autowired 默認按類型裝配,如果容器中有多個相同類型的 bean,則會查看 id 是否有和屬性名相同的 bean,如果也沒有則會報異常。不過我們可以搭配 @Qualifier 註解根據 id指明要注入的 bean。 如果沒有合適的 bean,我們也可以指定 reqired 屬性爲 false,這樣也不會報異常。
@Resource @Resource註解要求提供一個bean名稱的屬性,若該屬性爲空,則自動採用標註處的變量或方法名作爲bean的名稱。
@Inject @Inject和@Autowired註解一樣也是按類型注入匹配的bean,但沒有reqired屬性。

可以給 bean 的屬性賦哪些值

字面值

字面值就是 基本數據類型、String 類型等,可以通過 value 屬性或 value 標籤賦值。

如果字面值中包含特殊字符,可以用 <![CDATA[]]> 將字面值包起來:

<bean id="car2" class="com.spring.helloworld.Car">
    <constructor-arg value="ChangAnMazda"></constructor-arg>
    <!-- 若字面值中包含特殊字符, 則可以使用 DCDATA 來進行賦值. (瞭解) -->
    <constructor-arg>
    <value><![CDATA[<ATARZA>]]></value>
    </constructor-arg>
    <constructor-arg value="180" type="int"></constructor-arg>
</bean>

null 值

我們也可以給屬性賦 null 值,不過沒有什麼實際用處,因爲我們不賦值,就默認是 null 值。

<bean id="dao2" class="com.spring.ref.Dao">
    <!-- 爲 Dao 的 dataSource 屬性賦值爲 null, 若某一個 bean 的屬性值不是 null, 使用時需要爲其設置爲 null(瞭解) -->
    <property name="dataSource"><null/></property>
</bean>

外部 bean

使用 ref 屬性,來指定 bean 之間的依賴關係。

<!-- 配置 bean -->
<bean id="dao5" class="com.spring.ref.Dao"></bean>

<bean id="service" class="com.atguigu.spring.ref.Service">
    <!-- 通過 ref 屬性值指定當前屬性指向哪一個 bean! -->
    <property name="dao" ref="dao5"></property>
</bean>

內部 bean

內部 bean 指直接聲明在 <property> 標籤或 <constructor-arg> 標籤內部的 bean,不用指定 id 屬性,而且只能在本實例中使用。別的 bean 無法引用。

<!-- 聲明使用內部 bean -->
<bean id="service2" class="com.spring.ref.Service">
    <property name="dao">
        <!-- 內部 bean, 類似於匿名內部類對象. 不能被外部的 bean 來引用, 也沒有必要設置 id 屬性 -->
        <bean class="com.spring.ref.Dao">
            <property name="dataSource" value="c3p0"></property>
        </bean>
    </property>
</bean>

集合屬性如何賦值

如果屬性的類型是集合類型,那麼該怎麼爲屬性賦值呢?在 Spring 中可以通過一組內置的 XM L標籤來配置集合屬性,例如:、或

數組和 List

如果屬性的類型是 List 類型,則需要用 標籤,在 標籤的內部,可以指定上述講到的四種類型:字面值、null 值、外部 bean、內部 bean,甚至內嵌其他集合。

<bean id="shop" class="com.spring.bean.Shop" >
    <property name= "categoryList">
        <!-- 以字面量爲值的List集合 -->
        <list>
            <value> 歷史</value >
            <value> 軍事</value >
        </list>
    </property>
    <property name= "bookList">
        <!-- 以bean的引用爲值的List集合 -->
        <list>
            <ref bean= "book01"/>
            <ref bean= "book02"/>
        </list>
    </property>
</bean >

Set

配置 Set 類型,需要用到 標籤,配置方式和 List 無異。

Map

Map 類型使用 標籤定義, 標籤中需要使用一個一個的 標籤定義數據,每條數據包含鍵和值。鍵值可以使用 key、value、key-ref、value-ref 等屬性來定義。

<bean id="cup" class="com.spring.bean.Cup">
    <property name="bookMap">
       <map>
           <entry key = "bookKey01" value-ref="book01"></entry>
           <entry key = "bookKey02" value-ref="book02"></entry>
       </map>
    </property>
</bean>

Properties

Properties 類型使用 標籤來定義,與 Map 類型相似,每個 標籤下用 子標籤來定義每條數據。

<bean id="dataSource" class="com.atguigu.spring.bean.DataSource">
        <property name="properties">
           <props>
               <prop key="userName">root</prop>
               <prop key="password">root</prop>
               <prop key="url">jdbc:mysql:///test</prop>
               <prop key="driverClass">com.mysql.jdbc.Driver</prop>
           </props>
        </property>
</bean>

將集合定義到 bean 的外部,以便供多個 bean 引用

如果想讓一個集合定義到 bean 的外部,可以使用 <util:> 標籤,前提是必須要先在配置文件頂部添加 util schema 定義。

<util:list id="bookList">
    <ref bean="book01"/>
    <ref bean="book02"/>
    <ref bean="book03"/>
    <ref bean="book04"/>
    <ref bean="book05"/>
</util:list>

<util:list id="categoryList">
    <value>編程</value>
    <value>極客</value>
    <value>相聲</value>
    <value>評書</value>
</util:list>

Bean 的高級配置

配置 Bean 的作用域

默認在配置文件中配置的 bean 都是單例的,即容器一初始化的時候,同時也初始化這些 bean。不過 bean 還可以配置成多例的,多例的 bean,是在使用的時候才初始化,每次使用 getBean()
方法都會創建一個新的實例。

我們可以使用 scope 屬性設置 bean 的作用域:

<!-- 默認情況下 bean 是單例的! -->
<!-- 但有的時候, bean 就不能使單例的. 例如: Struts2 的 Action 就不是單例的! 可以通過 scope 屬性來指定 bean 的作用域 -->
<!--  
    prototype: 原型的. 每次調用 getBean 方法都會返回一個新的 bean. 且在第一次調用 getBean 方法時才創建實例
    singleton: 單例的. 每次調用 getBean 方法都會返回同一個 bean. 且在 IOC 容器初始化時即創建 bean 的實例. 默認值 
-->
<bean id="dao2" class="com.spring.ref.Dao" scope="prototype"></bean>

在這裏插入圖片描述
單例的 bean 只要容器存在,則該實例就會一直存在,只有當容器被銷燬的時候,該實例纔會被銷燬。
多例的 bean 除非一直使用着纔會一直存在,如果長時間不用,且沒有別的對象引用,則就會被 Java 的垃圾回收器回收。

Bean 的生命週期

Bean 的生命週期分爲 5 步:
① 通過構造器或工廠方法創建 bean 實例
② 爲 bean 的屬性設置值和對其他 bean 的引用
③ 調用bean的初始化方法
④ bean 可以使用了
⑤ 當容器關閉時,調用 bean 的銷燬方法

在配置 bean 時,通過 init-methoddestroy-method 屬性爲 bean 指定初始化和銷燬方法。

bean 的後置處理器可以在調用初始化方法前後對 bean 進行額外的處理。需要注意的是:bean 後置處理器對 IOC 容器裏的所有 bean 實例逐一處理,而非單一實例。其典型應用是:檢查 bean 屬性的正確性或根據特定的標準更改 bean 的屬性。

bean 後置處理器時需要實現接口:org.springframework.beans.factory.config.BeanPostProcessor。在初始化方法被調用前後,Spring 將把每個 bean 實例分別傳遞給上述接口的以下兩個方法:

  • postProcessBeforeInitialization(Object, String)
  • postProcessAfterInitialization(Object, String)

添加了 bean 的後置處理器後 bean 的生命週期變爲:
① 通過構造器或工廠方法創建 bean 實例
② 爲 bean 的屬性設置值和對其他 bean 的引用
③ 將 bean 實例傳遞給 bean 後置處理器的 postProcessBeforeInitialization() 方法
④ 調用bean的初始化方法
⑤ 將 bean 實例傳遞給 bean 後置處理器的 postProcessAfterInitialization() 方法
⑥ bean 可以使用了
⑦ 當容器關閉時,調用 bean 的銷燬方法

引用外部屬性文件

當 bean 的配置信息逐漸增多時,查找和修改一些 bean 的配置信息就變得愈加困難。這時可以將一部分信息提取到 bean 配置文件的外部,以 properties 格式的屬性文件保存起來,同時在 bean 的配置文件中引用 properties 屬性文件中的內容,從而實現一部分屬性值在發生變化時僅修改 properties 屬性文件即可。
(1)創建 properties 屬性文件

prop.userName=root
prop.password=root
prop.url=jdbc:mysql:///test
prop.driverClass=com.mysql.jdbc.Driver

(2)引入 context 名稱空間
(3)指定 properties 屬性文件的位置

<!-- 指定properties屬性文件的位置 -->
<!-- classpath:xxx 表示屬性文件位於類路徑下 -->
<context:property-placeholder location="classpath:jdbc.properties"/>

(4)從 properties 屬性文件中引入屬性值

<!-- 從properties屬性文件中引入屬性值 -->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
    <property name="user" value="${prop.userName}"/>
    <property name="password" value="${prop.password}"/>
    <property name="jdbcUrl" value="${prop.url}"/>
    <property name="driverClass" value="${prop.driverClass}"/>
</bean>

獲取 Bean

第一步:

我們想要獲取 Bean 實例之前,必須要先對容器進行實例化。

Spring 提供了兩種 IOC 容器實現:

  1. BeanFactory
    IOC 容器的基本實現,是 Spring 內部的基礎設施,是面向 Spring 本身的,不是提供給開發人員使用的。

  2. ApplicationContext
    ApplicationContext 是 BeanFactory 的子接口,提供了更多的高級特性。

下面看一下類的層次結構:
在這裏插入圖片描述

|–BeanFactory:Spring 容器的頂層接口。
|----ApplicationContext
|------ConfigurableApplicationContext:是 ApplicationContext 的子接口,它新增了兩個方法 refresh()close(),使得 ApplicationContext 具有啓動、關閉和刷新上下文的能力。
|--------ClassPathXmlApplicationContext:從類路徑下加載配置文件。
|--------FileSystemXmlApplicationContext:從文件系統中加載配置文件。
|--------AnnotationConfigApplicationContext:用於純註解的方式,通過配置類獲取容器:ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
|------WebApplicationContext:專門爲 Web 應用而準備的,它允許從相對於 Web 根目錄的路徑中完成初始化工作。

第二步:

接下來就是獲取 Bean 了。

我們可以調用 ApplicationContextgetBean() 方法獲取容器中的對象:
在這裏插入圖片描述
可以看到我們可以通過類型獲取,也可以通過 Bean 的 id 獲取:

ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");  

HelloWorld helloWorld = ctx.getBean(HelloWorld. class);
// or
HelloWorld helloWorld = (HelloWorld)ctx.getBean("helloWorld");

不過需要注意的是,根據類型來獲取 bean,如果容器中有兩個相同類型的對象,則會報異常,因爲 Spring 容器不知道對應的是哪一個實例。

整合多個配置文件

Spring 允許通過 <import> 將多個配置文件引入到一個文件中,進行配置文件的集成。這樣在啓動 Spring 容器時,僅需要指定這個合併好的配置文件就可以。

import 元素的 resource屬 性支持 Spring 的標準的路徑資源:
在這裏插入圖片描述

泛型依賴注入

在這裏插入圖片描述
BaseService<T> 中引用了 BaseRepository<T>,如果容器中有 UserService extends BaseService<User>UserRepository extends BaseRepository<User> ,則 UserService 中會自動注入 UserRepository。即當子類的泛型類型相同時,Spring 會幫助我們自動注入。
(1)組件基類

public class BaseRepository<T> {
    public void save() {
       System.out.println("Saved by BaseRepository");
    }
}

public class BaseService<T> {
    @Autowired
    private BaseRepository<T> repository;
    
    public void add() {
       repository.save();
    }

(2)組件實體類

@Repository
public class UserRepository extends BaseRepository<User>{
    public void save() {
       System.out.println("Saved by UserRepository");
    }
}

@Service
public class UserService extends BaseService<User>{

}

(3)實體類

public class User {

}

(4)測試

ApplicationContext ioc = new ClassPathXmlApplicationContext("di.xml");

UserService us = (UserService) ioc.getBean("userService");

us.add();

// 輸出:Saved by UserRepository
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章