Spring可以說是框架學習中繞不過去的一座山,也是當前面試或者是工作中開發(fā)所必須要掌握和熟練的技術。本篇文章將對主要對Spring的IOC(控制反轉)和DI(依賴注入)概念進行介紹和演練,同時針對常用的標簽進行講解,希望對各位讀者有所幫助。
一、Spring的介紹
按照以往的風格,在學習一門新技術的時候,最好先了解一下這門技術的作用和起源。
先說說什么是Spring
Spring是分層的 Java SE/EE應用 full-stack 輕量級開源框架,以 IOC(Inverse Of Control:反轉控制)和AOP(Aspect Oriented Programming:面向切面編程)為內(nèi)核。
Spring提供了展現(xiàn)層 SpringMVC和持久層 Spring JDBCTemplate以及業(yè)務層事務管理等眾多的企業(yè)級應用技術,還能整合開源世界眾多著名的第三方框架和類庫,逐漸成為使用最多的Java EE 企業(yè)應用開源框架。
簡單來說,Spring是滿足Java SE/EE開發(fā)需求,簡化我們開發(fā)流程的框架,它除了自身體系中的Spring MVC和JDBC Template之外,還能很方便地整合其他第三方框架,使得我們的開發(fā)變得更加簡潔,能夠更加專注于業(yè)務邏輯的實現(xiàn)。
再說說Spring的發(fā)展歷程
自1997年IBM提出EJB的思想到2006年EJB3發(fā)布,EJB一直是遵循J2EE規(guī)范,被許多項目采用的解決方案之一。但是EJB的實現(xiàn)非常復雜,在業(yè)務代碼之外,需要生成大量的描述性代碼。這給開發(fā)效率帶來了很大的問題。同時EJB的測試和框架集成的工作也十分的麻煩。在2004年,Rod John(Spring 之父)在其《Expert One-to-One J2EE Development without EJB》作品中闡述了 J2EE 開發(fā)不使用 EJB的解決方式,這也就是Spring 雛形的由來。同年,Spring1.0誕生,而后經(jīng)過不斷完善,其功能和生態(tài)不斷發(fā)展增強,也越來越受到開發(fā)者的青睞。2017 年 09 月,Spring 5.0 發(fā)布。
本篇文章對Spring的描述也是基于5.0版本進行的。
二、Spring的優(yōu)勢和體系結構
(一)Spring的優(yōu)勢(看看就行)
1)方便解耦,簡化開發(fā)
通過 Spring 提供的 IoC容器,可以將對象間的依賴關系交由 Spring 進行控制,避免硬編碼所造成的過度耦合。
用戶也不必再為單例模式類、屬性文件解析等這些很底層的需求編寫代碼,可以更專注于上層的應用。
2)AOP 編程的支持
通過 Spring的 AOP 功能,方便進行面向切面編程,許多不容易用傳統(tǒng) OOP 實現(xiàn)的功能可以通過 AOP 輕松實現(xiàn)。
3)聲明式事務的支持
可以將我們從單調(diào)煩悶的事務管理代碼中解脫出來,通過聲明式方式靈活的進行事務管理,提高開發(fā)效率和質量。
4)方便程序的測試
可以用非容器依賴的編程方式進行幾乎所有的測試工作,測試不再是昂貴的操作,而是隨手可做的事情。
5)方便集成各種優(yōu)秀框架
Spring對各種優(yōu)秀框架(Struts、Hibernate、Hessian、Quartz等)的支持。
6)降低 JavaEE API 的使用難度
Spring對 JavaEE API(如 JDBC、JavaMail、遠程調(diào)用等)進行了薄薄的封裝層,使這些 API 的使用難度大為降低。
7)Java 源碼是經(jīng)典學習范例
Spring的源代碼設計精妙、結構清晰、匠心獨用,處處體現(xiàn)著大師對Java 設計模式靈活運用以及對 Java技術的高深造詣。它的源代碼無意是 Java 技術的最佳實踐的范例。
(二)Spring的體系結構
我們從下面的圖片可以看到,最底下的Test模塊表示spring框架的可測試性,我們主要需要關注的是倒數(shù)第二層的核心容器,Beans表示spring的bean管理,Core是spring的核心代碼,Context是spring容器的上下文,SpEL表示spring的表達式。自頂向上分別對應持久層和web層兩個模塊,持久層中有JDBC,事務等功能,Web模塊中包含了WebSocket和Servlet等功能。

三、Spring的IOC
(一)IOC的快速入門
IOC的英文全稱是inverse of controller,也就是中文的控制反轉。其實含義也很好理解,我們以前寫代碼時創(chuàng)建對象的方法,一般都是直接“new”對象出來使用,而控制反轉就是將對象的管理者從開發(fā)人員轉移到了Spring身上,由Spring幫我們管理我們的對象。
這樣做有什么好處呢?
最明顯的好處是降低了代碼間耦合度。我們可以看一下下面的代碼:
模擬controller處理用戶請求,調(diào)用service方法的過程
- Controller層
public class UserController {
// 模擬controller調(diào)用service
public static void main(String[] args) {
UserService userService = new UserServiceImpl();
userService.save();
}
}
- Service層(接口和實現(xiàn))
public interface UserService {
void save();
}
public class UserServiceImpl implements UserService {
public void save() {
UserDao userDao = new UserDaoImpl();
userDao.save();
}
}
- Dao層(接口和實現(xiàn))
public interface UserDao {
void save();
}
public class UserDaoImpl implements UserDao {
public void save() {
System.out.println("save方法執(zhí)行了...");
}
}
在上面的代碼中,我們自然可以利用多態(tài)的特性,使用"new"的方式創(chuàng)建service和dao接口的對應實現(xiàn)子類,來完成對應方法的調(diào)用。但這樣的話會存在一個問題:假如現(xiàn)在我們需要更換掉某一個接口的實現(xiàn)子類,我們就不得不在代碼中修改其實現(xiàn)子類,當需要替換的地方比較多時,我們就需要花費比較多的時間在修改代碼上面。
那么,有沒有更好的一種解決方式呢?
答案自然是有的,在原先對象的創(chuàng)建是通過我們手動new創(chuàng)建出來的,如果我們把(子類)對象的創(chuàng)建交由spring來控制,讓它來管理具體的實現(xiàn)類。
下面我們就來具體的進行實現(xiàn)吧:
步驟一:引入依賴
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.5.RELEASE</version>
</dependency>
步驟二:創(chuàng)建spring配置文件
spring配置文件的文件名可以自定義,但習慣性我們會將文件命名為applicationContext.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.xsd">
<!--配置userDao接口實現(xiàn)子類-->
<bean id="userDao" class="com.qiqv.dao.impl.UserDaoImpl"></bean>
<!--配置 userService 接口實現(xiàn)子類-->
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl"></bean>
</beans>
步驟三:將對象的創(chuàng)建改為從讀取配置文件中讀取
- Controller層
public class UserController {
// 模擬controller調(diào)用service
public static void main(String[] args) {
// 給定配置文件的路徑和文件名
ClassPathXmlApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService = (UserService) app.getBean("userService");
userService.save();
}
}
- Service層
public class UserServiceImpl implements UserService {
public void save() {
ClassPathXmlApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao = (UserDao) app.getBean("userDao");
userDao.save();
}
}
經(jīng)過這樣處理之后,如果后面的開發(fā)中需要更換接口的實現(xiàn)子類,我們只需要改一下配置文件即可。本質上spring的IOC做的工作就是對原有的邏輯進行了抽取和封裝而已。

(二)Bean標簽的使用
我們在上一小節(jié)的spring配置文件中,使用了<bean>標簽進行實現(xiàn)類的配置。默認情況下它調(diào)用的是類中的無參構造函數(shù),如果沒有無參構造函數(shù)則不能創(chuàng)建成功。
同時,id 屬性表示Bean實例在Spring容器中的唯一標識(id屬性具有唯一性);class屬性表示id標識具體對應的實現(xiàn)類。
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl"></bean>
作用范圍
spring不僅能夠幫助我們管理對象的創(chuàng)建,還可以控制對象的作用范圍。
| 取值范圍 | 說明 |
|---|---|
| singleton | 單例 |
| prototype | 多例的 |
| request | WEB 項目中,Spring 創(chuàng)建一個 Bean 的對象,將對象存入到 request 域中 |
| session | WEB 項目中,Spring 創(chuàng)建一個 Bean 的對象,將對象存入到 session 域中 |
| global session | WEB 項目中,應用在 Portlet 環(huán)境,如果沒有 Portlet 環(huán)境那么globalSession 相當于 session |
關于單例和多例我們很好理解,二者的區(qū)別在于前者只會創(chuàng)建一個實例對象,后者會每次調(diào)用時都會創(chuàng)建一個實例對象。我們可以用下面的代碼來簡單的進行演示:
- 配置對象作用范圍
<!--配置userDao接口實現(xiàn)子類-->
<bean id="userDao" class="com.qiqv.dao.impl.UserDaoImpl" scope="singleton"></bean>
<!--配置 userService 接口實現(xiàn)子類-->
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" scope="prototype"></bean>
- 編寫測試代碼
public static void scopeTest(){
ClassPathXmlApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
UserService userService1 = (UserService) app.getBean("userService");
UserService userService2 = (UserService) app.getBean("userService");
UserDao userDao1 = (UserDao) app.getBean("userDao");
UserDao userDao2 = (UserDao) app.getBean("userDao");
System.out.println("userService1:" + userService1);
System.out.println("userService2:" + userService2);
System.out.println("userDao1:" + userDao1);
System.out.println("userDao2:" + userDao2);
}

除了實例化個數(shù)的區(qū)別外,不同的作用范圍對應的實例化時機和對應bean的生命周期。

我們還可以通過設置對象的初始化和銷毀方法,來滿足特定場景下的需求:
init-method:初始化對象時執(zhí)行的方法
destroy-method銷毀對象時執(zhí)行的方法
比如我們給userDaoImpl設置對應的初始化和銷毀方法
- 在spring配置文件配置bean的對應屬性
<!--配置userDao接口實現(xiàn)子類-->
<bean id="userDao" class="com.qiqv.dao.impl.UserDaoImpl" scope="singleton" init-method="initMethod" destroy-method="destroyMethod" ></bean>
- 在類中定義具體的初始化和銷毀方法
public class UserDaoImpl implements UserDao {
public void save() {
System.out.println("save方法執(zhí)行了...");
}
public void initMethod(){
System.out.println("userServiceImpl初始化了...");
}
public void destroyMethod(){
System.out.println("userServiceImpl已經(jīng)銷毀了...");
}
}
- 在方法中進行調(diào)用
public class SpringTest {
public static void main(String[] args) {
ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
UserDao userDao = (UserDao) app.getBean("userDao");
userDao.save();
((ClassPathXmlApplicationContext)app).close();
}
}

Bean實例化的三種方式
(1)無參構造方法實例化
在之前的學習中我們知道,默認情況下spring是通過無參構造方法來創(chuàng)建對象的,但如果bean中沒有默認無參構造函數(shù),將會創(chuàng)建失敗
<!--配置userDao接口實現(xiàn)子類-->
<bean id="userDao" class="com.qiqv.dao.impl.UserDaoImpl"></bean>
(2)工廠靜態(tài)方法實例化
- 創(chuàng)建工廠類
public class StaticFactoryBean {
public static UserDao createUserDao(){
return new UserDaoImpl();
}
}
- spring配置文件
<!--配置userDao接口實現(xiàn)子類-->
<bean id="userDao" class="com.qiqv.dao.UserDaoFactoryBean" factory-method="getUserDaoInstance"></bean>
(3)工廠實例方法實例化
工廠的非靜態(tài)方法返回Bean實例
- 創(chuàng)建工廠類實例方法
public class UserDaoFactoryBean {
public UserDao getUserDao(){
return new UserDaoImpl();
}
}
- 將工廠類配置為bean實例創(chuàng)建方式
<!--工廠類-->
<bean id="userFactory" class="com.qiqv.dao.UserDaoFactoryBean"></bean>
<!--配置userDao接口實現(xiàn)子類-->
<bean id="userDao" factory-bean="userFactory" factory-method="getUserDao" ></bean>
五、Spring的DI(依賴注入)講解
依賴注入(Dependency Injection):它是 Spring 框架核心 IOC 的具體實現(xiàn)。
在上一節(jié)中有過這么一個案例,我們在配置文件中配置了userDao和userService對應的實現(xiàn)類,然后在每次調(diào)用的時候創(chuàng)建applicationContext上下文對象來獲取對應的實例。創(chuàng)建多次上下文對象是一種浪費資源的做法,我們在調(diào)用某個方法時,我們就知道了具體要獲取哪些實現(xiàn)類,只是因為分層的存在使得獲取實現(xiàn)的動作變得不連貫。在這種場景下我們自然會希望有一種方法可以在調(diào)用某個方法的時候一次性將所有需要的bean給創(chuàng)建出來,這就是Spring的依賴注入的由來。
(一)Spring依賴注入的方式
(1)構造方法注入
- 在配置文件中使用
constructor-arg標簽進行依賴注入
<bean id="userDao" class="com.qiqv.dao.impl.UserDaoImpl"></bean>
<!--配置 userService 接口實現(xiàn)子類-->
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" >
<constructor-arg name="userDao" ref="userDao"></constructor-arg>
</bean>
- 在實現(xiàn)類中定義變量和構造方法
public class UserServiceImpl implements UserService {
private UserDao userDao;
public UserServiceImpl(UserDao userDao) {
this.userDao = userDao;
}
public void save() {
userDao.save();
}
}
有了這種方式后,以后我們就不需要每次獲取對象都創(chuàng)建applicationContext上下文對象了。
(2) set方法注入
- 在配置文件中使用
property標簽進行依賴注入
<bean id="userDao" class="com.qiqv.dao.impl.UserDaoImpl"></bean>
<!--配置 userService 接口實現(xiàn)子類-->
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" >
<!-- name表示實體類中屬性的名稱,ref表示配置文件中具體實現(xiàn)類的id標識 -->
<property name="userDao" ref="userDao"></property>
</bean>
- 在實現(xiàn)類中定義變量和set方法
public class UserServiceImpl implements UserService {
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
public void save() {
userDao.save();
}
}
(3) p名稱空間注入(比較少用,了解即可)
P命名空間注入本質也是set方法注入,但比起上述的set方法注入更加方便,主要體現(xiàn)在配置文件中,如下: 首先,需要引入P命名空間:
xmlns:p="http://www.springframework.org/schema/p"
然后將注入方式從property標簽改為p屬性注入
<bean id="userDao" class="com.qiqv.dao.impl.UserDaoImpl"></bean>
<!--配置 userService 接口實現(xiàn)子類-->
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" p:userDao-ref="userDao" ></bean>
具體的UserServiceImpl類還是需要擁有userDao屬性的set方法,這里就不再展示。
(二)Spring依賴注入的數(shù)據(jù)類型
上面的操作,都是注入的引用Bean,處了對象的引用可以注入,普通數(shù)據(jù)類型,集合等都可以在容器中進行注入。
(1)普通數(shù)據(jù)類型的引入
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" p:userDao-ref="userDao" >
<property name="age" value="15"></property>
<property name="name" value="xiaoming"></property>
</bean>
(2)集合數(shù)據(jù)類型的引入
List<String>
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" p:userDao-ref="userDao" >
<property name="strList" >
<list>
<value>xiaoming</value>
<value>xiaohong</value>
<value>xiaolan</value>
</list>
</property>
</bean>
List<User>
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" p:userDao-ref="userDao" >
<property name="userList">
<list>
<ref bean="user1"></ref>
<ref bean="user2"></ref>
</list>
</property>
</bean>
<bean id="user1" class="com.qiqv.pojo.User">
<property name="name" value="xiaoming"></property>
<property name="age" value="15"></property>
</bean>
<bean id="user2" class="com.qiqv.pojo.User">
<property name="name" value="xiaohong"></property>
<property name="age" value="41"></property>
</bean>
Map<String,User>
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" p:userDao-ref="userDao" >
<property name="userMap">
<map>
<entry key="user1" value-ref="user1"></entry>
<entry key="user2" value-ref="user2"></entry>
</map>
</property>
</bean>
<bean id="user1" class="com.qiqv.pojo.User">
<property name="name" value="xiaoming"></property>
<property name="age" value="15"></property>
</bean>
<bean id="user2" class="com.qiqv.pojo.User">
<property name="name" value="xiaohong"></property>
<property name="age" value="41"></property>
</bean>
Properties
<bean id="userService" class="com.qiqv.service.impl.UserServiceImpl" p:userDao-ref="userDao" >
<property name="userMap">
<props>
<prop key="name1">xiaoming</prop>
<prop key="name2">xiaohong</prop>
<prop key="name3">xiaolan</prop>
</props>
</property>
</bean>
六、引入其他配置文件
實際開發(fā)中,Spring的配置內(nèi)容非常多,這就導致Spring配置很繁雜且體積很大,所以,可以將部分配置拆解到其他配置文件中,而在Spring主配置文件通過import標簽進行加載
<import resource="applicationContext-xxx.xml"/
七、ApplicationContext繼承體系的介紹
在前面的代碼演示中,我們使用ApplicationContext接口的實現(xiàn)類來作為加載spring配置文件的啟動類,那么該接口所處的繼承體系到底是怎么樣的呢?我們可以參考一下下面的圖片:

對于
ApplicationContext接口而言,其主要的實現(xiàn)類有以下三個:
(1)ClassPathXmlApplicationContext
它是從類的根路徑下加載配置文件 推薦使用這種
(2)FileSystemXmlApplicationContext
它是從磁盤路徑上加載配置文件,配置文件可以在磁盤的任意位置。
(3)AnnotationConfigApplicationContext
當使用注解配置容器對象時,需要使用此類來創(chuàng)建 spring 容器。它用來讀取注解。
八、Properties標簽的使用
我們知道,使用原始JDBC的API進行操作的話,我們需要在代碼中對drivername、username等信息進行手動配置,但這種硬編碼在之后需要修改配置的時候會比較麻煩,所以演變到后面的封裝到.properties文件中。這樣我們就可以將JBDC配置寫在配置文件中,但這樣還不夠簡便,我們還是需要通過getProperty的方式對參數(shù)進行封裝,通過封裝得到數(shù)據(jù)庫連接對象Connection,當然了更多時候我們都是將參數(shù)封裝到連接池中。
但現(xiàn)在有了spring之后,這部分的工作就可以交由spring來完成:
- 引入數(shù)據(jù)庫驅動依賴和C3P0連接池依賴
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.21</version>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
- 配置spring核心配置文件
applicationContext.xml
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test"></property>
<property name="user" value="root"></property>
<property name="password" value="root"></property>
</bean>
- 在代碼中進行測試
public class SpringTest {
public static void main(String[] args) {
ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
DataSource dataSource = (DataSource) app.getBean("dataSource");
System.out.println(dataSource);
}
}
我們雖然通過配置的方式將JDBC的參數(shù)寫在了配置文件中,但我們習慣上為了解耦,還是會將這些數(shù)據(jù)庫連接參數(shù)另外封裝到一個jdbc.properties文件中,這時候我們就可以使用context:property-placeholder標簽來導入properties文件中的參數(shù)到spring配置文件中。
首先,需要引入context命名空間和約束路徑:
命名空間:
xmlns:context="http://www.springframework.org/schema/context"
約束路徑:
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
然后在核心配置文件中導入和使用我們的配置,我們可以在property標簽中使用${ }的方式來讀取property文件key對應的value值。
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${user.driverClass}"></property>
<property name="jdbcUrl" value="${user.jdbcUrl}"></property>
<property name="user" value="${user.user}"></property>
<property name="password" value="${user.password}"></property>
</bean>
說在最后
實際上,在了解IOC和DI的作用之后,掌握并快速上手并不難,對于程序員來說學習更重要的意義在于知道這門技術存在的背景和解決問題的思想,知曉這門技術的優(yōu)劣,這樣對自己的技術成長可能更有幫助。
參考資料
Spring誕生前夕的世界:
http://www.itdecent.cn/p/5783dc936516