由@Value註解引發的一次對bean的思考,值得一看

讀取配置文件屬性

前不久剛優化了一個關於文件上傳這塊的代碼,這裏面就涉及到圖片的路徑問題,我將某些屬性配置到了配置文件,但是在優化過程中,讓我對bean有了新的認識,既然牽扯到配置文件屬性的讀取,那麼我們先來看看怎麼獲取配置文件的屬性。

@Value註解

在配置文件中編寫一些屬性(application.yml)

server:
  port: 9199

file:
  server-name: oss.file.com/    #服務器的域名
  mapper: file/    #映射的名字
  images: images/   #存儲圖片的文件夾
  video: video/        #存放視頻的文件夾

FileConfig類

package com.ymy.config;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Data
@Configuration
public class FileConfig {
    
    /**
     * 服務器名稱
     */
    @Value("${file.server-name}")
    private  String serverName;

    /**
     * 映射
     */
    @Value("${file.mapper}")
    private String mapper;

    /**
     * 存放圖片的文件夾
     */
    @Value("${file.images}")
    private String images;


    /**
     * 存放視頻的文件夾
     */
    @Value("${file.video}")
    private String video;

}

@Data:lombok依賴中的註解,他會生成對應的get、Set、toString方法,簡便了我們的代碼量。
@Configuration 裝配,被它裝配的bean,回被spring的上下文掃描,這個有點類似springmvc的xml配中的bean,這個註解一定要加,否者讀取不到配置文件,當然也可以替換成@Component
@Value:用於讀取配置文件的屬性,格式 ${對應配置文件中的屬性}。

單元測試

package com.ymy;

import com.ymy.config.FileConfig;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@Slf4j
class StaticConfigVarApplicationTests {

    @Autowired
    private FileConfig fileConfig;

    @Test
    void valueTest() {
        //比如images目錄下存放着一張123.jpg的圖片;video目錄下存放着123.mp4
        log.info("服務名:{},映射:{},存儲圖片的文件夾:{},存放視頻的文件夾:{}",fileConfig.getServerName(),fileConfig.getMapper(),fileConfig.getImages(),fileConfig.getVideo());
        log.info("圖片目錄:{}",fileConfig.getServerName()+fileConfig.getMapper()+fileConfig.getImages()+"123.jpg");
        log.info("視頻目錄:{}",fileConfig.getServerName()+fileConfig.getMapper()+fileConfig.getVideo()+"123.mp4");
    }

}

測試結果如下:

2020-03-19 15:56:42.227  INFO 19884 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 服務名:oss.file.com/,映射:file/,存儲圖片的文件夾:images/,存放視頻的文件夾:video/
2020-03-19 15:56:42.229  INFO 19884 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 圖片目錄:oss.file.com/file/images/123.jpg
2020-03-19 15:56:42.230  INFO 19884 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 視頻目錄:oss.file.com/file/video/123.mp4

看到這樣的結果,說明讀取配之文件已經成功了,但是這個還有一點讓我很不爽,那就是FileConfig對象還需要注入,能不能不通過注入就能拿到配置文件的屬性呢?這個我等會再說,我們來看看還有沒有其他讀取配置文件的方式呢?答案肯定是有的,接着往下看。

@ConfigurationProperties

它的實現方式很簡單,只需要改動一個地方即可,請看代碼:

package com.ymy.config;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Slf4j
@Configuration
@ConfigurationProperties(prefix = "file")
public class FileConfig {

    /**
     * 服務器名稱
     */
    //@Value("${file.server-name}")
    private  String serverName;

    /**
     * 映射
     */
    //@Value("${file.mapper}")
    private String mapper;

    /**
     * 存放圖片的文件夾
     */
    //@Value("${file.images}")
    private String images;


    /**
     * 存放視頻的文件夾
     */
   // @Value("${file.video}")
    private String video;
}

我們在屬性上去掉了@Value註解,在bean中加入了:@ConfigurationProperties(prefix = “file”)。
@ConfigurationProperties():這個註解可以讓bean的屬性與配置文件的屬性一一對應,prefix:配置文件的前綴,格式要求:bean屬性要與配置文件的屬性名相同。

其他代碼都不需要改動,我們直接運行單元測試:

2020-03-19 16:21:00.271  INFO 1456 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 服務名:oss.file.com/,映射:file/,存儲圖片的文件夾:images/,存放視頻的文件夾:video/
2020-03-19 16:21:00.273  INFO 1456 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 圖片目錄:oss.file.com/file/images/123.jpg
2020-03-19 16:21:00.273  INFO 1456 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 視頻目錄:oss.file.com/file/video/123.mp4

我們發現同樣讀取到了配置文件的信息,這種方式要求bean的屬性名與配置文件的屬性名相同,否者將讀取不到,不知道你們有沒有發現一個問題,那就是FileConfig定義的服務器名稱是:serverName,配置文件配置的是:server-name,很神奇的是他居然找到了,爲什麼呢?
@ConfigurationProperties會默認去掉屬性名中間的特殊符號,並且不區分大小寫

Environment對象

上面的兩種讀取方式可能很多人都知道,但是有人瞭解過Environment這個對象嗎?

Environment是集成在容器中的抽象,它爲 application 環境的兩個 key 方面建模:profiles和properties。

profile 是 bean 定義的命名邏輯 group,僅當給定的 profile 爲 active 時才向容器註冊。 Beans 可以分配給 profile,無論是用 XML 定義還是通過 annotations 定義。與 profiles 相關的Environment object 的作用是確定哪些 profiles(如果有)當前是 active,以及哪些 profiles(如果有)默認情況下應該 active。

Properties 在幾乎所有 applications 中都發揮着重要作用,可能源自各種來源:properties files,JVM 系統 properties,系統環境變量,JNDI,servlet context 參數,ad-hoc Properties objects,Maps 等。與 properties 相關的Environment object 的作用是爲用戶提供方便的服務接口,用於配置 property 源和從中解析 properties。

這是官網的介紹,看着是否很暈?我們只需要抓中重點即可,那就是Environment管理着所有的配置文件。

我們一起來看看Environment是如何獲取配置文件屬性的,上代碼:

package com.ymy;

import com.ymy.config.FileConfig;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;

@SpringBootTest
@Slf4j
class StaticConfigVarApplicationTests {

//    @Autowired
//    private FileConfig fileConfig;

    @Autowired
    private Environment environment;

//    @Test
//    void valueTest() {
//        //比如images目錄下存放着一張123.jpg的圖片;video目錄下存放着123.mp4
//        log.info("服務名:{},映射:{},存儲圖片的文件夾:{},存放視頻的文件夾:{}",fileConfig.getSERVERNAME(),fileConfig.getMapper(),fileConfig.getImages(),fileConfig.getVideo());
//        log.info("圖片目錄:{}",fileConfig.getSERVERNAME()+fileConfig.getMapper()+fileConfig.getImages()+"123.jpg");
//        log.info("視頻目錄:{}",fileConfig.getSERVERNAME()+fileConfig.getMapper()+fileConfig.getVideo()+"123.mp4");
//    }
    
    
    @Test
    void environmentTest() {
        log.info("服務名:{}",environment.getProperty("file.server-name"));
        log.info("映射:{}",environment.getProperty("file.mapper"));
        log.info("存放圖片的文件夾:{}",environment.getProperty("file.imagese"));
        log.info("存放視頻的文件夾:{}",environment.getProperty("file.video"));
    }  
}

我們運行單元測試查看結果

2020-03-19 21:20:19.855  INFO 1680 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 服務名:oss.file.com/
2020-03-19 21:20:19.857  INFO 1680 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 映射:file/
2020-03-19 21:20:19.857  INFO 1680 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 存放圖片的文件夾:images/
2020-03-19 21:20:19.857  INFO 1680 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 存放視頻的文件夾:video/

沒錯,就是這麼簡單,是不是感覺到上面的三種獲取配置文件的方式很絲滑?其實還有幾種實現方式,我這裏做一下代碼的展示,具體的測試已經結果就不做展示了,爲了開篇說的問題能儘快和我們見面,下面我直接進行重點的代碼展示。

Properties讀取

package com.ymy.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.core.io.support.PropertiesLoaderUtils;

import java.io.IOException;
import java.util.Properties;

@Slf4j
public class MyPropertiesConfig {

    /**
     * 讀取配置文件
     * @param relpath  相對路徑   classpath目錄下
     * @return
     */
    public static Properties get(String relpath){
        Properties p = null;
        try {
             p = PropertiesLoaderUtils.loadProperties(new EncodedResource(new ClassPathResource(relpath),"utf-8"));
        } catch (IOException e) {
            e.printStackTrace();
        }
        return p;
    }
}

@Test
    void PeopertiesTest() {
        Properties properties = MyPropertiesConfig.get("application.yml");
        System.out.println(properties.getProperty("server-name"));
    }

Properties讀取的配置文件屬性和之前的幾種有點不一樣,它不需要前綴,因爲Properties是將配置文件的每一行做處理,比如file下面的server-name被拆分成了兩個鍵值對,file="" servername=oss.file.com/,所以使用這種方式讀取配置文件的時候需要注意。

@PropertySource

這種讀取方式會有一種弊端,那就是會將註釋一起讀取出來,所以再使用這種方式讀取配置文件信息的時候一定要注意,話不多說,直接上代碼

package com.ymy.config;

import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Data
@Slf4j
@Configuration
@ConfigurationProperties
@PropertySource(value = "classpath:application.yml",encoding = "utf-8")
public class FileConfigByPropertisSource {

    /**
     * 服務器名稱
     */
    //@Value("${file.server-name}")
    private  String servername;

    /**
     * 映射
     */
    //@Value("${file.mapper}")
    private String mapper;

    /**
     * 存放圖片的文件夾
     */
    //@Value("${file.images}")
    private String images;


    /**
     * 存放視頻的文件夾
     */
   // @Value("${file.video}")
    private String video;
}

@Test
    void fileConfigByPropertisSourceTest() {
        log.info("服務名:{}",fileConfigByPropertisSource.getServername());
    }

這種讀取方式還是藉助了@ConfigurationProperties,比較好的一點是它可以加載指定的配置文件,這裏我沒有指定前綴,所以這裏肯定是讀取不到的,要想得到我們在配置文件中配置的那幾個屬性,我們還是需要@ConfigurationProperties(prefix = “file”)這個玩意,@ConfigurationProperties配合@PropertySource的優勢可以讀取指定的配置文件,如果沒有@PropertySource,那麼只能讀取到application/yml/application.properties文件。

讀取配置文件的方式就先介紹到這裏,下面我們一起來看看我遇到的是什麼問題?是一個很詭異的問題。

靜態變量讀取配置文件屬性

沒錯,我是在使用靜態變量讀取配置文件屬性的時候發生的詭異問題,我先說說是怎麼發生的,然後在用代碼走一遍。

我在優化文件上傳所以來的配置文件的時候發現使用@Value註解獲取的配置文件屬性,需要在使用的地方注入管理配置文件的Bean,這樣我看着很不爽,所以我想到使用靜態變量去讀取這些配置屬性,請看代碼。

package com.ymy.config;

import com.ymy.enums.FileTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class FileConfig {
    /**
     * 服務器名稱
     */
    private static   String servername;

    /**
     * 映射
     */
    private static String mapper;

    /**
     * 存放圖片的文件夾
     */
    private static String images;


    /**
     * 存放視頻的文件夾
     */
    private static String video;

    /**
     * 存放apk的文件夾
     */
    private static String apk;


    @Value("${file.server-name}")
    public void setServername(String servername) {
        this.servername = servername;
    }
    @Value("${file.mapper}")
    public void setMapper(String mapper) {
        this.mapper = mapper;
    }
    @Value("${file.images}")
    public void setImages(String images) {
        this.images = images;
    }
    @Value("${file.video}")
    public void setVideo(String video) {
        this.video = video;
    }
    @Value("${file.apk}")
    public  void setApk(String apk) {
        FileConfig.apk = apk;
    }

    public static String getImages() {
        return images;
    }

    public static String getVideo() {
        return video;
    }

    public static String getApk() {
        return apk;
    }

    /**
     * 根據圖片的相對路徑查詢展示的絕對路徑
     * @param relPath
     * @return
     */
    public static String getImagePath(String relPath){

        return getlocation(relPath,FileTypeEnum.IMG.value());
    }

    /**
     * 根據視頻的相對路徑查詢展示的絕對路徑
     * @param relPath
     * @return
     */
    public static String getVideoPath(String relPath){

        return getlocation(relPath,FileTypeEnum.VIDEO.value());
    }

    /**
     * 根據apk的相對路徑查詢展示的絕對路徑
     * @param relPath
     * @return
     */
    public static String getApkPath(String relPath){

        return getlocation(relPath,FileTypeEnum.APK.value());
    }


    /**
     * 根據相對路徑與類型獲取到對應的絕對路徑
     * @param relPath 相對路徑
     * @param type 類型
     * @return
     */
    private static String getlocation(String relPath, Integer type) {

        return servername+mapper+FileTypeFactory.map.get(type)+relPath;
    }
}

解釋一下上面的代碼
1.定義靜態變量接收配置文件屬性
2.分別給上set方法(set方法必須是靜態的)。
3.在set方法上使用@Value註解。
4.裝配bean,在類中加上@Configuration註解

這樣,就能讓靜態變量獲取到配置文件的屬性了,而我有繼續做了一個騷操作,那就是使用一個枚舉類維護文件的類型,比如:1=圖片;2=視頻;3=apk,然後又創建了一個工廠,用於管理這三種類型,代碼如下:

枚舉類

package com.ymy.enums;

public enum FileTypeEnum {
    /**
     * 圖片
     */
    IMG(1),
    /**
     * 視頻
     */
    VIDEO(2),
    /**
     * apk
     */
    APK(3);

    private  Integer index;

    private FileTypeEnum(Integer value) {    //    必須是private的,否則編譯錯誤
        this.index = value;
    }

    public Integer value() {
        return this.index;
    }
}

工廠類

package com.ymy.config;

import com.ymy.enums.FileTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Configuration
@Slf4j
public class FileTypeFactory {
    public static final Map<Integer,String> map = new HashMap<Integer, String>();
    static { 
        map.put(FileTypeEnum.IMG.value(), FileConfig.getImages());
        map.put(FileTypeEnum.VIDEO.value(),FileConfig.getVideo());
        map.put(FileTypeEnum.APK.value(),FileConfig.getApk()); 
    }
}

然而問題也就隨着到來了,請看單元測試

 @Test
    void pathTest() {
        log.info("圖片文件夾:{},視頻文件夾:{},apk文件夾:{}",FileConfig.getImages(),FileConfig.getVideo(),FileConfig.getApk());
        String relImgPath = "123.png";
        String relVideoPath = "123.mp4";
        String relApkPath = "123.apk";
        log.info("圖片的絕對路徑:{}",FileConfig.getImagePath(relImgPath));
        log.info("視頻的絕對路徑:{}",FileConfig.getVideoPath(relVideoPath));
        log.info("apk的絕對路徑:{}",FileConfig.getApkPath(relApkPath));
    }

這個時候我們會發現一個神奇的問題,那就是獲取文件絕對路徑中有null值,但是分別獲取他們的文件夾名稱卻有值,請看結果:

2020-03-20 15:19:44.545  INFO 16956 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 圖片文件夾:images/,視頻文件夾:video/,apk文件夾:apk/
2020-03-20 15:19:44.547  INFO 16956 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 圖片的絕對路徑:oss.file.com/file/null123.png
2020-03-20 15:19:44.547  INFO 16956 --- [           main] com.ymy.StaticConfigVarApplicationTests  : 視頻的絕對路徑:oss.file.com/file/null123.mp4
2020-03-20 15:19:44.547  INFO 16956 --- [           main] com.ymy.StaticConfigVarApplicationTests  : apk的絕對路徑:oss.file.com/file/null123.apk

我就是用工廠代理了一下,就找不到目錄了?
在這裏插入圖片描述
FileConfig.getImages()能拿到值說明靜態變量images已經獲取到了配置文件的屬性,但爲什麼被工廠代理一下這個值就沒了呢?

這心態崩了啊,這能忍?我決定要深挖到底,我要搞明白是什麼原因讓我拿不到這三個值,如何分析問題?那我們就需要從bean的執行順序講起了。

Bean內部代碼加載順序

bean內部有靜態代碼塊、代碼塊、構造函數,還有一些方法,但是隻有前三個在實例化Bean的時候都會被執行,那麼他們的執行順序是什麼樣的?

我們直接用代碼接證明它們的執行順序,我們就改造一下FileTypeFactory這個工廠

package com.ymy.config;

import com.ymy.enums.FileTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

@Slf4j
public class FileTypeFactory {

    public FileTypeFactory(){
        log.info("我是文件類型工廠的構造函數,我被加載了");
    }

    public static final Map<Integer,String> map = new HashMap<Integer, String>();
    static {
        log.info("我是文件類型工廠的靜態代碼塊,我被加載了");
        map.put(FileTypeEnum.IMG.value(), FileConfig.getImages());
        map.put(FileTypeEnum.VIDEO.value(),FileConfig.getVideo());
        map.put(FileTypeEnum.APK.value(),FileConfig.getApk());
    }


    {
        log.info("我是文件類型工廠的代碼塊,我被加載了");
    }
}

單元測試

@Test
    void loadTest() {

        FileTypeFactory f1 = new FileTypeFactory();

        FileTypeFactory f2 = new FileTypeFactory();

    }

FileTypeFactory對象被我實例化了兩次,至於爲什麼要實例化兩次,一次不就能看到它們的執行順序了嗎?我們先來看執行結果

2020-03-20 15:52:02.700  INFO 12940 --- [           main] com.ymy.config.FileTypeFactory           : 我是文件類型工廠的靜態代碼塊,我被加載了
2020-03-20 15:52:02.701  INFO 12940 --- [           main] com.ymy.config.FileTypeFactory           : 我是文件類型工廠的代碼塊,我被加載了
2020-03-20 15:52:02.701  INFO 12940 --- [           main] com.ymy.config.FileTypeFactory           : 我是文件類型工廠的構造函數,我被加載了
2020-03-20 15:52:02.701  INFO 12940 --- [           main] com.ymy.config.FileTypeFactory           : 我是文件類型工廠的代碼塊,我被加載了
2020-03-20 15:52:02.701  INFO 12940 --- [           main] com.ymy.config.FileTypeFactory           : 我是文件類型工廠的構造函數,我被加載了

初始化第一個實例:靜態代碼塊最先加載 -->代碼塊被加載 —>構造函數被加載

所以我們基本上可以得出一個結論:靜態代碼塊 >代碼塊>構造函數

現在請看初始化第二個實例,我們發現居然只執行了代碼塊與構造函數,所以在這裏我們又可以得到一個結論,那就是:靜態代碼塊只會在第一次實例化bean的時候被加載

Bean與Bean之前的執行順序

我現在想知道bean與bean之前的執行順序,我新建了三個bean:user1、user2、user3,並且分別在它們內部寫上了靜態代碼塊、代碼塊以及構造函數,然後我們運行主程序看看這三個bean的加載順序。

package com.ymy.test;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class User1 {

    public User1(){
        log.info("我是User1的構造函數");
    }

    {
        log.info("我是User1的代碼塊");
    }
    static {
        log.info("我是User1的靜態代碼塊");
    }
}

package com.ymy.test;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class User2 {

    public User2(){
        log.info("我是User2的構造函數");
    }

    {
        log.info("我是User2的代碼塊");
    }
    static {
        log.info("我是User2的靜態代碼塊");
    }
}


package com.ymy.test;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;

@Slf4j
@Configuration
public class User3 {

    public User3(){
        log.info("我是User3的構造函數");
    }

    {
        log.info("我是User3的代碼塊");
    }
    static {
        log.info("我是User3的靜態代碼塊");
    }
}


請注意,我這裏使用了@Configuration註解,由於我這裏新建的是springboot項目,所以我直接啓動mian方法,我們一起來看執行結果:

2020-03-20 16:11:40.885  INFO 8932 --- [           main] com.ymy.StaticConfigVarApplication       : Starting StaticConfigVarApplication on LAPTOP-3GLHJRE9 with PID 8932 (D:\springboot\static-config-var\target\classes started by admin in D:\springboot)
2020-03-20 16:11:40.887  INFO 8932 --- [           main] com.ymy.StaticConfigVarApplication       : No active profile set, falling back to default profiles: default
2020-03-20 16:11:41.447  INFO 8932 --- [           main] com.ymy.test.User1                       : 我是User1的靜態代碼塊
2020-03-20 16:11:41.448  INFO 8932 --- [           main] com.ymy.test.User2                       : 我是User2的靜態代碼塊
2020-03-20 16:11:41.451  INFO 8932 --- [           main] com.ymy.test.User3                       : 我是User3的靜態代碼塊
2020-03-20 16:11:41.692  INFO 8932 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 9199 (http)
2020-03-20 16:11:41.699  INFO 8932 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-03-20 16:11:41.699  INFO 8932 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.31]
2020-03-20 16:11:41.778  INFO 8932 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-03-20 16:11:41.779  INFO 8932 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 858 ms
2020-03-20 16:11:41.831  INFO 8932 --- [           main] com.ymy.test.User1                       : 我是User1的代碼塊
2020-03-20 16:11:41.831  INFO 8932 --- [           main] com.ymy.test.User1                       : 我是User1的構造函數
2020-03-20 16:11:41.831  INFO 8932 --- [           main] com.ymy.test.User2                       : 我是User2的代碼塊
2020-03-20 16:11:41.831  INFO 8932 --- [           main] com.ymy.test.User2                       : 我是User2的構造函數
2020-03-20 16:11:41.832  INFO 8932 --- [           main] com.ymy.test.User3                       : 我是User3的代碼塊
2020-03-20 16:11:41.832  INFO 8932 --- [           main] com.ymy.test.User3                       : 我是User3的構造函數
2020-03-20 16:11:41.958  INFO 8932 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-03-20 16:11:42.172  INFO 8932 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 9199 (http) with context path ''
2020-03-20 16:11:42.176  INFO 8932 --- [           main] com.ymy.StaticConfigVarApplication       : Started StaticConfigVarApplication in 1.764 seconds (JVM running for 2.908)

程序加載完成之後,我們發現user1、user2、user3的靜態代碼塊被最先執行,在分別執行了他們各自的代碼塊以及構造函數,我們還發現這三個bean的執行順序是從上往下,所以在多個bean之間使用@Configuration裝配的bean之間的執行順序:

user1靜態代碼塊 > user2靜態代碼塊 > user3靜態代碼塊 > user1代碼塊 > user1構造函數 >
user2代碼塊 > user2構造函數 > user3代碼塊 > user3構造函數

看到這種執行順序我好像想通了爲什麼工廠拿不到FileConfig的屬性,我們一起來看

我在FileConfig bean中添加了構造函數,因爲只有被實例化了屬性纔會被賦值

 FileConfig(){
        log.info("這是文件配置類的構造函數");
    }

FileTypeFactory工廠沒有做修改,我們再此啓動項目查看結果

2020-03-20 16:30:43.965  INFO 11920 --- [           main] com.ymy.StaticConfigVarApplicationTests  : Starting StaticConfigVarApplicationTests on LAPTOP-3GLHJRE9 with PID 11920 (started by admin in D:\springboot\static-config-var)
2020-03-20 16:30:43.966  INFO 11920 --- [           main] com.ymy.StaticConfigVarApplicationTests  : No active profile set, falling back to default profiles: default
2020-03-20 16:30:44.731  INFO 11920 --- [           main] com.ymy.config.FileTypeFactory           : 我是文件類型工廠的靜態代碼塊,我被加載了
2020-03-20 16:30:44.909  INFO 11920 --- [           main] com.ymy.config.FileConfig                : 這是文件配置類的構造函數
2020-03-20 16:30:44.939  INFO 11920 --- [           main] com.ymy.config.FileTypeFactory           : 我是文件類型工廠的代碼塊,我被加載了
2020-03-20 16:30:44.940  INFO 11920 --- [           main] com.ymy.config.FileTypeFactory           : 我是文件類型工廠的構造函數,我被加載了

看到這個證實了我的想法,工廠類的靜態代碼塊被先加載,配置類的構造被後加載,所以導致了讀取屬性爲空的情況,由於靜態代碼塊是對象被初始化的時候執行,那我只需要將他放到配置類後執行就可以了啊,最簡單有效的方式就是去掉FileTypeFactory工廠類的@Configuration註解,這樣,程序啓動的時候就不會加載工廠,而是在被調用的時候加載,這個時候配置類早就加載完成了,之前爲啥要手賤去加一個註解呢?可能是腦子進水了,不過幸虧加了這個註解讓我學習了一波@Configuration註解與@Component註解的區別。

@Configuration註解與@Component註解的區別

剛剛分析的代碼中我們並沒有牽扯到@Component註解,爲什麼會學習了一波他們的區別呢?我之前一直認爲他們的作用是一致的,知道我測試的時候使用了他們兩個之後,我發現我錯了。

從表面看@Configuration屬於@Component的派生類,所以他們兩個@Component能幹的事情@Configuration應該也能幹,這樣的認知雖然沒錯,但是不嚴謹,我們還是使用剛剛新建的user1、2、3來說明他們的區別,我們將他們的裝配註解替換成@Component
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述
其他都不需要修改,我們直接來看他們的執行順序

2020-03-20 16:42:40.274  INFO 21048 --- [           main] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 725 ms
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User1                       : 我是User1的靜態代碼塊
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User1                       : 我是User1的代碼塊
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User1                       : 我是User1的構造函數
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User2                       : 我是User的靜態代碼塊
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User2                       : 我是User的代碼塊
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User2                       : 我是User的構造函數
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User3                       : 我是User3的靜態代碼塊
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User3                       : 我是User3的代碼塊
2020-03-20 16:42:40.311  INFO 21048 --- [           main] com.ymy.test.User3                       : 我是User3的構造函數

還記得被@Configuration裝配的bean的執行順序嗎?

user1靜態代碼塊 > user2靜態代碼塊 > user3靜態代碼塊 > user1代碼塊 > user1構造函數 >
user2代碼塊 > user2構造函數 > user3代碼塊 > user3構造函數

發現什麼了嗎?被@Component裝配的bean執行順序居然發生改變了,執行順序如下:

user1靜態代碼塊 > user1代碼塊 > user1構造函數 > user2靜態代碼塊 > user2代碼塊
user2構造函數 > user3靜態代碼塊 > user3代碼塊 > user3構造函數

可以看出被這兩個裝配的bean執行順序發生了很大的改變,這是爲什麼呢?

爲什麼@Configuration註解與@Component註解裝配的bean執行順序不一樣?

那是因爲@Configuration註解是被動態代理的(CGLIB),很容易理解,執行完靜態代碼塊之後,真正的實例代碼塊這些都會在代理中執行,而不是它本身,所以我們會看到所有被@Configuration裝配的bean都是先執行了靜態代碼塊,而後面猜分別執行他們的構造函數,而@Component註解卻沒有,所以它嚴格的按照這bean的執行順序執行,這也就是我們看到的爲什麼@Configuration註解與@Component註解裝配的bean執行順序不一樣。如果你們對spring的註解感興趣的話,我這裏有一個spring的中文官方文檔:spring中文官方文檔

總結

寫到這裏就結束了,spring還是那麼的強大,我們對它的瞭解還是那麼的細微,本人最近在努力的加深對spring的理解,希望以後會寫出更漂亮的文章,如果不是這次的小優化,我可能一直還是以爲@Configuration註解與@Component註解就是一個東西,真的是活到老,學到老啊。
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章