Hello,今天給各位童鞋們分享Spring IOC,趕緊拿出小本子記下來吧!

1. IoC原理
IoC全稱Inversion of Control,直譯為控制反轉(zhuǎn)。
為什么要使用IoC?
我們假定一個在線書店,通過BookService獲取書籍:
public class BookService {
? ? private HikariConfig config = new HikariConfig();
? ? private DataSource dataSource = new HikariDataSource(config);
? ? public Book getBook(long bookId) {
? ? ? ? try (Connection conn = dataSource.getConnection()) {
?...
? ? ? ? ? ? return book;
? ? ? ? }
? ? }
}
為了從數(shù)據(jù)庫查詢書籍,BookService持有一個DataSource。為了實例
HikariDataSource,又不得不實例化一個HikariConfig。現(xiàn)在,我們繼續(xù)編
UserService獲取用戶:
public class UserService {
? ? private HikariConfig config = new HikariConfig();
? ? private DataSource dataSource = new HikariDataSource(config);
? ? public User getUser(long userId) {
? ? ? ? try (Connection conn = dataSource.getConnection()) {
? ? ? ? ? ? ...
? ? ? ? ? ? return user;
? ? ? ? }
? ? }
}
因為UserService也需要訪問數(shù)據(jù)庫,因此,我們不得不也實例化一個HikariDataSource。
每一次調(diào)用方法, 都需要實例化一個HikariDataSource,容易造成資源浪費。如果用某種方法實現(xiàn)了共享資源,那么怎么確保在所有功能完整的情況下,銷毀以釋放資源呢?
因此,核心問題是:
誰負責創(chuàng)建組件?
誰負責根據(jù)依賴關系組裝組件?
銷毀時,如何按依賴順序正確銷毀?
解決這一問題的核心方案就是IoC。
傳統(tǒng)的應用程序中,**控制權在程序本身,**程序的控制流程完全由開發(fā)者控制,即在程序內(nèi)部進行實例化類。
而在IoC模式下,控制權發(fā)生了反轉(zhuǎn),即從應用程序轉(zhuǎn)移到了IoC容器,所有組件不再由應用程序自己創(chuàng)建和配置,而是由IoC容器負責,這樣,應用程序只需要直接使用已經(jīng)創(chuàng)建好并且配置好的組件。
為了能讓組件在IoC容器中被“裝配”出來,需要某種“注入”機制,例如,BookService自己并不會創(chuàng)建DataSource,而是等待外部通過setDataSource()方法來注入一個DataSource:
public class BookService {
? ? private DataSource dataSource;
? ? public void setDataSource(DataSource dataSource) {
? ? ? ? this.dataSource = dataSource;
? ? }
}
不直接new一個DataSource,而是注入一個DataSource,這個小小的改動雖然簡單,卻帶來了一系列好處:
BookService不再關心如何創(chuàng)建DataSource,因此,不必編寫讀取數(shù)據(jù)庫配置之類的代碼;
DataSource實例被注入到BookService,同樣也可以注入到UserService,因此,共享一個組件非常簡單;
測試BookService更容易,因為注入的是DataSource,可以使用內(nèi)存數(shù)據(jù)庫,而不是真實的MySQL配置。
因此,IoC又稱為依賴注入(DI:Dependency Injection),它解決了一個最主要的問題:將組件的創(chuàng)建+配置與組件的使用相分離,并且,由IoC容器負責管理組件的生命周期。
因為IoC容器要負責實例化所有的組件,因此,有必要告訴容器如何創(chuàng)建組件,以及各組件的依賴關系。一種最簡單的配置是通過XML文件來實現(xiàn),例如:
<beans>
? ? <bean id="dataSource" class="HikariDataSource" />
? ? <bean id="bookService" class="BookService">
? ? ? ? <property name="dataSource" ref="dataSource" />
? ? </bean>
? ? <bean id="userService" class="UserService">
? ? ? ? <property name="dataSource" ref="dataSource" />
? ? </bean>
</beans>
上述XML配置文件指示IoC容器創(chuàng)建3個JavaBean組件,并把id為dataSource的組件通過屬性dataSource(即調(diào)用setDataSource()方法)注入到另外兩個組件中。
在Spring的IoC容器中,我們把所有組件統(tǒng)稱為JavaBean,即配置一個組件就是配置一個Bean。
依賴注入(DI:Dependency Injection)方式
從上面的代碼我們可以得知,依賴注入可以通過set()方法實現(xiàn),但同時我們也可以通過構造方法來實現(xiàn):
//set()方法
public class BookService {
? ? private DataSource dataSource;
? ? public void setDataSource(DataSource dataSource) {
? ? ? ? this.dataSource = dataSource;
? ? }
}
//構造器方法
public class BookService {
? ? private DataSource dataSource;
? ? public BookService(DataSource dataSource) {
? ? ? ? this.dataSource = dataSource;
? ? }
}
無侵入容器
在設計上,Spring的IoC容器是一個高度可擴展的無侵入容器。所謂無侵入,是指應用程序的組件無需實現(xiàn)Spring的特定接口,或者說,組件根本不知道自己在Spring的容器中運行。這種無侵入的設計有以下好處:
應用程序組件既可以在Spring的IoC容器中運行,也可以自己編寫代碼自行組裝配置;
測試的時候并不依賴Spring容器,可單獨進行測試,大大提高了開發(fā)效率。
2. 裝配Bean組件
我們來看一個具體的用戶注冊登錄的例子。整個工程的結(jié)構如下:

我們先編寫一個MailService,用于在用戶登錄和注冊成功后發(fā)送郵件通知:
public class MailService {
? ? private ZoneId zoneId = ZoneId.systemDefault();
? ? public void setZoneId(ZoneId zoneId) {
? ? ? ? this.zoneId = zoneId;
? ? }
? ? public String getTime() {
? ? ? ? return ZonedDateTime.now(this.zoneId).format(DateTimeFormatter.ISO_ZONED_DATE_TIME);
? ? }
? ? public void sendLoginMail(User user) {
? ? ? ? System.err.println(String.format("Hi, %s! You are logged in at %s", user.getName(), getTime()));
? ? }
? ? public void sendRegistrationMail(User user) {
? ? ? ? System.err.println(String.format("Welcome, %s!", user.getName()));
? ? }
}
再編寫一個UserService,實現(xiàn)用戶注冊和登錄:

注意到UserService通過setMailService()注入了一個MailService。然后,我們需要編寫一個特定的application.xml配置文件,告訴Spring的IoC容器應該如何創(chuàng)建并組裝Bean:
<?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
? ? ? ? https://www.springframework.org/schema/beans/spring-beans.xsd">
? ? <bean id="userService" class="com.itranswarp.learnjava.service.UserService">
? ? ? ? <property name="mailService" ref="mailService" />
? ? </bean>
? ? <bean id="mailService" class="com.itranswarp.learnjava.service.MailService" />
</beans>
注意觀察上述配置文件,其中與XML Schema相關的部分格式是固定的,我們只關注兩個<bean ...>的配置:
每個<bean ...>都有一個id標識,相當于Bean的唯一ID;
在userServiceBean中,通過<property name="..." ref="..." />注入了另一個Bean;
Bean的順序不重要,Spring根據(jù)依賴關系會自動正確初始化。
最后一步,我們需要創(chuàng)建一個Spring的IoC容器實例,然后加載配置文件,讓Spring容器為我們創(chuàng)建并裝配好配置文件中指定的所有Bean,這只需要一行代碼:
ApplicationContextcontext=newClassPathXmlApplicationContext("application.xml");
接下來,我們就可以從Spring容器中“取出”裝配好的Bean然后使用它:
// 獲取Bean:
UserService userService = context.getBean(UserService.class);
// 正常調(diào)用:
User user = userService.login("bob@example.com", "password");
創(chuàng)建Spring IoC容器
ClassPathXmlApplicationContext(常用)
我們從創(chuàng)建Spring容器的代碼:
ApplicationContextcontext=newClassPathXmlApplicationContext("application.xml");
可以看到,Spring容器就是ApplicationContext,它是一個接口,有很多實現(xiàn)類,這里我們選擇ClassPathXmlApplicationContext,表示它會自動從classpath中查找指定的XML配置文件。
從ApplicationContext中我們可以根據(jù)Bean的ID獲取Bean,但更多的時候我們根據(jù)Bean的類型獲取Bean的引用:
UserService userService = context.getBean(UserService.class);
其中,userService為實例化的一個類,得到的userService可以調(diào)用類中的方法。
BeanFactory
Spring還提供另一種IoC容器叫BeanFactory,使用方式和ApplicationContext
類似:
BeanFactoryfactory=newXmlBeanFactory(newClassPathResource("application.xml"));
MailService mailService = factory.getBean(MailService.class);
BeanFactory和ApplicationContext的區(qū)別在于,BeanFactory的實現(xiàn)是按需創(chuàng)建,即第一次獲取Bean時才創(chuàng)建這個Bean,而ApplicationContext會一次性創(chuàng)建所有的Bean。實際上,ApplicationContext接口是從BeanFactory接口繼承而來的,并且,ApplicationContext提供了一些額外的功能,包括國際化支持、事件和通知機制等。通常情況下,我們總是使用ApplicationContext,很少會考慮使用BeanFactory。
3. 使用Annotation進行簡化配置
我們可以使用Annotation配置,可以完全不需要XML,讓Spring自動掃描Bean并組裝它們。
先刪除XML配置文件,然后,給UserService和MailService添加幾個注解。
首先,我們給MailService添加一個@Component注解:
@Component
public class MailService {
? ? ...
}
這個@Component注解就相當于定義了一個Bean,它有一個可選的名稱,默認是mailService,即小寫開頭的類名。
然后,我們給UserService添加一個@Component注解和一個@Autowired注解:
@Component
public class UserService {
? ? @Autowired
? ? MailService mailService;
? ? ...
}
使用@Autowired就相當于把指定類型的Bean注入到指定的字段中。此外,還可以直接寫在構造方法中:

最后,編寫一個AppConfig類啟動容器:

這里需要說明的是,
使用的實現(xiàn)類是AnnotationConfigApplicationContext,所以必須傳入一個標注了@Configuration的類名。
AppConfig還標注了@ComponentScan,它告訴容器,自動搜索當前類所在的包以及子包,把所有標注為@Component的Bean自動創(chuàng)建出來,并根據(jù)@Autowired進行裝配。
使用@ComponentScan非常方便,但是,我們也要特別注意包的層次結(jié)構。通常來說,啟動配置AppConfig位于自定義的頂層包,其他Bean按類別放入子包。
4. 定制Bean組件
Scope(@Scope(“prototype”))
Bean只需要一個實例:
對于Spring容器來說,當我們把一個Bean標記為@Component后,它就會自動為我們創(chuàng)建一個單例(Singleton),即容器初始化時創(chuàng)建Bean,容器關閉前銷毀Bean。在容器運行期間,我們調(diào)用getBean(Class)獲取到的Bean總是同一個實例。
需要不同實例:
還有一種Bean,我們每次調(diào)用getBean(Class),容器都返回一個新的實例,這種Bean稱為Prototype(原型),它的生命周期顯然和Singleton不同。聲明一個Prototype的Bean時,需要添加一個額外的@Scope注解:
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // @Scope("prototype")
public class MailSession {
? ? ...
}
注入List
有些時候,我們會有一系列接口相同,不同實現(xiàn)類的Bean。例如,注冊用戶時,我們要對email、password和name這3個變量進行驗證。為了便于擴展,我們先定義驗證接口:
public interface Validator {
? ? //定義方法
? ? void validate(String email, String password, String name);
}
然后,分別使用3個Validator對用戶參數(shù)進行驗證:

最后,我們通過一個Validators作為入口進行驗證:

注意到Validators被注入了一個List<Validator>,Spring會自動把所有類型為Validator的Bean裝配為一個List注入進來,這樣一來,我們每新增一個Validator類型,就自動被Spring裝配到Validators中了,非常方便。
可選注入(無指定Bean)
默認情況下,當我們標記了一個@Autowired后,Spring如果沒有找到對應類型的Bean,它會拋出NoSuchBeanDefinitionException異常。
可以給@Autowired增加一個required = false的參數(shù):
@Component
public class MailService {
? ? @Autowired(required = false)
? ? ZoneId zoneId = ZoneId.systemDefault();
? ? ...
}
這個參數(shù)告訴Spring容器,如果找到一個類型為ZoneId的Bean,就注入,如果找不到,就忽略。
這種方式非常適合有定義就使用定義,沒有就使用默認值的情況。
創(chuàng)建第三方Bean(不在包中的Bean)
如果一個Bean不在我們自己的package管理之內(nèi),例如ZoneId,如何創(chuàng)建它?
答案是我們自己在@Configuration類中編寫一個Java方法創(chuàng)建并返回它,注意給方法標記一個@Bean注解:
@Configuration
@ComponentScan
public class AppConfig {
? ? // 創(chuàng)建一個Bean:
? ? @Bean
? ? ZoneId createZoneId() {
? ? ? ? return ZoneId.of("Z");
? ? }
}
Spring對標記為@Bean的方法只調(diào)用一次,因此返回的Bean仍然是單例。(多次則@Bean(prototype))
初始化和銷毀
有些時候,一個Bean在注入必要的依賴后,需要進行初始化(監(jiān)聽消息等)。在容器關閉時,有時候還需要清理資源(關閉連接池等)。
在此之前,需要引入JSR-250定義的Annotation:
<dependency>
? ? <groupId>javax.annotation</groupId>
? ? <artifactId>javax.annotation-api</artifactId>
? ? <version>1.3.2</version>
</dependency>
在Bean的初始化和清理方法上標記@PostConstruct和@PreDestroy:

Spring容器會對上述Bean做如下初始化流程:
調(diào)用構造方法創(chuàng)建MailService實例;
根據(jù)@Autowired進行注入;
調(diào)用標記有@PostConstruct的init()方法進行初始化。
而銷毀時,容器會首先調(diào)用標記有@PreDestroy的shutdown()方法。
Spring只根據(jù)Annotation查找無參數(shù)方法,對方法名不作要求。
使用別名
當我們需要創(chuàng)建多個同類型的Bean時,我們就會用到別名:

可以用@Bean("name")指定別名,也可以用@Bean+@Qualifier("name")指定別名。
指定了別名后,注入時就需要指定Bean的名稱,不然會報錯:
@Component
public class MailService {
@Autowired(required = false)
@Qualifier("z") // 指定注入名稱為"z"的ZoneId
ZoneId zoneId = ZoneId.systemDefault();
? ? ...
}
或者指定默認Bean,當注入時沒有指定Bean的名字,則默認注入標記有@Primary的Bean:

使用FactoryBean(工廠模式)
用工廠模式創(chuàng)建Bean需要實現(xiàn)
FactoryBean接口。我們觀察下面的代碼:

當一個Bean實現(xiàn)了FactoryBean接口后,Spring會先實例化這個工廠,然后調(diào)用getObject()創(chuàng)建真正的Bean。getObjectType()可以指定創(chuàng)建的Bean的類型,因為指定類型不一定與實際類型一致,可以是接口或抽象類。
因此,如果定義了一個FactoryBean,要注意Spring創(chuàng)建的Bean實際上是這個FactoryBean的getObject()方法返回的Bean。為了和普通Bean區(qū)分,我們通常都以XxxFactoryBean命名。
5. 使用Resource讀取文件
在Java程序中,我們經(jīng)常會讀取配置文件、資源文件等。使用Spring容器時,我們也可以把“文件”注入進來,方便程序讀取。

上圖是工程的結(jié)構,我們需要讀取logo.txt文件,通常情況下,我們需要寫很多繁瑣的代碼,主要是為了定位文件,打開InputStream。Spring則提供了一個org.springframework.core.io.Resource,可以直接注入:

也可以直接指定文件的路徑,例如:
@Value("file:/path/to/logo.txt")
private Resource resource;
6. 注入配置(讀取配置文件)
@PropertySource注入配置
除了像Resource讀取文件那樣,Spring容器提供了一個更簡單的@PropertySource
來自動讀取配置文件。我們只需要在@Configuration配置類上再添加一個注解:

Spring容器看到@PropertySource("app.properties")注解后,自動讀取這個配置文件,然后,我們使用@Value正常注入:
@Value("${app.zone:Z}")
String zoneId;
注意注入的字符串語法,它的格式如下:
"${app.zone}"表示讀取key為app.zone的value,如果key不存在,
啟動將報錯;
"${app.zone:Z}"表示讀取key為app.zone的value,但如果key不存在,就使用默認值Z
還可以把注入的注解寫到方法參數(shù)中:
@BeanZoneId?
createZoneId(@Value("${app.zone:Z}") String zoneId) {
? ? return ZoneId.of(zoneId);
}
Bean中標記,需要注入的地方再標記
另一種注入配置的方式是先通過一個簡單的JavaBean持有所有的配置,例如,一個
SmtpConfig:

然后,在需要讀取的地方,使用#{smtpConfig.host}注入:

"#{smtpConfig.host}"的意思是,從名稱為smtpConfig的Bean讀取host屬性,即調(diào)用getHost()方法。
使用一個獨立的JavaBean持有所有屬性,然后在其他Bean中以#{bean.property}注入的好處是,多個Bean都可以引用同一個Bean的某個屬性。例如,如果SmtpConfig決定從數(shù)據(jù)庫中讀取相關配置項,那么MailService注入的@Value("#{smtpConfig.host}")仍然可以不修改正常運行。
7. 使用條件裝配
定義不同環(huán)境
Spring為應用程序準備了Profile這一概念,用來表示不同的環(huán)境。例如,我們分別定義開發(fā)、測試和生產(chǎn)這3個環(huán)境:
native
test
production
創(chuàng)建某個Bean時,Spring容器可以根據(jù)注解@Profile來決定是否創(chuàng)建。例如,以下配置:

如果當前的Profile設置為test,則Spring容器會調(diào)用createZoneIdForTest()創(chuàng)建ZoneId,否則,調(diào)用createZoneId()創(chuàng)建ZoneId。注意到@Profile("!test")表示非test環(huán)境。
在運行程序時,加上JVM參數(shù)-Dspring.profiles.active=test就可以指定以test環(huán)境啟動。
實際上,Spring允許指定多個Profile,例如:
-Dspring.profiles.active=test,master
可以表示test環(huán)境,并使用master分支代碼。
要滿足多個Profile條件,可以這樣寫:
@Bean
@Profile({ "test", "master" }) // 同時滿足test和master
ZoneId createZoneId() {
? ? ...
}
使用Conditional(條件注解)決定是否創(chuàng)建Bean
除了根據(jù)@Profile條件來決定是否創(chuàng)建某個Bean外,Spring還可以根據(jù)
@Conditional決定是否創(chuàng)建某個Bean。
例如,我們對SmtpMailService添加如下注解:
@Component
@Conditional(OnSmtpEnvCondition.class)
public class SmtpMailService implements MailService {
? ? ...
}
它的意思是,如果滿足OnSmtpEnvCondition的條件,才會創(chuàng)建SmtpMailService這個Bean。
public class OnSmtpEnvCondition implements Condition {
? ? public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
? ? ? ? return "true".equalsIgnoreCase(System.getenv("smtp"));
? ? }
}
Spring只提供了@Conditional注解,具體判斷邏輯還需要我們自己實現(xiàn)。
好啦,今天的文章就到這里,希望能幫助到屏幕前迷茫的你們!