面試必問的 Spring,你懂了嗎?
前言
之前在?4 年 Java 經(jīng)驗面試總結(jié)、心得體會?中列出了一些高頻面試題,這些題目大部分是我自己在面試中碰到過的、小部分是我覺得比較重要的,但是當時并沒有給出答案,后面收到有不少同學留言說希望給出答案。之前一直比較忙,所以沒時間出。
最近換了工作后,沒之前壓力那么大了,所以想把這個之前留下的坑給填上。之后會針對這些題目按專題來給出對應的解析,同時結(jié)合當前的面試環(huán)境,適當補充點當前熱門題目。

另外,針對這些面試題的重要程度/出現(xiàn)頻率,我會給出一個評分(1~10),分值越高代表出現(xiàn)的概率越大,其中8分及以上的可以認為是高頻面試題,評分僅供參考。
針對 Spring 這個知識點,面試中的重要程度綜合評分為8分。
正文
Spring IoC 的容器構建流程(8分)
核心的構建流程如下,也就是 refresh 方法的核心內(nèi)容:

Spring bean 的生命周期(10分)
bean 的生命周期主要有以下幾個階段,深色底的5個是比較重要的階段。

BeanFactory?和?FactoryBean 的區(qū)別(6分)
BeanFactory:Spring 容器最核心也是最基礎的接口,本質(zhì)是個工廠類,用于管理 bean 的工廠,最核心的功能是加載 bean,也就是 getBean 方法,通常我們不會直接使用該接口,而是使用其子接口。
FactoryBean:該接口以 bean 樣式定義,但是它不是一種普通的 bean,它是個工廠 bean,實現(xiàn)該接口的類可以自己定義要創(chuàng)建的 bean 實例,只需要實現(xiàn)它的 getObject 方法即可。
FactoryBean 被廣泛應用于 Java 相關的中間件中,如果你看過一些中間件的源碼,一定會看到 FactoryBean 的身影。
一般來說,都是通過?FactoryBean#getObject?來返回一個代理類,當我們觸發(fā)調(diào)用時,會走到代理類中,從而可以在代理類中實現(xiàn)中間件的自定義邏輯,比如:RPC 最核心的幾個功能,選址、建立連接、遠程調(diào)用,還有一些自定義的監(jiān)控、限流等等。
BeanFactory?和?ApplicationContext 的區(qū)別(6分)
BeanFactory:基礎 IoC 容器,提供完整的 IoC 服務支持。
ApplicationContext:高級 IoC 容器,BeanFactory 的子接口,在 BeanFactory 的基礎上進行擴展。包含 BeanFactory 的所有功能,還提供了其他高級的特性,比如:事件發(fā)布、國際化信息支持、統(tǒng)一資源加載策略等。正常情況下,我們都是使用的 ApplicationContext。

?
這邊以電話來舉個簡單的例子:
我們家里使用的 “座機”?就類似于 BeanFactory,可以進行電話通訊,滿足了最基本的需求。
而現(xiàn)在非常普及的智能手機,iPhone、小米等,就類似于 ApplicationContext,除了能進行電話通訊,還有其他很多功能:拍照、地圖導航、聽歌等。
Spring 的 AOP 是怎么實現(xiàn)的(5分)
本質(zhì)是通過動態(tài)代理來實現(xiàn)的,主要有以下幾個步驟。
1、獲取增強器,例如被 Aspect 注解修飾的類。
2、在創(chuàng)建每一個 bean 時,會檢查是否有增強器能應用于這個 bean,簡單理解就是該 bean 是否在該增強器指定的?execution 表達式中。如果是,則將增強器作為攔截器參數(shù),使用動態(tài)代理創(chuàng)建 bean 的代理對象實例。
3、當我們調(diào)用被增強過的 bean?時,就會走到代理類中,從而可以觸發(fā)增強器,本質(zhì)跟攔截器類似。
多個AOP的順序怎么定(6分)
通過 Ordered 和 PriorityOrdered 接口進行排序。PriorityOrdered?接口的優(yōu)先級比 Ordered?更高,如果同時實現(xiàn)?PriorityOrdered?或?Ordered?接口,則再按 order 值排序,值越小的優(yōu)先級越高。
Spring?的?AOP 有哪幾種創(chuàng)建代理的方式(9分)
Spring 中的 AOP 目前支持?JDK 動態(tài)代理和 Cglib 代理。
通常來說:如果被代理對象實現(xiàn)了接口,則使用 JDK 動態(tài)代理,否則使用 Cglib 代理。另外,也可以通過指定 proxyTargetClass=true 來實現(xiàn)強制走 Cglib 代理。
JDK 動態(tài)代理和 Cglib 代理的區(qū)別(9分)
1、JDK 動態(tài)代理本質(zhì)上是實現(xiàn)了被代理對象的接口,而 Cglib 本質(zhì)上是繼承了被代理對象,覆蓋其中的方法。
2、JDK 動態(tài)代理只能對實現(xiàn)了接口的類生成代理,Cglib 則沒有這個限制。但是 Cglib 因為使用繼承實現(xiàn),所以 Cglib 無法代理被 final 修飾的方法或類。
3、在調(diào)用代理方法上,JDK 是通過反射機制調(diào)用,Cglib是通過FastClass 機制直接調(diào)用。FastClass 簡單的理解,就是使用 index 作為入?yún)?,可以直接定位到要調(diào)用的方法直接進行調(diào)用。
4、在性能上,JDK1.7 之前,由于使用了 FastClass 機制,Cglib 在執(zhí)行效率上比 JDK 快,但是隨著 JDK 動態(tài)代理的不斷優(yōu)化,從 JDK 1.7 開始,JDK 動態(tài)代理已經(jīng)明顯比 Cglib 更快了。
JDK 動態(tài)代理為什么只能對實現(xiàn)了接口的類生成代理(7分)
根本原因是通過 JDK 動態(tài)代理生成的類已經(jīng)繼承了 Proxy 類,所以無法再使用繼承的方式去對類實現(xiàn)代理。
Spring?的事務傳播行為有哪些(6分)
1、REQUIRED:Spring 默認的事務傳播級別,如果上下文中已經(jīng)存在事務,那么就加入到事務中執(zhí)行,如果當前上下文中不存在事務,則新建事務執(zhí)行。
2)REQUIRES_NEW:每次都會新建一個事務,如果上下文中有事務,則將上下文的事務掛起,當新建事務執(zhí)行完成以后,上下文事務再恢復執(zhí)行。
3)SUPPORTS:如果上下文存在事務,則加入到事務執(zhí)行,如果沒有事務,則使用非事務的方式執(zhí)行。
4)MANDATORY:上下文中必須要存在事務,否則就會拋出異常。
5)NOT_SUPPORTED :如果上下文中存在事務,則掛起事務,執(zhí)行當前邏輯,結(jié)束后恢復上下文的事務。
6)NEVER:上下文中不能存在事務,否則就會拋出異常。
7)NESTED:嵌套事務。如果上下文中存在事務,則嵌套事務執(zhí)行,如果不存在事務,則新建事務。
Spring 的事務隔離級別底層其實是基于數(shù)據(jù)庫的,Spring 并沒有自己的一套隔離級別。
DEFAULT:使用數(shù)據(jù)庫的默認隔離級別。
Spring 的事務隔離級別是如何做到和數(shù)據(jù)庫不一致的?(5分)
比如數(shù)據(jù)庫是可重復讀,Spring 是讀已提交,這是怎么實現(xiàn)的?
Spring 的事務隔離級別本質(zhì)上還是通過數(shù)據(jù)庫來控制的,具體是在執(zhí)行事務前先執(zhí)行命令修改數(shù)據(jù)庫隔離級別,命令格式如下:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTEDSpring 事務的實現(xiàn)原理(8分)
Spring 事務的底層實現(xiàn)主要使用的技術:AOP(動態(tài)代理) + ThreadLocal + try/catch。
動態(tài)代理:基本所有要進行邏輯增強的地方都會用到動態(tài)代理,AOP 底層也是通過動態(tài)代理實現(xiàn)。
ThreadLocal:主要用于線程間的資源隔離,以此實現(xiàn)不同線程可以使用不同的數(shù)據(jù)源、隔離級別等等。
try/catch:最終是執(zhí)行 commit 還是 rollback,是根據(jù)業(yè)務邏輯處理是否拋出異常來決定。
Spring 事務的核心邏輯偽代碼如下:
public void invokeWithinTransaction() {// 1.事務資源準備try {// 2.業(yè)務邏輯處理,也就是調(diào)用被代理的方法} catch (Exception e) {// 3.出現(xiàn)異常,進行回滾并將異常拋出} finally {????????//?現(xiàn)場還原:還原舊的事務信息}// 4.正常執(zhí)行,進行事務的提交// 返回業(yè)務邏輯處理結(jié)果}
詳細流程如下圖所示:

Spring 怎么解決循環(huán)依賴的問題(9分)
Spring 是通過提前暴露 bean 的引用來解決的,具體如下。
Spring 首先使用構造函數(shù)創(chuàng)建一個 “不完整”?的 bean 實例(之所以說不完整,是因為此時該 bean 實例還未初始化),并且提前曝光該 bean 實例的 ObjectFactory(提前曝光就是將 ObjectFactory 放到 singletonFactories 緩存).
通過 ObjectFactory 我們可以拿到該 bean 實例的引用,如果出現(xiàn)循環(huán)引用,我們可以通過緩存中的 ObjectFactory 來拿到 bean 實例,從而避免出現(xiàn)循環(huán)引用導致的死循環(huán)。
舉個例子:A 依賴了 B,B 也依賴了 A,那么依賴注入過程如下。
檢查 A 是否在緩存中,發(fā)現(xiàn)不存在,進行實例化
通過構造函數(shù)創(chuàng)建 bean A,并通過 ObjectFactory 提前曝光?bean A
A 走到屬性填充階段,發(fā)現(xiàn)依賴了 B,所以開始實例化 B。
首先檢查 B 是否在緩存中,發(fā)現(xiàn)不存在,進行實例化
通過構造函數(shù)創(chuàng)建 bean B,并通過 ObjectFactory 曝光創(chuàng)建的 bean B
B 走到屬性填充階段,發(fā)現(xiàn)依賴了 A,所以開始實例化 A。
檢查?A?是否在緩存中,發(fā)現(xiàn)存在,拿到?A 對應的?ObjectFactory?來獲得 bean A,并返回。
B 繼續(xù)接下來的流程,直至創(chuàng)建完畢,然后返回 A 的創(chuàng)建流程,A 同樣繼續(xù)接下來的流程,直至創(chuàng)建完畢。
這邊通過緩存中的 ObjectFactory 拿到的 bean 實例雖然拿到的是 “不完整”?的 bean 實例,但是由于是單例,所以后續(xù)初始化完成后,該 bean 實例的引用地址并不會變,所以最終我們看到的還是完整 bean 實例。
Spring 能解決構造函數(shù)循環(huán)依賴嗎(6分)
答案是不行的,對于使用構造函數(shù)注入產(chǎn)生的循環(huán)依賴,Spring 會直接拋異常。
為什么無法解決構造函數(shù)循環(huán)依賴?
上面解決邏輯的第一句話:“首先使用構造函數(shù)創(chuàng)建一個 “不完整”?的 bean 實例”,從這句話可以看出,構造函數(shù)循環(huán)依賴是無法解決的,因為當構造函數(shù)出現(xiàn)循環(huán)依賴,我們連 “不完整”?的 bean 實例都構建不出來。
Spring 三級緩存(6分)
Spring 的三級緩存其實就是解決循環(huán)依賴時所用到的三個緩存。
singletonObjects:正常情況下的 bean 被創(chuàng)建完畢后會被放到該緩存,key:beanName,value:bean 實例。
singletonFactories:上面說的提前曝光的?ObjectFactory 就會被放到該緩存中,key:beanName,value:ObjectFactory。
earlySingletonObjects:該緩存用于存放?ObjectFactory 返回的 bean,也就是說對于一個 bean,ObjectFactory 只會被用一次,之后就通過?earlySingletonObjects 來獲取,key:beanName,早期 bean 實例。@Resource 和 @Autowire 的區(qū)別(7分)
1、@Resource 和 @Autowired 都可以用來裝配?bean
2、@Autowired 默認按類型裝配,默認情況下必須要求依賴對象必須存在,如果要允許null值,可以設置它的required屬性為false。
3、@Resource?如果指定了 name 或 type,則按指定的進行裝配;如果都不指定,則優(yōu)先按名稱裝配,當找不到與名稱匹配的 bean 時才按照類型進行裝配。
@Autowire 怎么使用名稱來注入(6分)
配合 @Qualifier 使用,如下所示:
public class Test {private UserService userService;}
@PostConstruct 修飾的方法里用到了其他 bean 實例,會有問題嗎(5分)
該題可以拆解成下面3個問題:
1、@PostConstruct 修飾的方法被調(diào)用的時間
2、bean 實例依賴的其他 bean?被注入的時間,也可理解為屬性的依賴注入時間
3、步驟2的時間是否早于步驟1:如果是,則沒有問題,如果不是,則有問題
解析:
1、PostConstruct 注解被封裝在 CommonAnnotationBeanPostProcessor中,具體觸發(fā)時間是在 postProcessBeforeInitialization 方法,從 doCreateBean?維度看,則是在?initializeBean 方法里,屬于初始化?bean?階段。
2、屬性的依賴注入是在 populateBean 方法里,屬于屬性填充階段。
3、屬性填充階段位于初始化之前,所以本題答案為沒有問題。
bean?的 init-method?屬性指定的方法里用到了其他?bean?實例,會有問題嗎(5分)
該題同上面這題類似,只是將 @PostConstruct?換成了 init-method 屬性。
答案是不會有問題。同上面一樣,init-method 屬性指定的方法也是在 initializeBean 方法里被觸發(fā),屬于初始化?bean?階段。
要在 Spring?IoC 容器構建完畢之后執(zhí)行一些邏輯,怎么實現(xiàn)(6分)
1、比較常見的方法是使用事件監(jiān)聽器,實現(xiàn) ApplicationListener 接口,監(jiān)聽 ContextRefreshedEvent 事件。
2、還有一種比較少見的方法是實現(xiàn) SmartLifecycle 接口,并且 isAutoStartup 方法返回 true,則會在 finishRefresh() 方法中被觸發(fā)。
兩種方式都是在 finishRefresh 中被觸發(fā),SmartLifecycle在ApplicationListener之前。
Spring?中的常見擴展點有哪些(5分)
1、ApplicationContextInitializer
initialize 方法,在 Spring 容器刷新前觸發(fā),也就是 refresh 方法前被觸發(fā)。
2、BeanFactoryPostProcessor
postProcessBeanFactory 方法,在加載完 Bean 定義之后,創(chuàng)建 Bean 實例之前被觸發(fā),通常使用該擴展點來加載一些自己的 bean 定義。
3、BeanPostProcessor
postProcessBeforeInitialization 方法,執(zhí)行 bean 的初始化方法前被觸發(fā);postProcessAfterInitialization 方法,執(zhí)行 bean 的初始化方法后被觸發(fā)。
4、@PostConstruct
該注解被封裝在 CommonAnnotationBeanPostProcessor 中,具體觸發(fā)時間是在 postProcessBeforeInitialization 方法。
5、InitializingBean
afterPropertiesSet 方法,在 bean 的屬性填充之后,初始化方法(init-method)之前被觸發(fā),該方法的作用基本等同于 init-method,主要用于執(zhí)行初始化相關操作。
6、ApplicationListener,事件監(jiān)聽器
onApplicationEvent 方法,根據(jù)事件類型觸發(fā)時間不同,通常使用的 ContextRefreshedEvent 觸發(fā)時間為上下文刷新完畢,通常用于 IoC 容器構建結(jié)束后處理一些自定義邏輯。
7、@PreDestroy
該注解被封裝在 DestructionAwareBeanPostProcessor 中,具體觸發(fā)時間是在 postProcessBeforeDestruction 方法,也就是在銷毀對象之前觸發(fā)。
8、DisposableBean
destroy 方法,在 bean 的銷毀階段被觸發(fā),該方法的作用基本等同于destroy-method,主用用于執(zhí)行銷毀相關操作。Spring中如何讓兩個bean按順序加載?(8分)
1、使用 @DependsOn、depends-on
2、讓后加載的類依賴先加載的類
public class A {private B b;}
3、使用擴展點提前加載,例如:BeanFactoryPostProcessor
public class TestBean implements BeanFactoryPostProcessor {public void postProcessBeanFactory(ConfigurableListableBeanFactoryconfigurableListableBeanFactory) throws BeansException {// 加載beanbeanFactory.getBean("a");}}
使用 Mybatis 時,調(diào)用 DAO接口時是怎么調(diào)用到 SQL 的(7分)
詳細的解析見:《面試題:mybatis 中的 DAO 接口和 XML 文件里的 SQL 是如何建立關系的?》
簡單點說,當我們使用 Spring+MyBatis 時:
1、DAO接口會被加載到 Spring 容器中,通過動態(tài)代理來創(chuàng)建
2、XML中的SQL會被解析并保存到本地緩存中,key是SQL 的 namespace + id,value 是SQL的封裝
3、當我們調(diào)用DAO接口時,會走到代理類中,通過接口的全路徑名,從步驟2的緩存中找到對應的SQL,然后執(zhí)行并返回結(jié)果
最后
現(xiàn)在的 Java 服務基本都是基于 Spring 來開發(fā),Spring 的重要性不言而喻。
想比于應付面試,Spring 對于工作中的幫助會大的多。學習 Spring 有助于解決工作中遇到的問題、閱讀其他 Java 框架的源碼、學習優(yōu)秀的設計思想等等。
原創(chuàng)不易,如果你覺得本文寫的還不錯,對你有幫助,請通過【點贊】【在看】【分享】【留言】讓我知道,支持我寫出更好的文章,這對我很重要。
推薦閱讀
字節(jié)、美團、快手核心部門面試總結(jié)(真題解析)
4 年 Java 經(jīng)驗面試總結(jié)、心得體會
