<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          三萬字盤點(diǎn)Spring最最核心的9大核心功能

          共 43990字,需瀏覽 88分鐘

           ·

          2024-05-13 08:15

          大家好,我是三友~~

          大概有一個多月沒寫原創(chuàng)文章了,可能讓大家有點(diǎn)失望了

          之所以這么長時間沒寫文章,主要有兩個原因:

          第一點(diǎn)就是我最近在忙一件大事,很重要,現(xiàn)在還沒有結(jié)果,等有結(jié)果了我再跟大家匯報匯報

          第二點(diǎn)就是我最近也沒有想好寫什么,如果你有什么想學(xué)習(xí)和了解的,可以私信或者留言告訴我,只要我覺得可以,我一定會抽時間去寫

          由于沒寫文章,所以這里我就把之前我覺得寫的還可以的一篇關(guān)于Spring文章拿過來再分享給大家

          這篇文章從前期的選題、準(zhǔn)備、翻源碼、動手到寫完,前后跨度接近一個月的時間,花了好幾個周末,寫了三萬字,最終才算完成

          文章很長,我相信你看完之后一定會有所收貨

          資源管理

          資源管理是Spring的一個核心的基礎(chǔ)功能,不過在說Spring的資源管理之前,先來簡單說一下Java中的資源管理。

          Java資源管理

          Java中的資源管理主要是通過java.net.URL來實(shí)現(xiàn)的,通過URL的openConnection方法可以對資源打開一個連接,通過這個連接讀取資源的內(nèi)容。

          資源不僅僅指的是網(wǎng)絡(luò)資源,還可以是本地文件、一個jar包等等。

          1、來個Demo

          舉個例子,比如你想到訪問www.baidu.com這個百度首頁網(wǎng)絡(luò)資源,那么此時就可以這么寫

          public class JavaResourceDemo {

              public static void main(String[] args) throws IOException {
                  //構(gòu)建URL 指定資源的協(xié)議為http協(xié)議
                  URL url = new URL("http://www.baidu.com");
                  //打開資源連接
                  URLConnection urlConnection = url.openConnection();
                  //獲取資源輸入流
                  InputStream inputStream = urlConnection.getInputStream();
                  //通過hutool工具類讀取流中數(shù)據(jù)
                  String content = IoUtil.read(new InputStreamReader(inputStream));
                  System.out.println(content);
              }

          }

          解釋一下上面代碼的意思:

          • 首先構(gòu)建一個URL,指定資源的訪問協(xié)議為http協(xié)議
          • 通過URL打開一個資源訪問連接,然后獲取一個輸入流,讀取內(nèi)容

          運(yùn)行結(jié)果

          成功讀取到百度首頁的數(shù)據(jù)。

          當(dāng)然,也可以通過URL訪問本地文件資源,在創(chuàng)建URL的時候只需要指定協(xié)議類型為file://和文件的路徑就行了

          URL url = new URL("file://" + "文件的路徑");

          這種方式這里我就不演示了。

          其實(shí)這種方式實(shí)際上最終也是通過FileInputStream來讀取文件數(shù)據(jù)的,不信你可以自己debug試試。

          2、原理

          每種協(xié)議的URL資源都需要一個對應(yīng)的一個URLStreamHandler來處理。

          URLStreamHandler

          比如說,http://協(xié)議有對應(yīng)的URLStreamHandler的實(shí)現(xiàn),file://協(xié)議的有對應(yīng)的URLStreamHandler的實(shí)現(xiàn)。

          Java除了支持http://file://協(xié)議之外,還支持其它的協(xié)議,如下圖所示:

          對于的URLStreamHandler如下圖所示

          當(dāng)在構(gòu)建URL的時候,會去解析資源的訪問協(xié)議,根據(jù)訪問協(xié)議找到對應(yīng)的URLStreamHandler的實(shí)現(xiàn)。

          當(dāng)然,除了Java本身支持的協(xié)議之外,我們還可以自己去擴(kuò)展這個協(xié)議,大致只需要兩步即可:

          • 實(shí)現(xiàn)URLConnection,可以通過這個連接讀取資源的內(nèi)容
          • 實(shí)現(xiàn)URLStreamHandler,通過URLStreamHandler可以獲取到URLConnection

          不過需要注意的是,URLStreamHandler的實(shí)現(xiàn)需要放在sun.net.www.protocol.協(xié)議名稱包下,類名必須是Handler,這也是為什么截圖中的實(shí)現(xiàn)類名都叫Handler的原因。

          當(dāng)然如果不放在指定的包下也可以,但是需要實(shí)現(xiàn)java.net.URLStreamHandlerFactory接口。

          對于擴(kuò)展我就不演示了,如果你感興趣可以自行谷歌一下。

          Spring資源管理

          雖然Java提供了標(biāo)準(zhǔn)的資源管理方式,但是Spring并沒有用,而是自己搞了一套資源管理方式。

          1、資源抽象

          在Spring中,資源大致被抽象為兩個接口

          • Resource:可讀資源,可以獲取到資源的輸入流
          • WritableResource:讀寫資源,除了資源輸入流之外,還可以獲取到資源的輸出流
          Resource

          Resource接口繼承了InputStreamSource接口,而InputStreamSource接口可以獲取定義了獲取輸入流的方法

          WritableResource

          WritableResource繼承了Resource接口,可以獲取到資源的輸出流,因為有的資源不僅可讀,還可寫,就比如一些本地文件的資源,往往都是可讀可寫的

          Resource的實(shí)現(xiàn)很多,這里我舉幾個常見的:

          • FileSystemResource:讀取文件系統(tǒng)的資源
          • UrlResource:前面提到的Java的標(biāo)準(zhǔn)資源管理的封裝,底層就是通過URL來訪問資源
          • ClassPathResource:讀取classpath路徑下的資源
          • ByteArrayResource:讀取靜態(tài)字節(jié)數(shù)組的數(shù)據(jù)

          比如,想要通過Spring的資源管理方式來訪問前面提到百度首頁網(wǎng)絡(luò)資源,就可以這么寫

          //構(gòu)建資源
          Resource resource = new UrlResource("http://www.baidu.com");
          //獲取資源輸入流
          InputStream inputStream = resource.getInputStream();

          如果是一個本地文件資源,那么除了可以使用UrlResource,也可以使用FileSystemResource,都是可以的。

          2、資源加載

          雖然Resource有很多實(shí)現(xiàn),但是在實(shí)際使用中,可能無法判斷使用具體的哪個實(shí)現(xiàn),所以Spring提供了ResourceLoader資源加載器來根據(jù)資源的類型來加載資源。

          ResourceLoader

          通過getResource方法,傳入一個路徑就可以加載到對應(yīng)的資源,而這個路徑不一定是本地文件,可以是任何可加載的路徑。

          ResourceLoader有個唯一的實(shí)現(xiàn)DefaultResourceLoader

          比如對于上面的例子,就可以通過ResourceLoader來加載資源,而不用直接new具體的實(shí)現(xiàn)了

          //創(chuàng)建ResourceLoader
          ResourceLoader resourceLoader = new DefaultResourceLoader();
          //獲取資源
          Resource resource = resourceLoader.getResource("http://www.baidu.com");

          除了ResourceLoader之外,還有一個ResourcePatternResolver可以加載資源

          ResourcePatternResolver繼承了ResourceLoader

          通過ResourcePatternResolver提供的方法可以看出,他可以加載多個資源,支持使用通配符的方式,比如classpath*:,就可以加載所有classpath的資源。

          ResourcePatternResolver只有一個實(shí)現(xiàn)PathMatchingResourcePatternResolver

          PathMatchingResourcePatternResolver

          3、小結(jié)

          到這就講完了Spring的資源管理,這里總結(jié)一下本節(jié)大致的內(nèi)容

          Java的標(biāo)準(zhǔn)資源管理:

          • URL
          • URLStreamHandler

          Spring的資源管理:

          • 資源抽象:Resource 、WritableResource
          • 資源加載:ResourceLoader 、ResourcePatternResolver

          Spring的資源管理在Spring中用的很多,比如在SpringBoot中,application.yml的文件就是通過ResourceLoader加載成Resource,之后再讀取文件的內(nèi)容的。

          環(huán)境

          上一節(jié)末尾舉的例子中提到,SpringBoot配置文件是通過ResourceLoader來加載配置文件,讀取文件的配置內(nèi)容

          那么當(dāng)配置文件都加載完成之后,這個配置應(yīng)該存到哪里,怎么能夠讀到呢?

          這就引出了Spring框架中的一個關(guān)鍵概念,環(huán)境,它其實(shí)就是用于管理應(yīng)用程序配置的。

          1、Environment

          Environment就是環(huán)境抽象出來的接口

          Environment繼承PropertyResolver

          public interface PropertyResolver {

              boolean containsProperty(String key);

              String getProperty(String key);

              <T> getProperty(String key, Class<T> targetType);

              <T> getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;

              String resolvePlaceholders(String text);

          }

          如上是PropertyResolver提供的部分方法,這里簡單說一下上面方法的作用

          • getProperty(String key),很明顯是通過配置的key獲取對應(yīng)的value值
          • getProperty(String key, Class<T> targetType),這是獲取配置,并轉(zhuǎn)換成對應(yīng)的類型,比如你獲取的是個字符串的"true",這里就可以給你轉(zhuǎn)換成布爾值的true,具體的底層實(shí)現(xiàn)留到下一節(jié)講
          • resolvePlaceholders(String text),這類方法可以處理${...}占位符,也就是先取出${...}占位符中的key,然后再通過key獲取到值

          所以Environment主要有一下幾種功能:

          • 根據(jù)key獲取配置
          • 獲取到指定類型的配置
          • 處理占位符

          來個demo

          先在application.yml的配置文件中加入配置

          測試代碼如下

          @SpringBootApplication
          public class EnvironmentDemo {

              public static void main(String[] args) {
                  ConfigurableApplicationContext applicationContext = SpringApplication.run(EnvironmentDemo.classargs);

                  //從ApplicationContext中獲取到ConfigurableEnvironment
                  ConfigurableEnvironment environment = applicationContext.getEnvironment();

                  //獲取name屬性對應(yīng)的值
                  String name = environment.getProperty("name");
                  System.out.println("name = " + name);
              }

          }

          啟動應(yīng)用,獲取到ConfigurableEnvironment對象,再獲取到值

          ConfigurableEnvironment是Environment子接口,通過命名也可以知道,他可以對Environment進(jìn)行一些功能的配置。

          運(yùn)行結(jié)果:

          name = 三友的java日記

          2、配置屬性源PropertySource

          PropertySource是真正存配置的地方,屬于配置的來源,它提供了一個統(tǒng)一的訪問接口,使得應(yīng)用程序可以以統(tǒng)一的方式獲取配置獲取到屬性。

          PropertySource

          來個簡單demo

          public class PropertySourceDemo {

              public static void main(String[] args) {

                  Map<String, Object> source = new HashMap<>();
                  source.put("name""三友的java日記");

                  PropertySource<Map<String, Object>> propertySource = new MapPropertySource("myPropertySource", source);

                  Object name = propertySource.getProperty("name");

                  System.out.println("name = " + name);
              }

          }

          簡單說一下上面代碼的意思

          • 首先創(chuàng)建了一個map,就是配置來源,往里面添加了一個配置key-value
          • 創(chuàng)建了一個PropertySource,使用的實(shí)現(xiàn)是MapPropertySource,需要傳入配置map,所以最終獲取到屬性不用想就知道是從map中獲取的

          最后成獲取到屬性

          除了MapPropertySource之外,還有非常多的實(shí)現(xiàn)

          PropertySource實(shí)現(xiàn)

          比如CommandLinePropertySource,它其實(shí)就封裝了通過命令啟動時的傳遞的配置參數(shù)

          既然PropertySource才是真正存儲配置的地方,那么Environment獲取到的配置真正也就是從PropertySource獲取的,并且他們其實(shí)是一對多的關(guān)系

          其實(shí)很好理解一對多的關(guān)系,因為一個應(yīng)用程序的配置可能來源很多地方,比如在SpringBoot環(huán)境底下,除了我們自定義的配置外,還有比如系統(tǒng)環(huán)境配置等等,這些都可以通過Environment獲取到

          當(dāng)從Environment中獲取配置的時候,會去遍歷所有的PropertySource,一旦找到配置key對應(yīng)的值,就會返回

          所以,如果有多個PropertySource都含有同一個配置項的話,也就是配置key相同,那么獲取到的配置是從排在前面的PropertySource的獲取的

          這就是為什么,當(dāng)你在配置文件配置username屬性時獲取到的卻是系統(tǒng)變量username對應(yīng)的值,因為系統(tǒng)的PropertySource排在配置文件對應(yīng)的PropertySource之前

          3、SpringBoot是如何解析配置文件

          SpringBoot是通過PropertySourceLoader來解析配置文件的

          load方法的第二個參數(shù)就是我們前面提到的資源接口Resource

          通過Resource就可以獲取到配置文件的輸入流,之后就可以讀取到配置文件的內(nèi)容,再把配置文件解析成多個PropertySource,之后把PropertySource放入到Environment中,這樣我們就可以通過Environment獲取到配置文件的內(nèi)容了。

          PropertySourceLoader默認(rèn)有兩個實(shí)現(xiàn),分別用來解析propertiesyml格式的配置文件

          此時,上面的圖就可以優(yōu)化成這樣

          類型轉(zhuǎn)換

          在上一節(jié)介紹Environment時提到了它的getProperty(String key, Class<T> targetType)可以將配置的字符串轉(zhuǎn)換成對應(yīng)的類型,那么他是如何轉(zhuǎn)換的呢?

          這就跟本文要講的Spring類型轉(zhuǎn)換機(jī)制有關(guān)了

          1、類型轉(zhuǎn)換API

          Spring類型轉(zhuǎn)換主要涉及到以下幾個api:

          • PropertyEditor
          • Converter
          • GenericConverter
          • ConversionService
          • TypeConverter

          接下來我會來詳細(xì)介紹這幾個api的原理和他們之間的關(guān)系。

          1.1、PropertyEditor

          PropertyEditor并不是Spring提供的api,而是JDK提供的api,他的主要作用其實(shí)就是將String類型的字符串轉(zhuǎn)換成Java對象屬性值。

          public interface PropertyEditor {

              void setValue(Object value);

              Object getValue();

              String getAsText();

              void setAsText(String text) throws java.lang.IllegalArgumentException;
              
          }

          就拿項目中常用的@Value來舉例子,當(dāng)我們通過@Value注解的方式將配置注入到字段時,大致步驟如下圖所示:

          • 取出@Value配置的key
          • 根據(jù)@Value配置的key調(diào)用Environment的resolvePlaceholders(String text)方法,解析占位符,找到配置文件中對應(yīng)的值
          • 調(diào)用PropertyEditor將對應(yīng)的值轉(zhuǎn)換成注入的屬性字段類型,比如注入的字段類型是數(shù)字,那么就會將字符串轉(zhuǎn)換成數(shù)字

          在轉(zhuǎn)換的過程中,Spring會先調(diào)用PropertyEditor的setAsText方法將字符串傳入,然后再調(diào)用getValue方法獲取轉(zhuǎn)換后的值。

          Spring提供了很多PropertyEditor的實(shí)現(xiàn),可以實(shí)現(xiàn)字符串到多種類型的轉(zhuǎn)換

          在這么多實(shí)現(xiàn)中,有一個跟我們前面提到的Resource有關(guān)的實(shí)現(xiàn)ResourceEditor,它是將字符串轉(zhuǎn)換成Resource對象

          ResourceEditor

          也就是說,可以直接通過@Value的方式直接注入一個Resource對象,就像下面這樣

          @Value("http://www.baidu.com")
          private Resource resource;

          其實(shí)歸根到底,底層也是通過ResourceLoader來加載的,這個結(jié)論是不變的。

          所以,如果你想知道@Value到底支持注入哪些字段類型的時候,看看PropertyEditor的實(shí)現(xiàn)就可以了,當(dāng)然如果Spring自帶的都不滿足你的要求,你可以自己實(shí)現(xiàn)PropertyEditor,比如把String轉(zhuǎn)成Date類型,Spring就不支持。

          1.2、Converter

          由于PropertyEditor局限于字符串的轉(zhuǎn)換,所以Spring在后續(xù)的版本中提供了叫Converter的接口,他也用于類型轉(zhuǎn)換的,相比于PropertyEditor更加靈活、通用

          Converter

          Converter是個接口,泛型S是被轉(zhuǎn)換的對象類型,泛型T是需要被轉(zhuǎn)成的類型。

          同樣地,Spring也提供了很多Converter的實(shí)現(xiàn)

          這些主要包括日期類型的轉(zhuǎn)換和String類型轉(zhuǎn)換成其它的類型

          1.3、GenericConverter

          GenericConverter也是類型轉(zhuǎn)換的接口

          這個接口的主要作用是可以處理帶有泛型類型的轉(zhuǎn)換,主要的就是面向集合數(shù)組轉(zhuǎn)換操作,從Spring默認(rèn)提供的實(shí)現(xiàn)就可以看出

          那Converter跟GenericConverter有什么關(guān)系呢?

          這里我舉個例子,假設(shè)現(xiàn)在需要將將源集合Collection<String>轉(zhuǎn)換成目標(biāo)集合Collection<Date>

          假設(shè)現(xiàn)在有個String轉(zhuǎn)換成Date類型的Converter,咱就叫StringToDateConverter,那么整個轉(zhuǎn)換過程如下:

          • 首先會找到GenericConverter的一個實(shí)現(xiàn)CollectionToCollectionConverter,從名字也可以看出來,是將一個幾個轉(zhuǎn)換成另一個集合
          • 然后遍歷源集合Collection<String>,取出元素
          • 根據(jù)目標(biāo)集合泛型Date,找到StringToDateConverter,將String轉(zhuǎn)換成Date,將轉(zhuǎn)換的Date存到一個新的集合
          • 返回這個新的集合,這樣就實(shí)現(xiàn)了集合到集合的轉(zhuǎn)換

          所以通過這就可以看出Converter和GenericConverter其實(shí)是依賴關(guān)系

          1.4、ConversionService

          對于我們使用者來說,不論是Converter還是GenericConverter,其實(shí)都是類型轉(zhuǎn)換的,并且類型轉(zhuǎn)換的實(shí)現(xiàn)也很多,所以Spring為了方便我們使用Converter還是GenericConverter,提供了一個門面接口ConversionService

          ConversionService

          我們可以直接通過ConversionService來進(jìn)行類型轉(zhuǎn)換,而不需要面向具體的Converter或者是GenericConverter

          ConversionService有一個基本的實(shí)現(xiàn)GenericConversionService

          GenericConversionService

          同時GenericConversionService還實(shí)現(xiàn)了ConverterRegistry的接口

          ConverterRegistry提供了對Converter和GenericConverter進(jìn)行增刪改查的方法。

          ConverterRegistry

          這樣就可以往ConversionService中添加Converter或者是GenericConverter了,因為最終還是通過Converter和GenericConverter來實(shí)現(xiàn)轉(zhuǎn)換的

          但是我們一般不直接用GenericConversionService,而是用DefaultConversionService或者是ApplicationConversionService(SpringBoot環(huán)境底下使用)

          因為DefaultConversionService和ApplicationConversionService在創(chuàng)建的時候,會添加很多Spring自帶的Converter和GenericConverter,就不需要我們手動添加了。

          1.5、TypeConverter

          TypeConverter其實(shí)也是算是一個門面接口,他也定義了轉(zhuǎn)換方法

          他是將PropertyEditor和ConversionService進(jìn)行整合,方便我們同時使用PropertyEditor和ConversionService

          convertIfNecessary方法會去調(diào)用PropertyEditor和ConversionService進(jìn)行類型轉(zhuǎn)換,值得注意的是,優(yōu)先使用PropertyEditor進(jìn)行轉(zhuǎn)換,如果沒有找到對應(yīng)的PropertyEditor,會使用ConversionService進(jìn)行轉(zhuǎn)換

          TypeConverter有個簡單的實(shí)現(xiàn)SimpleTypeConverter,這里來個簡單的demo

          public class TypeConverterDemo {

              public static void main(String[] args) {
                  SimpleTypeConverter typeConverter = new SimpleTypeConverter();
                  
                  //設(shè)置ConversionService
                  typeConverter.setConversionService(DefaultConversionService.getSharedInstance());

                  //將字符串"true"轉(zhuǎn)換成Boolean類型的true
                  Boolean b = typeConverter.convertIfNecessary("true", Boolean.class);
                  System.out.println("b = " + b);
              }

          }

          這里需要注意,ConversionService需要我們手動設(shè)置,但是PropertyEditor不需要,因為SimpleTypeConverter默認(rèn)會去添加PropertyEditor的實(shí)現(xiàn)。

          小結(jié)

          到這就講完了類型轉(zhuǎn)換的常見的幾個api,這里再簡單總結(jié)一下:

          • PropertyEditor:String轉(zhuǎn)換成目標(biāo)類型
          • Converter:用于一個類型轉(zhuǎn)換成另一個類型
          • GenericConverter:用于處理泛型的轉(zhuǎn)換,主要用于集合
          • ConversionService:門面接口,內(nèi)部會調(diào)用Converter和GenericConverter
          • TypeConverter:門面接口,內(nèi)部會調(diào)用PropertyEditor和ConversionService

          畫張圖來總結(jié)他們之間的關(guān)系

          前面在舉@Value的例子時說,類型轉(zhuǎn)換是根據(jù)PropertyEditor來的,其實(shí)只說了一半,因為底層實(shí)際上是根據(jù)TypeConverter來轉(zhuǎn)換的,所以@Value類型轉(zhuǎn)換時也能使用ConversionService類轉(zhuǎn)換,所以那張圖實(shí)際上應(yīng)該這么畫才算對

          2、Environment中到底是如何進(jìn)行類型轉(zhuǎn)換的?

          這里我們回到開頭提到的話題,Environment中到底是如何進(jìn)行類型轉(zhuǎn)換的,讓我們看看Environment類的接口體系

          Environment有個子接口ConfigurableEnvironment中,前面也提到過

          它繼承了ConfigurablePropertyResolver接口

          而ConfigurablePropertyResolver有一個setConversionService方法

          所以從這可以看出,Environment底層實(shí)際上是通過ConversionService實(shí)現(xiàn)類型轉(zhuǎn)換的

          這其實(shí)也就造成了一個問題,因為ConversionService和PropertyEditor屬于并列關(guān)系,那么就會導(dǎo)致Environment無法使用PropertyEditor來進(jìn)行類型轉(zhuǎn)換,也就會喪失部分Spring提供的類型轉(zhuǎn)換功能,就比如無法通過Environment將String轉(zhuǎn)換成Resource對象,因為Spring沒有實(shí)現(xiàn)String轉(zhuǎn)換成Resource的Converter

          當(dāng)然你可以自己實(shí)現(xiàn)一個String轉(zhuǎn)換成Resource的Converter,然后添加到ConversionService,之后Environment就支持String轉(zhuǎn)換成Resource了。

          數(shù)據(jù)綁定

          上一節(jié)我們講了類型轉(zhuǎn)換,而既然提到了類型轉(zhuǎn)換,那么就不得不提到數(shù)據(jù)綁定了,他們是密不可分的,因為在數(shù)據(jù)綁定時,往往都會伴隨著類型轉(zhuǎn)換,

          數(shù)據(jù)綁定的意思就是將一些配置屬性跟我們的Bean對象的屬性進(jìn)行綁定。

          不知你是否記得,在遠(yuǎn)古的ssm時代,我們一般通過xml方式聲明Bean的時候,可以通過<property/>來設(shè)置Bean的屬性

          <bean class="com.sanyou.spring.core.basic.User">
              <property name="username" value="三友的java日記"/>
          </bean>
          @Data
          public class User {

              private String username;

          }

          然后Spring在創(chuàng)建User的過程中,就會給username屬性設(shè)置為三友的java日記

          這就是數(shù)據(jù)綁定,將三友的java日記綁定到username這個屬性上。

          數(shù)據(jù)綁定的核心api主要包括以下幾個:

          • PropertyValues
          • BeanWrapper
          • DataBinder

          1、PropertyValues

          這里我們先來講一下PropertyValue(注意沒有s)

          顧明思議,PropertyValue就是就是封裝了屬性名和對應(yīng)的屬性值,它就是數(shù)據(jù)綁定時屬性值的來源。

          以前面的提到的xml創(chuàng)建Bean為例,Spring在啟動的時候會去解析xml中的<property/>標(biāo)簽,然后將namevalue封裝成PropertyValue

          當(dāng)創(chuàng)建User這個Bean的時候,到了屬性綁定的階段的時候,就會取出PropertyValue,設(shè)置到User的username屬性上。

          而PropertyValues,比PropertyValue多了一個s,也就是復(fù)數(shù)的意思,所以其實(shí)PropertyValues本質(zhì)上就是PropertyValue的一個集合

          因為一個Bean可能有多個屬性配置,所以就用PropertyValues來保存。

          2、BeanWrapper

          BeanWrapper其實(shí)就數(shù)據(jù)綁定的核心api了,因為在Spring中涉及到數(shù)據(jù)綁定都是通過BeanWrapper來完成的,比如前面提到的Bean的屬性的綁定,就是通過BeanWrapper來的

          BeanWrapper是一個接口,他有一個唯一的實(shí)現(xiàn)BeanWrapperImpl。

          先來個demo

          public class BeanWrapperDemo {

              public static void main(String[] args) {
                  //創(chuàng)建user對象
                  User user = new User();

                  //創(chuàng)建BeanWrapper對象,把需要進(jìn)行屬性綁定的user對象放進(jìn)去
                  BeanWrapper beanWrapper = new BeanWrapperImpl(user);

                  //進(jìn)行數(shù)據(jù)綁定,將三友的java日記這個屬性值賦值到username這個屬性上
                  beanWrapper.setPropertyValue(new PropertyValue("username""三友的java日記"));

                  System.out.println("username = " + user.getUsername());
              }

          }

          結(jié)果

          成功獲取到,說明設(shè)置成功

          BeanWrapperImpl也間接實(shí)現(xiàn)了TypeConverter接口

          當(dāng)然底層還是通過前面提到的ConversionService和PropertyEditor實(shí)現(xiàn)的

          所以當(dāng)配置的類型跟屬性的類型不同時,就可以對配置的類型進(jìn)行轉(zhuǎn)換,然后再綁定到屬性上

          這里簡單說一下數(shù)據(jù)綁定和@Value的異同,因為這兩者看起來好像是一樣的,但實(shí)際還是有點(diǎn)區(qū)別的

          相同點(diǎn):

          • 兩者都會涉及到類型轉(zhuǎn)換,@Value和數(shù)據(jù)綁定都會將值轉(zhuǎn)換成目標(biāo)屬性對應(yīng)的類型,并且都是通過TypeConverter來轉(zhuǎn)換的

          不同點(diǎn):

          • 1、發(fā)生時機(jī)不同,@Value比數(shù)據(jù)綁定更早,當(dāng)@Value都注入完成之后才會發(fā)生數(shù)據(jù)綁定(屬性賦值)
          • 2、屬性賦值方式不同,@Value是通過反射來的,而是數(shù)據(jù)綁定是通過setter方法來的,如果沒有setter方法,屬性是沒辦法綁定的

          3、DataBinder

          DataBinder也是用來進(jìn)行數(shù)據(jù)綁定的,它的底層也是間接通過BeanWrapper來實(shí)現(xiàn)的數(shù)據(jù)綁定的

          但是他相比于BeanWrapper多了一些功能,比如在數(shù)據(jù)綁定之后,可以對數(shù)據(jù)校驗,比如可以校驗字段的長度等等

          說到數(shù)據(jù)校驗,是不是想到了SpringMVC中的參數(shù)校驗,通過@Valid配合一些諸如@NotBlank、@NotNull等注解,實(shí)現(xiàn)優(yōu)雅的參數(shù)校驗。

          其實(shí)SpringMVC的參數(shù)校驗就是通過DataBinder來的,所以DataBinder其實(shí)在SpringMVC中用的比較多,但是在Spring中確用的很少。

          如果你有興趣,可以翻一下SpringMVC中關(guān)于請求參數(shù)處理的HandlerMethodArgumentResolver的實(shí)現(xiàn),里面有的實(shí)現(xiàn)會用到DataBinder(WebDataBinder)來進(jìn)行數(shù)據(jù)請求參數(shù)跟實(shí)體類的數(shù)據(jù)綁定、類型轉(zhuǎn)換、數(shù)據(jù)校驗等等。

          不知道你有沒有注意過,平時寫接口的時候,前端傳來的參數(shù)String類型的時間字符串無法通過Spring框架本身轉(zhuǎn)換成Date類型,有部分原因就是前面提到的Spring沒有相關(guān)的Converter實(shí)現(xiàn)

          總的來說,數(shù)據(jù)綁定在xml配置和SpringMVC中用的比較多的,并且數(shù)據(jù)綁定也是Spring Bean生命周期中一個很重要的環(huán)節(jié)。

          泛型處理

          Spring為了方便操作和處理泛型類型,提供了一個強(qiáng)大的工具類——ResolvableType。

          泛型處理其實(shí)是一塊相對獨(dú)立的東西,因為它就只是一個工具類,只還不過這個工具類在Spring中卻是無處不在!

          ResolvableType提供了有一套靈活的API,可以在運(yùn)行時獲取和處理泛型類型等信息。

          ResolvableType

          接下來就通過一個案例,來看一看如何通過ResolvableType快速簡單的獲取到泛型的

          首先我聲明了一個MyMap類,繼承HashMap,第一個泛型參數(shù)是Integer類型,第二個泛型參數(shù)是List類型,List的泛型參數(shù)又是String

          public class MyMap extends HashMap<IntegerList<String>> {

          }

          接下來就來演示一下如何獲取到HashMap的泛型參數(shù)以及List的泛型參數(shù)

          第一步,先來通過ResolvableType#forClass方法創(chuàng)建一個MyMap類型對應(yīng)的ResolvableType

          //創(chuàng)建MyMap對應(yīng)的ResolvableType
          ResolvableType myMapType = ResolvableType.forClass(MyMap.class);

          因為泛型參數(shù)是在父類HashMap中,所以我們得獲取到父類HashMap對應(yīng)的ResolvableType,通過ResolvableType#getSuperType()方法獲取

          //獲取父類HashMap對應(yīng)的ResolvableType
          ResolvableType hashMapType = myMapType.getSuperType();

          接下來需要獲取HashMap的泛型參數(shù)對應(yīng)的ResolvableType類型,可以通過ResolvableType#getGeneric(int... indexes)就可以獲取指定位置的泛型參數(shù)ResolvableType,方法參數(shù)就是指第幾個位置的泛型參數(shù),從0開始

          比如獲取第一個位置的對應(yīng)的ResolvableType類型

          //獲取第一個泛型參數(shù)對應(yīng)的ResolvableType
          ResolvableType firstGenericType = hashMapType.getGeneric(0);

          現(xiàn)在有了第一個泛型參數(shù)的ResolvableType類型,只需要通過ResolvableType#resolve()方法就可以獲取到ResolvableType類型對應(yīng)的class類型,這樣就可以獲取到一個泛型參數(shù)的class類型

          //獲取第一個泛型參數(shù)對應(yīng)的ResolvableType對應(yīng)的class類型,也就是Integer的class類型
          Class<?> firstGenericClass = firstGenericType.resolve();

          如果你想獲取到HashMap第二個泛型參數(shù)的泛型類型,也就是List泛型類型就可以這么寫

          //HashMap第二個泛型參數(shù)的對應(yīng)的ResolvableType,也就是List<String>
          ResolvableType secondGenericType = hashMapType.getGeneric(1);
          //HashMap第二個泛型參數(shù)List<String>的第一個泛型類型String對應(yīng)的ResolvableType
          ResolvableType secondFirstGenericType = secondGenericType.getGeneric(0);
          //這樣就獲取到了List<String>的泛型類型String
          Class<?> secondFirstGenericClass = secondFirstGenericType.resolve();

          從上面的演示下來可以發(fā)現(xiàn),其實(shí)每變化一步,其實(shí)就是獲取對應(yīng)泛型或者是父類等等對應(yīng)的ResolvableType,父類或者是泛型參數(shù)又可能有泛型之類的,只需要一步一步獲取就可以了,當(dāng)需要獲取到具體的class類型的時候,通過ResolvableType#resolve()方法就行了。

          除了上面提到的通過ResolvableType#forClass方法創(chuàng)建ResolvableType之外,還可以通過一下幾個方法創(chuàng)建:

          • forField(Field field):獲取字段類型對應(yīng)的ResolvableType
          • forMethodReturnType(Method method):獲取方法返回值類型對應(yīng)的ResolvableType
          • forMethodParameter(Method method, int parameterIndex):獲取方法某個位置方法參數(shù)對應(yīng)的ResolvableType
          • forConstructorParameter(Constructor<?> constructor, int parameterIndex):獲取構(gòu)造方法某個構(gòu)造參數(shù)對應(yīng)的ResolvableType

          通過上面解釋可以看出,對于一個類方法參數(shù),方法返回值,字段等等都可以獲取到對應(yīng)的ResolvableType

          國際化

          國際化(Internationalization,簡稱i18n)也是Spring提供的一個核心功能,它其實(shí)也是一塊相對獨(dú)立的功能。

          所謂的國際化,其實(shí)理解簡單點(diǎn)就是對于不同的地區(qū)國家,輸出的文本內(nèi)容語言不同。

          Spring的國際化其實(shí)主要是依賴Java中的國際化和文本處理方式。

          1、Java中的國際化

          Locale

          Locale是Java提供的一個類,它可以用來標(biāo)識不同的語言和地區(qū),如en_US表示美國英語,zh_CN表示中國大陸中文等。

          目前Java已經(jīng)窮舉了很多國家的地區(qū)Locale。

          我們可以使用Locale類獲取系統(tǒng)默認(rèn)的Locale,也可以手動設(shè)置Locale,以適應(yīng)不同的語言環(huán)境。

          ResourceBundle

          ResourceBundle是一個加載本地資源的一個類,他可以根據(jù)傳入的Locale不同,加載不同的資源。

          來個demo

          首先準(zhǔn)備資源文件,資源文件通常是.properties文件,文件名命名規(guī)則如下:

          basename_lang_country.properties

          basename無所謂,叫什么都可以,而lang和country是從Locale中獲取的。

          舉個例子,我們看看英語地區(qū)的Locale

          從上圖可以看出,英語Locale的lang為en,country為空字符串,那么此時英語地區(qū)對應(yīng)資源文件就可以命名為:basename_en.properties,由于country為空字符串,可以省略

          中國大陸Locale如下圖

          此時文件就可以命為:basename_zh_CN.properties

          好了,現(xiàn)在既然知道了命名規(guī)則,我們就創(chuàng)建兩個文件,basename就叫message,一個英語,一個中文,放在classpath路徑下

          中文資源文件:message_zh_CN.properties,內(nèi)容為:

          name=三友的java日記

          英文資源文件:message_en.properties,內(nèi)容為:

          name=sanyou's java diary

          有了文件之后,就可以通過ResourceBundle#getBundle(String baseName,Locale locale)方法來獲取獲取ResourceBundle

          • 第一個參數(shù)baseName就是我們的文件名中的basename,對于我們的demo來說,就是message
          • 第二個參數(shù)就是地區(qū),根據(jù)地區(qū)的不同加載不同地區(qū)的文件

          測試一下

          public class ResourceBundleDemo {

              public static void main(String[] args) {

                  //獲取ResourceBundle,第一個參數(shù)baseName就是我們的文件名稱,第二個參數(shù)就是地區(qū)
                  ResourceBundle chineseResourceBundle = ResourceBundle.getBundle("message", Locale.SIMPLIFIED_CHINESE);
                  //根據(jù)name鍵取值
                  String chineseName = chineseResourceBundle.getString("name");
                  System.out.println("chineseName = " + chineseName);

                  ResourceBundle englishResourceBundle = ResourceBundle.getBundle("message", Locale.ENGLISH);
                  String englishName = englishResourceBundle.getString("name");
                  System.out.println("englishName = " + englishName);

              }

          }

          運(yùn)行結(jié)果

          其實(shí)運(yùn)行結(jié)果可以看出,其實(shí)是成功獲取了,只不過中文亂碼了,這主要是因為ResourceBundle底層其實(shí)編碼是ISO-8859-1,所以會導(dǎo)致亂碼。

          解決辦法最簡單就是把中文用Java Unicode序列來表示,之后就可以讀出中文了了,比如三友的java日記用Java Unicode序列表示為\u4e09\u53cb\u7684java\u65e5\u8bb0

          除了這種方式之外,其實(shí)還可以繼承ResourceBundle內(nèi)部一個Control類

          Control

          重寫newBundle方法

          newBundle

          newBundle是創(chuàng)建ResourceBundle對應(yīng)核心方法,重寫的時候你就可以隨心所欲讓它支持其它編碼方式。

          有了新的Control之后,獲取ResourceBundle時只需要通過ResourceBundle#getBundle(String baseName, Locale targetLocale,Control control)方法指定Control就可以了。

          Spring實(shí)際上就是通過這種方式擴(kuò)展,支持不同編碼的,后面也有提到。

          MessageFormat

          MessageFormat顧明思議就是把消息格式化。它可以接收一條包含占位符的消息模板,并根據(jù)提供的參數(shù)替換占位符,生成最終的消息。

          MessageFormat對于將動態(tài)值插入到消息中非常有用,如歡迎消息、錯誤消息等。

          先來個Demo

          public class MessageFormatDemo {

              public static void main(String[] args) {
                  String message = MessageFormat.format("你好:{0}""張三");
                  System.out.println("message = " + message);
              }

          }

          解釋一下上面這段代碼:

          • 你好:{0}其實(shí)就是前面提到的消息的模板,{0}就是占位符,中間的0代表消息格式化的時候?qū)?strong style="color: black;">提供的參數(shù)第一個參數(shù)替換占位符的值
          • 張三就是提供的參數(shù),你可以寫很多個,但是我們的demo只會取第一個參數(shù),因為是{0}

          所以輸出結(jié)果為:

          message = 你好:張三

          成功格式化消息。

          2、Spring國際化

          Spring提供了一個國際化接口MessageSource

          MessageSource

          他有一個基于ResourceBundle + MessageFormat的實(shí)現(xiàn)ResourceBundleMessageSource

          ResourceBundleMessageSource

          他的本質(zhì)可以在資源文件存儲消息的模板,然后通過MessageFormat來替換占位符,MessageSource的getMessage方法就可以傳遞具體的參數(shù)

          來個demo

          現(xiàn)在模擬登錄歡迎語句,對于不同的人肯定要有不同的名字,所以資源文件需要存模板,需要在不同的資源文件加不同的模板

          中文資源文件:message_zh_CN.properties

          welcome=您好:{0}

          英文資源文件:message_en.properties

          welcome=hello:{0}

          占位符,就是不同人不同名字

          測試代碼

          public class MessageSourceDemo {

              public static void main(String[] args) {
                  ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();

                  //Spring已經(jīng)擴(kuò)展了ResourceBundle的Control,支持資源文件的不同編碼方式,但是需要設(shè)置一下
                  messageSource.setDefaultEncoding("UTF-8");

                  //添加 baseName,就是前面提到的文件中的basename
                  messageSource.addBasenames("message");

                  //中文,傳個中文名字
                  String chineseWelcome = messageSource.getMessage("welcome"new Object[]{"張三"}, Locale.SIMPLIFIED_CHINESE);
                  System.out.println("chineseWelcome = " + chineseWelcome);

                  //英文,英語國家肯定是英文名
                  String englishWelcome = messageSource.getMessage("welcome"new Object[]{"Bob"}, Locale.ENGLISH);
                  System.out.println("englishWelcome = " + englishWelcome);
              }

          }

          運(yùn)行結(jié)果

          chineseWelcome = 您好:張三
          englishWelcome = hello:Bob

          成功根據(jù)完成不同國家資源的加載和模板消息的格式化。

          小結(jié)

          這里來簡單總結(jié)一下這一小節(jié)說的內(nèi)容

          • Locale:不同國家和地區(qū)的信息封裝
          • ResourceBundle:根據(jù)不同國家的Locale,加載對應(yīng)的資源文件,這個資源文件的命名需要遵守basename_lang_country.properties命名規(guī)范
          • MessageFormat:其實(shí)就是一個文本處理的方式,他可以解析模板,根據(jù)參數(shù)替換模板的占位符
          • MessageSource:Spring提供的國際化接口,其實(shí)他底層主要是依賴Java的ResourceBundle和MessageFormat,資源文件存儲模板信息,MessageFormat根據(jù)MessageSource方法的傳參替換模板中的占位符

          BeanFactory

          我們知道Spring的核心就是IOC和AOP,而BeanFactory就是大名鼎鼎的IOC容器,他可以幫我們生產(chǎn)對象。

          1、BeanFactory接口體系

          BeanFactory本身是一個接口

          BeanFactory

          從上面的接口定義可以看出從可以從BeanFactory獲取到Bean。

          他也有很多子接口,不同的子接口有著不同的功能

          • ListableBeanFactory
          • HierarchicalBeanFactory
          • ConfigurableBeanFactory
          • AutowireCapableBeanFactory

          ListableBeanFactory

          ListableBeanFactory

          從提供的方法可以看出,提供了一些獲取集合的功能,比如有的接口可能有多個實(shí)現(xiàn),通過這些方法就可以獲取這些實(shí)現(xiàn)對象的集合。

          HierarchicalBeanFactory

          HierarchicalBeanFactory

          從接口定義可以看出,可以獲取到父容器,說明BeanFactory有子父容器的概念。

          ConfigurableBeanFactory

          ConfigurableBeanFactory

          從命名可以看出,可配置BeanFactory,所以可以對BeanFactory進(jìn)行配置,比如截圖中的方法,可以設(shè)置我們前面提到的類型轉(zhuǎn)換的東西,這樣在生成Bean的時候就可以類型屬性的類型轉(zhuǎn)換了。

          AutowireCapableBeanFactory

          提供了自動裝配Bean的實(shí)現(xiàn)、屬性填充、初始化、處理獲取依賴注入對象的功能。

          比如@Autowired最終就會調(diào)用AutowireCapableBeanFactory#resolveDependency處理注入的依賴。

          其實(shí)從這里也可以看出,Spring在BeanFactory的接口設(shè)計上面還是基于不同的職責(zé)進(jìn)行接口的劃分,其實(shí)不僅僅是在BeanFactory,前面提到的那些接口也基本符合這個原則。

          2、BeanDefinition及其相關(guān)組件

          BeanDefinition

          BeanDefinition是Spring Bean創(chuàng)建環(huán)節(jié)中很重要的一個東西,它封裝了Bean創(chuàng)建過程中所需要的元信息。

          public interface BeanDefinition extends AttributeAccessorBeanMetadataElement {
              //設(shè)置Bean className
              void setBeanClassName(@Nullable String beanClassName);

              //獲取Bean className
              @Nullable
              String getBeanClassName();
              
              //設(shè)置是否是懶加載
              void setLazyInit(boolean lazyInit);

              //判斷是否是懶加載
              boolean isLazyInit();
              
              //判斷是否是單例
              boolean isSingleton();

          }

          如上代碼是BeanDefinition接口的部分方法,從這方法的定義名稱可以看出,一個Bean所創(chuàng)建過程中所需要的一些信息都可以從BeanDefinition中獲取,比如這個Bean的class類型,這個Bean是否是懶加載,這個Bean是否是單例的等等,因為有了這些信息,Spring才知道要創(chuàng)建一個什么樣的Bean。

          讀取BeanDefinition

          讀取BeanDefinition大致分為以下幾類

          • BeanDefinitionReader
          • ClassPathBeanDefinitionScanner

          BeanDefinitionReader

          BeanDefinitionReader

          BeanDefinitionReader可以通過loadBeanDefinitions(Resource resource)方法來加載BeanDefinition,方法參數(shù)就是我們前面說的資源,比如可以將Bean定義在xml文件中,這個xml文件就是一個資源

          BeanDefinitionReader的相關(guān)實(shí)現(xiàn):

          • XmlBeanDefinitionReader:讀取xml配置的Bean
          • PropertiesBeanDefinitionReader:讀取properties文件配置的Bean,是的,你沒看錯,Bean可以定義在properties文件配置中
          • AnnotatedBeanDefinitionReader:讀取通過注解定義的Bean,比如@Lazy注解等等,AnnotatedBeanDefinitionReader不是BeanDefinitionReader的實(shí)現(xiàn),但是作用是一樣的

          ClassPathBeanDefinitionScanner

          這個作用就是掃描指定包下通過@Component及其派生注解(@Service等等)注解定義的Bean,其實(shí)就是@ComponentScan注解的底層實(shí)現(xiàn)

          ClassPathBeanDefinitionScanner這個類其實(shí)在很多其它框架中都有使用到,因為這個類可以掃描指定包下,生成BeanDefinition,對于那些需要掃描包來生成BeanDefinition來說,用的很多

          比如說常見的MyBatis框架,他的注解@MapperScan可以掃描指定包下的Mapper接口,其實(shí)他也是通過繼承ClassPathBeanDefinitionScanner來掃描Mapper接口的

          BeanDefinitionRegistry

          這個從命名就可以看出,是BeanDefinition的注冊中心,也就是用來保存BeanDefinition的。

          提供了BeanDefinition的增刪查的功能。

          講到這里,就可以用一張圖來把前面提到東西關(guān)聯(lián)起來

          • 通過BeanDefinitionReader或者是ClassPathBeanDefinitionScanner為每一個Bean生成一個BeanDefinition
          • BeanDefinition生成之后,添加到BeanDefinitionRegistry中
          • 當(dāng)從BeanFactory中獲取Bean時,會從BeanDefinitionRegistry中拿出需要創(chuàng)建的Bean對應(yīng)的BeanDefinition,根據(jù)BeanDefinition的信息來生成Bean
          • 當(dāng)生成的Bean是單例的時候,Spring會將Bean保存到SingletonBeanRegistry中,也就是平時說的三級緩存中的第一級緩存中,以免重復(fù)創(chuàng)建,需要使用的時候直接從SingletonBeanRegistry中查找

          3、BeanFactory核心實(shí)現(xiàn)

          前面提到的BeanFactory體系都是一個接口,那么BeanFactory的實(shí)現(xiàn)類是哪個類呢?

          BeanFactory真正底層的實(shí)現(xiàn)類,其實(shí)就只有一個,那就是DefaultListableBeanFactory這個類,這個類以及父類真正實(shí)現(xiàn)了BeanFactory及其子接口的所有的功能。

          并且接口的實(shí)現(xiàn)上可以看出,他也實(shí)現(xiàn)了BeanDefinitionRegistry,也就是說,在底層的實(shí)現(xiàn)上,其實(shí)BeanFactory跟BeanDefinitionRegistry的實(shí)現(xiàn)是同一個實(shí)現(xiàn)類。

          上面說了這么多,來個demo

          public class BeanFactoryDemo {

              public static void main(String[] args) {
                  //創(chuàng)建一個BeanFactory
                  DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

                  //創(chuàng)建一個BeanDefinitionReader,構(gòu)造參數(shù)是一個BeanDefinitionRegistry
                  //因為DefaultListableBeanFactory實(shí)現(xiàn)了BeanDefinitionRegistry,所以直接把beanFactory當(dāng)做構(gòu)造參數(shù)傳過去
                  AnnotatedBeanDefinitionReader beanDefinitionReader = new AnnotatedBeanDefinitionReader(beanFactory);

                  //讀取當(dāng)前類 BeanFactoryDemo 為一個Bean,讓Spring幫我們生成這個Bean
                  beanDefinitionReader.register(BeanFactoryDemo.class);

                  //從容器中獲取注冊的BeanFactoryDemo的Bean
                  BeanFactoryDemo beanFactoryDemo = beanFactory.getBean(BeanFactoryDemo.class);

                  System.out.println("beanFactoryDemo = " + beanFactoryDemo);
              }

          }

          簡單說一下上面代碼的意思

          • 創(chuàng)建一個BeanFactory,就是DefaultListableBeanFactory
          • 創(chuàng)建一個AnnotatedBeanDefinitionReader,構(gòu)造參數(shù)是一個BeanDefinitionRegistry,因為BeanDefinitionReader需要把讀出來的BeanDefinition存到BeanDefinitionRegistry中,同時因為DefaultListableBeanFactory實(shí)現(xiàn)了BeanDefinitionRegistry,所以直接把beanFactory當(dāng)做構(gòu)造參數(shù)傳過去
          • 讀取當(dāng)前類 BeanFactoryDemo 為一個Bean,讓Spring幫我們生成這個Bean
          • 后面就是獲取打印

          運(yùn)行結(jié)果

          成功獲取到我們注冊的Bean

          總結(jié)

          本節(jié)主要講了實(shí)現(xiàn)IOC的幾個核心的組件

          BeanFactory及其接口體系:

          • ListableBeanFactory
          • HierarchicalBeanFactory
          • ConfigurableBeanFactory
          • AutowireCapableBeanFactory

          BeanDefinition及其相關(guān)組件:

          • BeanDefinition
          • BeanDefinitionReader和ClassPathBeanDefinitionScanner:讀取資源,生成BeanDefinition
          • BeanDefinitionRegistry:存儲BeanDefinition

          BeanFactory核心實(shí)現(xiàn):

          • DefaultListableBeanFactory:IOC容器,同時實(shí)現(xiàn)了BeanDefinitionRegistry接口

          ApplicationContext

          終于講到了ApplicationContext,因為前面說的那么多其實(shí)就是為ApplicationContext做鋪墊的

          先來看看ApplicationContext的接口

          你會驚訝地發(fā)現(xiàn),ApplicationContext繼承的幾個接口,除了EnvironmentCapable和ApplicationEventPublisher之外,其余都是前面說的。

          EnvironmentCapable這個接口比較簡單,提供了獲取Environment的功能

          EnvironmentCapable

          說明了可以從ApplicationContext中獲取到Environment,所以EnvironmentCapable也算是前面說過了

          至于ApplicationEventPublisher我們留到下一節(jié)說。

          ApplicationContext也繼承了ListableBeanFactory和HierarchicalBeanFactory,也就說明ApplicationContext其實(shí)他也是一個BeanFactory,所以說ApplicationContext是IOC容器的說法也沒什么毛病,但是由于他還繼承了其它接口,功能比BeanFactory多多了。

          所以,ApplicationContext是一個集萬千功能為一身的接口,一旦你獲取到了ApplicationContext(可以@Autowired注入),你就可以用來獲取Bean、加載資源、獲取環(huán)境,還可以國際化一下,屬實(shí)是個王炸。

          雖然ApplicationContext繼承了這些接口,但是ApplicationContext對于接口的實(shí)現(xiàn)是通過一種委派的方式,而真正的實(shí)現(xiàn)都是我們前面說的那些實(shí)現(xiàn)

          什么叫委派呢,咱寫一個例子你就知道了

          public class MyApplicationContext implements ApplicationContext {

              private final ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();

              @Override
              public Resource[] getResources(String locationPattern) throws IOException {
                  return resourcePatternResolver.getResources(locationPattern);
              }
              
          }

          如上,其實(shí)是一段偽代碼

          因為ApplicationContext繼承了ResourcePatternResolver接口,所以我實(shí)現(xiàn)了getResources方法,但是真正的實(shí)現(xiàn)其實(shí)是交給變量中的PathMatchingResourcePatternResolver來實(shí)現(xiàn)的,這其實(shí)就是委派,不直接實(shí)現(xiàn),而是交給其它真正實(shí)現(xiàn)了這個接口的類來處理

          同理,ApplicationContext對于BeanFactory接口的實(shí)現(xiàn)其實(shí)最終也是交由DefaultListableBeanFactory來委派處理的。

          委派這種方式在Spring內(nèi)部還是用的非常多的,前面提到的某些接口在的實(shí)現(xiàn)上也是通過委派的方式來的

          ApplicationContext有一個子接口,ConfigurableApplicationContext

          從提供的方法看出,就是可以對ApplicationContext進(jìn)行配置,比如設(shè)置Environment,同時也能設(shè)置parent,說明了ApplicationContext也有子父的概念

          我們已經(jīng)看到了很多以Configurable開頭的接口,這就是命名習(xí)慣,表示了可配置的意思,提供的都是set、add之類的方法

          ApplicationContext的實(shí)現(xiàn)很多,但是他有一個非常重要的抽象實(shí)現(xiàn)AbstractApplicationContext,因為其它的實(shí)現(xiàn)都是繼承這個抽象實(shí)現(xiàn)

          AbstractApplicationContext

          這個類主要是實(shí)現(xiàn)了一些繼承的接口方法,通過委派的方式,比如對于BeanFactory接口的實(shí)現(xiàn)

          并且AbstractApplicationContext這個類也實(shí)現(xiàn)了一個非常核心的refresh方法

          所有的ApplicationContext在創(chuàng)建之后必須調(diào)用這個refresh方法之后才能使用,至于這個方法干了哪些事,后面有機(jī)會再寫一篇文章來著重扒一扒。

          事件

          上一小節(jié)在說ApplicationContext繼承的接口的時候,我們留下了一個懸念,那就是ApplicationEventPublisher的作用,而ApplicationEventPublisher就跟本節(jié)要說的事件有關(guān)。

          Spring事件是一種觀察者模式的實(shí)現(xiàn),他的作用主要是用來解耦合的。

          當(dāng)發(fā)生了某件事,只要發(fā)布一個事件,對這個事件的監(jiān)聽者(觀察者)就可以對事件進(jìn)行響應(yīng)或者處理。

          舉個例子來說,假設(shè)發(fā)生了火災(zāi),可能需要打119、救人,那么就可以基于事件的模型來實(shí)現(xiàn),只需要打119、救人監(jiān)聽火災(zāi)的發(fā)生就行了,當(dāng)發(fā)生了火災(zāi),通知這些打119、救人去觸發(fā)相應(yīng)的邏輯操作。

          1、什么是Spring Event 事件

          Spring Event 事件就是Spring實(shí)現(xiàn)了這種事件模型,你只需要基于Spring提供的API進(jìn)行擴(kuò)展,就可以輕易地完成事件的發(fā)布與訂閱

          Spring事件相關(guān)api主要有以下幾個:

          • ApplicationEvent
          • ApplicationListener
          • ApplicationEventPublisher

          ApplicationEvent

          ApplicationEvent

          事件的父類,所有具體的事件都得繼承這個類,構(gòu)造方法的參數(shù)是這個事件攜帶的參數(shù),監(jiān)聽器就可以通過這個參數(shù)來進(jìn)行一些業(yè)務(wù)操作。

          ApplicationListener

          ApplicationListener

          事件監(jiān)聽的接口,泛型是需要監(jiān)聽的事件類型,子類需要實(shí)現(xiàn)onApplicationEvent,參數(shù)就是監(jiān)聽的事件類型,onApplicationEvent方法的實(shí)現(xiàn)就代表了對事件的處理,當(dāng)事件發(fā)生時,Spring會回調(diào)onApplicationEvent方法的實(shí)現(xiàn),傳入發(fā)布的事件。

          ApplicationEventPublisher

          ApplicationEventPublisher

          上一小節(jié)留下來的接口,事件發(fā)布器,通過publishEvent方法就可以發(fā)布一個事件,然后就可以觸發(fā)監(jiān)聽這個事件的監(jiān)聽器的回調(diào)。

          ApplicationContext繼承了ApplicationEventPublisher,說明只要有ApplicationContext就可以來發(fā)布事件了。

          話不多說,上代碼

          就以上面的火災(zāi)為例

          創(chuàng)建一個火災(zāi)事件類

          火災(zāi)事件類繼承ApplicationEvent

          // 火災(zāi)事件
          public class FireEvent extends ApplicationEvent {

              public FireEvent(String source) {
                  super(source);
              }

          }

          創(chuàng)建火災(zāi)事件的監(jiān)聽器

          打119的火災(zāi)事件的監(jiān)聽器:

          public class Call119FireEventListener implements ApplicationListener<FireEvent{

              @Override
              public void onApplicationEvent(FireEvent event) {
                  System.out.println("打119");
              }

          }

          救人的火災(zāi)事件的監(jiān)聽器:

          public class SavePersonFireEventListener implements ApplicationListener<FireEvent{

              @Override
              public void onApplicationEvent(FireEvent event) {
                  System.out.println("救人");
              }

          }

          事件和對應(yīng)的監(jiān)聽都有了,接下來進(jìn)行測試:

          public class Application {

              public static void main(String[] args) {
                  //創(chuàng)建一個Spring容器
                  AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
                  //將 事件監(jiān)聽器 注冊到容器中
                  applicationContext.register(Call119FireEventListener.class);
                  applicationContext.register(SavePersonFireEventListener.class);
                  applicationContext.refresh();

                  // 發(fā)布著火的事件,觸發(fā)監(jiān)聽
                  applicationContext.publishEvent(new FireEvent("著火了"));
              }

          }

          將兩個事件注冊到Spring容器中,然后發(fā)布FireEvent事件

          運(yùn)行結(jié)果:

          119
          救人

          控制臺打印出了結(jié)果,觸發(fā)了監(jiān)聽。

          如果現(xiàn)在需要對火災(zāi)進(jìn)行救火,那么只需要去監(jiān)聽FireEvent,實(shí)現(xiàn)救火的邏輯,注入到Spring容器中,就可以了,其余的代碼根本不用動。

          2、Spring內(nèi)置的事件

          Spring內(nèi)置的事件很多,這里我羅列幾個

          事件類型 觸發(fā)時機(jī)
          ContextRefreshedEvent 在調(diào)用ConfigurableApplicationContext 接口中的refresh()方法時觸發(fā)
          ContextStartedEvent 在調(diào)用ConfigurableApplicationContext的start()方法時觸發(fā)
          ContextStoppedEvent 在調(diào)用ConfigurableApplicationContext的stop()方法時觸發(fā)
          ContextClosedEvent 當(dāng)ApplicationContext被關(guān)閉時觸發(fā)該事件,也就是調(diào)用close()方法觸發(fā)

          在ApplicationContext(Spring容器)啟動的過程中,Spring會發(fā)布這些事件,如果你需要這Spring容器啟動的某個時刻進(jìn)行什么操作,只需要監(jiān)聽對應(yīng)的事件即可。

          3、Spring事件的傳播特性

          Spring事件的傳播是什么意思呢?

          前面提到,ApplicationContext有子父容器的概念,而Spring事件的傳播就是指當(dāng)通過子容器發(fā)布一個事件之后,不僅可以觸發(fā)在這個子容器的事件監(jiān)聽器,還可以觸發(fā)在父容器的這個事件的監(jiān)聽器。

          上代碼

          public class EventPropagateApplication {

              public static void main(String[] args) {

                  // 創(chuàng)建一個父容器
                  AnnotationConfigApplicationContext parentApplicationContext = new AnnotationConfigApplicationContext();
                  //將 打119監(jiān)聽器 注冊到父容器中
                  parentApplicationContext.register(Call119FireEventListener.class);
                  parentApplicationContext.refresh();

                  // 創(chuàng)建一個子容器
                  AnnotationConfigApplicationContext childApplicationContext = new AnnotationConfigApplicationContext();
                  //將 救人監(jiān)聽器 注冊到子容器中
                  childApplicationContext.register(SavePersonFireEventListener.class);
                  childApplicationContext.refresh();

                  // 設(shè)置一下父容器
                  childApplicationContext.setParent(parentApplicationContext);

                  // 通過子容器發(fā)布著火的事件,觸發(fā)監(jiān)聽
                  childApplicationContext.publishEvent(new FireEvent("著火了"));

              }

          }

          創(chuàng)建了兩個容器,父容器注冊了打119的監(jiān)聽器,子容器注冊了救人的監(jiān)聽器,然后將子父容器通過setParent關(guān)聯(lián)起來,最后通過子容器,發(fā)布了著火的事件。

          運(yùn)行結(jié)果:

          救人
          119

          從打印的日志,的確可以看出,雖然是子容器發(fā)布了著火的事件,但是父容器的監(jiān)聽器也成功監(jiān)聽了著火事件。

          而這種傳播特性,從源碼中也可以看出來

          事件傳播源碼

          如果父容器不為空,就會通過父容器再發(fā)布一次事件。

          傳播特性的一個小坑

          前面說過,在Spring容器啟動的過程,會發(fā)布很多事件,如果你需要有相應(yīng)的擴(kuò)展,可以監(jiān)聽這些事件。

          但是,不知道你有沒有遇到過這么一個坑,就是在SpringCloud環(huán)境下,你監(jiān)聽這些Spring事件的監(jiān)聽器會執(zhí)行很多次,這其實(shí)就是跟傳播特性有關(guān)。

          在SpringCloud環(huán)境下,為了使像FeignClient和RibbonClient這些不同服務(wù)的配置相互隔離,會為每個FeignClient或者是RibbonClient創(chuàng)建一個Spring容器,而這些容器都有一個公共的父容器,那就是SpringBoot項目啟動時創(chuàng)建的容器

          假設(shè)你監(jiān)聽了容器刷新的ContextRefreshedEvent事件,那么你自己寫的監(jiān)聽器就在SpringBoot項目啟動時創(chuàng)建的容器中

          每個服務(wù)的配置容器他也是Spring容器,啟動時也會發(fā)布ContextRefreshedEvent,那么由于傳播特性的關(guān)系,你的事件監(jiān)聽器就會觸發(fā)執(zhí)行多次

          如何解決這個坑呢?

          你可以進(jìn)行判斷這些監(jiān)聽器有沒有執(zhí)行過,比如加一個判斷的標(biāo)志;或者是監(jiān)聽類似的事件,比如ApplicationStartedEvent事件,這種事件是在SpringBoot啟動中發(fā)布的事件,而子容器不是SpringBoot,所以不會多次發(fā)這種事件,也就會只執(zhí)行一次。

          總結(jié)

          到這到這整篇文章終于寫完了,這里再來簡單地回顧一下本文說的幾個核心功能:

          • 資源管理:對資源進(jìn)行統(tǒng)一的封裝,方便資源讀取和管理
          • 環(huán)境:對容器或者是項目的配置進(jìn)行管理
          • 類型轉(zhuǎn)換:將一種類型轉(zhuǎn)換成另一種類型
          • 數(shù)據(jù)綁定:將數(shù)據(jù)跟對象的屬性進(jìn)行綁定,綁定之前涉及到類型轉(zhuǎn)換
          • 泛型處理:一個操作泛型的工具類,Spring中到處可見
          • 國際化:對Java的國際化進(jìn)行了統(tǒng)一的封裝
          • BeanFactory:IOC容器
          • ApplicationContext:一個集萬千功能于一身的王炸接口,也可以說是IOC容器
          • 事件:Spring提供的基于觀察者模式實(shí)現(xiàn)的解耦合利器

          當(dāng)然除了上面,Spring還有很多其它核心功能,就比如AOP、SpEL表達(dá)式等等

          由于AOP涉及到Bean生命周期,本篇文章也沒有涉及到Bean生命周期的講解,所以這里就不講了,后面有機(jī)會再講

          至于SpEL他是Spring提供的表達(dá)式語言,主要是語法,解析語法的一些東西,這里也就不講了

          好了,本文就講到這里,如果覺得本篇文章對你有所幫助,還請多多點(diǎn)贊、轉(zhuǎn)發(fā)、在看,非常感謝!!

          參考資料:

          [1].《極客時間--小馬哥講Spring核心編程思想》 [2].https://blog.csdn.net/zzuhkp/article/details/119455964 [3].https://blog.csdn.net/zzuhkp/article/details/119455948 [4].https://blog.csdn.net/u010086122/article/details/81566515

          ··············  END  ·············

          往期熱門文章推薦

          如何去閱讀源碼,我總結(jié)了18條心法

          如何寫出漂亮代碼,我總結(jié)了45個小技巧

          三萬字盤點(diǎn)Spring/Boot的那些常用擴(kuò)展點(diǎn)

          兩萬字盤點(diǎn)那些被玩爛了的設(shè)計模式

          萬字+20張圖探秘Nacos注冊中心核心實(shí)現(xiàn)原理

          萬字+20張圖剖析Spring啟動時12個核心步驟

          1.5萬字+30張圖盤點(diǎn)索引常見的11個知識點(diǎn)

          瀏覽 36
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日韩欧美视频 | 亚洲色图21p | 国产蜜臀AV一区二区 | 精品八区 | 欧美极品网站 |