目錄
1. Bean 的作用域
1.1 singleton
1.2?prototype
1.3 request
1.4 session
1.5 application
1.5.1 servletContext 和 applicationContext 區別
2. Bean 的生命周期
2.1 詳解初始化
2.1.1 Aware 接口回調
2.1.2 執行初始化方法
2.2 代碼示例
2.3 源碼 [面試題]
3. SpringBoot 自動配置
3.1 問題復現
3.2 解決方法
?3.2.1?@ComponentScan
3.2.2?@Import({第三方類.class})
3.2.3?@Import(MySelector.class)
?3.2.4 第三方 jar 中定義注解
3.3 源碼解讀
1. Bean 的作用域
Bean 的作用域, 這里的 "作用域" 是指 Bean 在 Spring 框架中不同的行為模式.
舉個例子, 當 Spring 容器中相同名稱的 Bean 只有一個時, 我們無論是通過 context.getBean 獲取還是通過 @Autowired 獲取, 獲取到的 Bean 都是同一個對象(內存地址相同).
這就是 Spring Bean 默認的作用域/行為模式 --- 單例作用域.
Bean 的作用域有以下 6 種:
作用域 | 說明 |
singleton(單例作用域) | 每個 Spring IoC 容器內同名稱的 bean 只有一個實例 (默認). |
prototype(原型/多例作用域) | 每次獲取 bean 時會創建新的實例 (非單例) |
request(請求作用域) | 每個 HTTP 請求生命周期內,創建新的實例 (web 環境中,了解) |
session(會話作用域) | 每個 HTTP Session 生命周期內,創建新的實例 (web 環境中,了解) |
application(全局作用域) | 每個 ServletContext 生命周期內,創建新的實例 (web 環境中,了解) |
websocket(HTTP WebSocket 作用域) | 每個 WebSocket 生命周期內,創建新的實例 (web 環境中,了解) |
1.1 singleton
singleton 是 Spring 應用設計模式中單例模式的體現.
singleton 單例作用域, 也是 Spring 默認的作用域, 指在?Spring 容器中, 相同名稱的 Bean 只有一個.
如上圖所示, 只要獲取的是同一個 Bean name 的 Bean,?不管是 @Resource?獲取, 還是 getBean? 獲取, 又或者是不同的客戶端(google, postman, ...)獲取, 獲取到的都是同一個 Bean.
1.2?prototype
多例模式, 每次獲取 Bean 時, 都會創建一個新的實例(無論 Bean name 是否相同)
如上圖所示, 每發送一個請求, 通過 applicationContext 獲取的 bean 就發生了變化. 但是, 通過 @Resource 獲取的 bean 卻一直不變.
這是因為: 每請求一次, context 就會重新執行一次 getBean 方法(重新獲取 bean), 而我們設置的作用域是?prototype, 自然每次獲取到的都是一個新的 bean.
但是, 通過 @Resource 獲取?bean 賦值給屬性這一步驟, 是在項目啟動時執行獲取 bean 的, 只會執行這一次, 因此只會獲取一次 bean.?因此無論請求多少次, 這個屬性的值依然是之前的 bean, 是不會變的(除非項目重新啟動).
@Resource/@Autowired 是在項目啟動時, 底層調用的是 ApplicationContext.getBean 獲取 bean 的.
1.3 request
作用域為 request 時, 每次請求, 都會創建一個全新的實例.?
但在同一個請求內是單例的(同一次請求中, 如果 bean name 相同, 那么獲取到的是相同的 bean).
1.4 session
作用域為 session 時, 同一個會話中, 同一個 bean name, 獲取到的都是同一個 bean.
1.5 application
作用域為 application 時, 同一個 servletContext 中, 獲取到的同一個 bean.
就目前來看, application 和 singleton 的效果是一致的, 接下來為大家介紹一下兩者區別.
1.5.1 servletContext 和 applicationContext 區別
servletContext 可以認為是一個 Tomcat?服務器, 一個 Tomcat 服務器中, 可以部署多個服務, 其中每一個服務是一個 applicationContext.
因此, 一個 servletContext 可以包含多個 applicationContext.
但是目前, 我們都是基于 Spring 進行開發, 而 Spring 集成了 Tomcat, 一個 Tomcat 中只運行一個服務, 因此目前的現狀是: 一個 Tomcat 只部署一個服務.
因此, 對于 Spring 項目來說, 作用域為 application 和 singleton 的效果是一樣的.
2. Bean 的生命周期
bean 的生命周期和我們人的生命周期, 從出生到死亡. 又或者說像一個戲劇演員, 從登臺到謝幕...
簡潔來說, bean 的生命周期為:?實例化 ? 填充屬性/依賴注入 ? 初始化 ? 使用?? 銷毀
- 實例化: 為 bean 分配內存空間(執行構造方法)?
- 屬性賦值: 對屬性 bean 進行依賴注入
- 初始化: 1. 執行各種 aware 通知(BeanNameAware, BeanFactoryAware, ...) 2. 執行初始化方法(@Bean, @PostConstruct, ...)
- 使用 bean
- 銷毀 bean(執行 @PreDestroy 方法)
先打個比方:
- 實例化: 買房
- 屬性賦值: 裝修
- 初始化: 買家電
- 使用 bean: 住房
- 銷毀 bean: 賣房
2.1 詳解初始化
初始化較難理解, 我們這里聚焦初始化這一步.
初始化中, 有兩個關鍵動作:
- 執行 aware 接口回調
- 執行初始化方法
2.1.1 Aware 接口回調
如果 Bean 實現了 Spring 提供的一些特定的 Aware 接口, 如:?BeanNameAware,?BeanFactoryAware,?ApplicationContextAware, ... 那么在初始化時,?Spring 會在初始化階段調用這些 Aware 接口中定義的方法, 使得 Bean 可以獲取到 Spring 容器的相關信息.
這幾個接口的作用如下:
-
BeanNameAware: 獲取當前 Bean 的 bean name(告訴你你叫什么名字)
-
BeanFactoryAware: 獲取 BeanFactory, 允許你在 Bean 內部動態訪問容器中的其他 Bean.(相當于 Spring 在初始化你時, 告訴你: 這是我們的 Bean 管理中心鑰匙, 需要時你可以自己去開柜子拿其他 Bean)
-
ApplicationContextAware: 獲取 ApplicationContext(獲取 Spring 配置信息, 告訴你你生長在什么配置環境下)
Aware 接口名 | 要干嘛? | 你要怎么用? |
---|---|---|
BeanNameAware | 想知道自己的名字 | 實現接口:重寫 setBeanName(String name) |
BeanFactoryAware | 想拿到 Bean 工廠引用 | 實現接口:重寫 setBeanFactory(BeanFactory) |
ApplicationContextAware | 想拿到上下文容器 | 實現接口:重寫 setApplicationContext(...) |
EnvironmentAware | 想獲取環境變量 | 實現接口:重寫 setEnvironment(...) |
ResourceLoaderAware | 想讀取資源文件 | 實現接口:重寫 setResourceLoader(...) |
2.1.2 執行初始化方法
初始化中的第二個關鍵動作, 即: 執行初始化方法.
定義初始化方法, 有以下三種方式:
方式 | 是否需要你主動聲明 | 如何聲明 |
---|---|---|
@PostConstruct 注解 | ? 要加注解 | @PostConstruct public void init() {...} |
實現 InitializingBean 接口 | ? 要重寫方法 | 重寫 afterPropertiesSet() 方法 |
initMethod 屬性 | ? 要在配置里寫 | @Bean(initMethod = "xxx")或 XML |
注意:?無論是 Aware 回調,還是初始化方法,確實都需要你“主動聲明” —— 要么實現接口,要么加注解,要么在配置中寫明?!!
- Aware 回調:靠你“實現接口”,Spring 才知道你“想要這個功能”
- 初始化方法:你也得“聲明想執行”,才會觸發
2.2 代碼示例
@Component
public class BeanLifeComponent implements BeanNameAware {// 注入 bean: 1. @Autowired... 2. 構造方法 3. set// 這里通過 set 方法注入private DogComponent dogComponent;// 1. 實例化public BeanLifeComponent() {System.out.println("1. 實例化, 執行構造方法");}// 2. 構造方法注入屬性 bean@Autowiredpublic void setDogComponent(DogComponent dogComponent) {this.dogComponent = dogComponent;System.out.println("2. 屬性注入");}// 3. 執行 Aware 回調@Overridepublic void setBeanName(String name) {System.out.println("3. Aware 回調, bean name: " + name);}// 4. 執行初始化方法@PostConstructpublic void init() {System.out.println("4. 執行初始化方法");}// 5. 使用 Beanpublic void use() {System.out.println("5. 使用 Bean");}// 6. 銷毀 Bean@PreDestroypublic void destroy() {System.out.println("6. 銷毀 bean");}
}
執行結果如下:
2.3 源碼 [面試題]
Bean 生命周期的核心流程在 AbstractAutowireCapableBeanFactory 的 createBean 方法中.
1. 實例化: 根據類的全限定名, 獲取類的 .class, 從 BeanFactory 中執行 Bean 的構造方法, 創建?Bean 的實例.
2. 屬性注入: 先查看有沒有屬性, 如果有的話, 判斷 Bean 是根據 Bean name 注入的還是根據類型注入的, 分別執行不同的方法, 進行屬性注入
3. 初始化過程, 在源碼中分為四部分:
- 檢查 Bean 實現了哪些 Aware 接口, 并執行其中的執行 Aware 回調方法
- 執行初始化前置方法
- 執行初始化方法
- 執行初始化后置方法
?4. Bean 使用
5. 執行 Bean 銷毀注解的方法
3. SpringBoot 自動配置
通過 Maven 將第三方 JAR 包的依賴引入項目中后, 然后這些第三方 jar 提供的 Bean, 就能通過注解 "自動地" 注入到我們的項目, 而不需要我們手動寫 @ComponentScan 指定它的包, 或者寫 @Import 去導入它的配置類(不受啟動類目錄限制/不受 Spring 掃描路徑的限制) —— 這就是 Spring Boot 自動配置.
3.1 問題復現
為了更好的讓大家理解 Spring 的自動配置, 我們先模擬一下沒有 "自動配置" 的場景.
我們新建一個 config 包, 模擬第三方 jar 代碼:
我們使用 @Configuration 將第三方代碼中的 Bean 交給 Spring 管理.
接著, 在啟動類中使用 getBean 獲取這些第三方 Bean, 觀察是否能執行成功:
答案是顯而易見的, Spring 只會掃描啟動類所在目錄, 不會掃描第三方 jar 代碼, 因此 Spring 容器根本沒有執行那個第三方 JAR 中的?@Configuration?類, 因此也根本沒有管理其中定義的 Bean, 更不會獲取到這些 Bean.
3.2 解決方法
解決方法有以下四種:
- @ComponentScan(basePackages = "com.config"): 在啟動類上,?明確指定要掃描的第三方 JAR 的包路徑
- @Import({DemoConfig.class, DemoConfig2.class}): 在啟動類上, 手動地導入第三方 JAR 中的?@Configuration?類
- @Import(MySelector.class): 第三方 jar 實現 ImportSelector 接口, 一次性導入所有第三方 jar 類
- 第三方 jar 中自定義注解, 在該注解的定義中使用?@Import?來引用同一個 JAR 包中的其他配置類
?3.2.1?@ComponentScan
@ComponentScan(basePackages = "com.config"): 在啟動類上,?明確指定要掃描的第三方 JAR 的包路徑.
3.2.2?@Import({第三方類.class})
@Import({DemoConfig.class, DemoConfig2.class}): 在啟動類上, 手動地導入第三方 JAR 中的?@Configuration?類
3.2.3?@Import(MySelector.class)
@Import(MySelector.class): 在第三方 jar 中實現 ImportSelector 接口, 一次性導入所有第三方 jar 類
創建自定類?MySelector 實現 ImportSelector 接口, 重寫方法, 對要導入的第三方類統一封裝.
?3.2.4 第三方 jar 中定義注解
第三方 jar 中自定義注解, 在該注解的定義中使用?@Import?來引用同一個 JAR 包中的其他配置類
Spring 底層也是采用注解的方式, 來完成 自動配置 功能的.
3.3 源碼解讀
我們知道, Spring 項目啟動時, 只會默認掃描啟動類所在的目錄及其子孫目錄, 可是我們通過 Maven 引入的第三方 jar 包中的 Bean 是怎么被 Spring 掃描到, 并交由 Spring 管理的呢?
我們來看源碼是怎么做的.
總結:
Spring 的自動配置, 是通過 @SpringBootApplication 注解實現的. @SpringBootApplication 中包含三個關鍵注解:
- @SpringBootConfiguration
- @EnableAutoConfiguration
- @ComponentScan
其中, @SpringBootConfiguration 可以認為是 @Configuration 的封裝, 標識啟動類為配置類, 本質也是交給 Spring 管理.
而 @ComponentScan, 指定了 Spring 的默認掃描路徑, 也就是掃描啟動類所在目錄.
最核心的是 @EnableAutoConfiguration 注解, 這個注解中又包含了兩個注解:
- @AutoConfigurationPackage
- @Import(提供了一個 ImportSelector)
其中, @Import 中的 ImportSelector, 讓 Spring 能夠掃描第三方 jar 包提供的 import 文件中的類(第三方會提供一個 import 文件, 其中包含了要被 Spring 管理的類),?但是 Spring 不會將 import 文件中的所有類都進行管理, 而是會通過 @Condition 條件注解進行篩選, 篩選出要管理的 Bean.
而 @AutoConfigurationPackage 和 @ComponentScan 有點相似, 共同作用使 Spring 默認掃描啟動類所在包及其子包.
@AutoConfigurationPackage 和 @ComponentScan 區別:
- @ComponentScan: 這是真正執行掃描動作的注解, 如果沒有指定掃描路徑, 會默認按照 @AutoConfigurationPackage 提供的路徑即啟動類路徑進行掃描.
- @AutoConfigurationPackage: 注冊啟動類所在的包. 它本身不執行掃描, 它的主要作用是告訴 @ComponentScan 啟動類在哪里
面試時, 先總結給出一個高層次的概述, 如果面試官追問細節, 再用這些內容詳細去闡述 @ComponentScan 和 @AutoConfigurationPackage 的具體區別.
END?