什麼是 IOC
IOC(Inversion of Control)的意思是控制反轉,什麼意思呢?
意思就是反轉資源的獲取方向,傳統的方式是組件主動的從容器中獲取所需要的資源,在這樣的模式
下開發人員往往需要知道在具體容器中特定資源的獲取方式,增加了學習成本,同時降低了開發效率。
而應用了 IOC 之後, 則是容器主動地將資源推送給它所管理的組件, 開發人員不需要知道容器是如何創建資源對象的,只需要提供接收資源的方式即可,極大的降低了學習成本,提高了開發的效率。
我的理解就是沒用 IOC 之前,我們需要主動的 new 一個對象或者通過工廠,而 IOC 的意思就是,Spring 事先就幫我們創建好對象並放到容器中,當我們需要的時候,容器自動給我們提供。
舉一個通俗簡單的例子:平時我們如果想做飯,就需要自己去菜市場買菜,而 IOC 呢就是我們需要什麼菜,不用我們自己去買,菜市場的人會爲我們自動送來,我們所要做的就是用什麼來盛蔬菜。或者說我們都看過少林寺的電影,如果僧人想要喝水,就需要自己跑到山下去挑水,後來方丈弄了一個抽水泵,水直接從山底送到了寺院中,而僧人要做的就是用容器來裝水,用碗也好,用桶也好,用水缸也好。
IOC 的前生
需求: 生成 HTML 或 PDF 格式的不同類型的報表。
-
分離接口與實現
在 ReportService 中不僅需要知道接口與具體的實現類,還需要知道實現類的獲取方式,耦合很高。比如:ReportGenerator reportGenerator = new PdfReportGenerator();
或ReportGenerator reportGenerator = new HtmlReportGenerator();
舉個例子:比如在遠古社會,我們想要製作一把斧子,還要知道斧子的形狀,以及怎麼製作斧子。
-
採用工廠設計模式
採用工廠設計模式,我們只需要通過工廠就可以得到具體的實現類,不用關注實現類的具體實現細節,雖然這樣好很多,但是仍然要依賴工廠類,而且代碼比以前要複雜。到了封建社會,我們可以直接去鐵匠鋪,用銀子買一把斧子,不用知道製造斧子的流程。
-
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 實例工廠)
-
通過靜態工廠
通過靜態工廠創建 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>
-
通過實例工廠
通過實例工廠創建 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-filter
和 exclude-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 屬性來爲屬性賦值,而不用 property
、constructor-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 類型使用
<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-method
和 destroy-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 容器實現:
-
BeanFactory
IOC 容器的基本實現,是 Spring 內部的基礎設施,是面向 Spring 本身的,不是提供給開發人員使用的。 -
ApplicationContext
ApplicationContext 是 BeanFactory 的子接口,提供了更多的高級特性。
下面看一下類的層次結構:
|–BeanFactory:Spring
容器的頂層接口。
|----ApplicationContext
|------ConfigurableApplicationContext:是 ApplicationContext
的子接口,它新增了兩個方法 refresh()
和 close()
,使得 ApplicationContext
具有啓動、關閉和刷新上下文的能力。
|--------ClassPathXmlApplicationContext:從類路徑下加載配置文件。
|--------FileSystemXmlApplicationContext:從文件系統中加載配置文件。
|--------AnnotationConfigApplicationContext:用於純註解的方式,通過配置類獲取容器:ApplicationContext ac = new AnnotationConfigApplicationContext(SpringConfiguration.class);
|------WebApplicationContext:專門爲 Web 應用而準備的,它允許從相對於 Web 根目錄的路徑中完成初始化工作。
第二步:
接下來就是獲取 Bean 了。
我們可以調用 ApplicationContext
的 getBean()
方法獲取容器中的對象:
可以看到我們可以通過類型獲取,也可以通過 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