強推!Java大牛熬夜一周梳理的 Spring IOC筆記,收藏一波

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)。

好啦,今天的文章就到這里,希望能幫助到屏幕前迷茫的你們!

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容