學妹問的Spring Bean常用配置,我用最通俗易懂的講解讓她學會了

你好呀,我是沉默王二,一枚有趣的程序員,寫的文章一直充滿靈氣,力求清新脫俗。昨天跑去王府井的小米店訂購了一臺小米 10,說是一週之內能到貨,但我還是忍不住今天就想見到她。見我茶不思飯不想的,老婆就勸我說,與其在瞎想,還不如滾去寫你的文章。於是就有了今天這篇“Spring Bean 的常用配置”,通過我和三妹對話的形式。

教妹學 Java,沒見過這麼放肆的標題吧?“語不驚人死不休”,沒錯,本篇文章的標題就是這麼酷炫,不然你怎麼會點進來?

我有一個漂亮如花的妹妹(見上圖),她叫什麼呢?我想聰明的讀者能猜得出:沉默王三,沒錯,年方三六。父母正考慮讓她向我學習,做一名正兒八經的 Java 程序員。我期初是反對的,因爲程序員這行業容易掉頭髮。但家命難爲啊,與其反對,不如做點更積極的事情,比如說寫點有趣的文章教教她。

“二哥,Spring 基礎篇學完後,我有一種強烈的感覺,Spring 真的好強大,就如春風佛面一般。”

“哎呀,三妹,你這個比喻雖然有些牽強,但多少有些詩意。”

“好吧,讓我們開始今天的學習吧!”

01、Bean 的 Scope 配置

“二哥,據說 Bean 的 Scope 類型有好幾種,用於定義了 Bean 的生命週期和使用環境,你能給我具體說說嗎?”

“沒問題啊。”

1)singleton

也就是單例模式,如果把一個 Bean 的 Scope 定義爲 singleton,意味着一個 Bean 在 Spring 容器中只會創建一次實例,對該實例的任何修改都會反映到它的引用上面。這也是 Scope 的默認配置項,可省略。

來新建一個 Writer 類,內容如下:

public class Writer {
    private String name;

    public Writer() {
    }

    // getter setter
}

再來新建一個 SingletonConfig 類,內容如下:

@Configuration
public class SingletonConfig {
    @Bean
    @Scope("singleton")
    public Writer getWriterSingleton() {
        return new Writer();
    }
}

@Configuration 註解表明當前類是一個配置類,相當於 Spring 配置的一個 xml 文件。

@Bean 註解用在 getWriterSingleton() 方法上,表明當前方法返回一個 Bean 對象(Writer),然後將其交給 Spring 管理。

可以使用 Spring 定義的常量來代替字符串 singleton:

@Scope(value = ConfigurableBeanFactory.SCOPE_SINGLETON)

當然也可以完全省略,於是 SingletonConfig 瘦身了。

@Configuration
public class SingletonConfig {
    @Bean
    public Writer getWriterSingleton() {
        return new Writer();
    }
}

新建 SingletonMain 類,代碼如下:

public class SingletonMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SingletonConfig.class);
        Writer writer1 = context.getBean(Writer.class);
        Writer writer2 = context.getBean(Writer.class);

        System.out.println(writer1);
        System.out.println(writer2);

        writer1.setName("沉默王二");
        System.out.println(writer2.getName());

        context.close();
    }
}

程序輸出的結果如下所示:

commonuse.singleton.Writer@19dc67c2
commonuse.singleton.Writer@19dc67c2
沉默王二

writer1 和 writer2 兩個對象的字符串表示形式完全一樣,都是 commonuse.singleton.Writer@19dc67c2;另外,改變了 writer1 對象的 name,writer2 也跟着變了。

從結果中我們可以得出這樣的結論:Scope 爲 singleton 的時候,儘管使用 getBean() 獲取了兩次 Writer 實例,但它們是同一個對象。只要更改它們其中任意一個對象的狀態,另外一個也會同時改變。

2)prototype

prototype 的英文詞義是複數的意思,它表示一個 Bean 會在 Spring 中創建多次實例,適合用於多線程的場景。

新建一個 PrototypeConfig 類,內容如下:

@Configuration
public class PrototypeConfig {
    @Bean
    @Scope("prototype")
    public Writer getWriterPrototype() {
        return new Writer();
    }
}

可以使用 Spring 定義的常量來代替字符串 prototype:

@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)

新建 PrototypeMain 類,代碼如下:

public class PrototypeMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(PrototypeConfig.class);
        Writer writer1 = context.getBean(Writer.class);
        Writer writer2 = context.getBean(Writer.class);

        System.out.println(writer1);
        System.out.println(writer2);

        writer1.setName("沉默王二");
        System.out.println(writer2.getName());

        context.close();
    }
}

程序輸出的結果如下所示:

commonuse.Writer@78a2da20
commonuse.Writer@dd3b207
null

writer1 和 writer2 兩個對象的字符串表示形式完全不一樣,一個是 commonuse.Writer@78a2da20,另一個是 commonuse.Writer@dd3b207;另外,雖然 writer1 對象的 name 被改變爲“沉默王二”,但 writer2 的 name 仍然爲 null。

從結果中我們可以得出這樣的結論:Scope 爲 prototype 的時候,每次調用 getBean() 都會返回一個新的實例,它們不是同一個對象。更改它們其中任意一個對象的狀態,另外一個並不會同時改變。

3)request、session、application、websocket

這 4 個作用域僅在 Web 應用程序的上下文中可用,在實踐中並不常用。request 用於爲 HTTP 請求創建 Bean 實例,session 用於爲 HTTP 會話創建 Bean 實例, application 用於爲 ServletContext 創建 Bean 實例,而 websocket 用於爲特定的 WebSocket 會話創建 Bean 實例。

02、Bean 的字段注入

“二哥,據說 Spring 開發中經常涉及調用各種配置文件,需要用到 @Value 註解,你能給我詳細說說嗎?”

“沒問題啊。”

1)注入普通字符串

來新建一個 ValueConfig 類,內容如下:

@Configuration
public class ValueConfig {
    @Value("沉默王二")
    private String name;

    public void output() {
        System.out.println(name);
    }
}

@Value 註解用在成員變量 name 上,表明當前注入 name 的值爲“沉默王二”。

來新建一個 ValueMain 類,內容如下:

public class ValueMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SpELStringConfig.class);

        SpELStringConfig service = context.getBean(SpELStringConfig.class);
        service.output();

        context.close();
    }
}

程序輸出結果如下:

沉默王二

結果符合我們的預期。

2)注入 Spring 表達式

使用 @Value 注入普通字符串的方式最爲簡單,我們來升級一下,注入 Spring 表達式,先來個加法運算吧。

@Value("#{18 + 12}") // 30
private int add;

雙引號中需要用到 #{}。再來個關係運算和邏輯運算吧。

@Value("#{1 == 1}") // true
private boolean equal;

@Value("#{400 > 300 || 150 < 100}") // true
private boolean or;

覺得還不夠刺激,再來個三元運算吧。

@Value("#{2 > 1 ? '沉默是金' : '不再沉默'}") // "沉默是金"
private String ternary;

3)注入配置文件

假如你覺得以上這些都不夠有意思,那來注入配置文件吧。

在 resources 目錄下新建 value.properties 文件,內容如下:

name=沉默王二
age=18

新建一個 ValuePropertiesConfig 類,內容如下:

@Configuration
@PropertySource("classpath:value.properties")
public class ValuePropertiesConfig {

    @Value("${name}")
    private String name;

    @Value("${age}")
    private int age;

    public void output() {
        System.out.println("姓名:" + name + " 年紀:" + age);
    }
}

@PropertySource 註解用於指定載入哪個配置文件(value.properties),classpath: 表明從 src 或者 resources 目錄下找。

注意此時 @Value("") 的雙引號中爲 $ 符號而非 # 符號,{} 中爲配置文件中的 key。

新建一個 ValuePropertiesMain 類,內容如下:

public class ValuePropertiesMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ValuePropertiesConfig.class);

        ValuePropertiesConfig service = context.getBean(ValuePropertiesConfig.class);
        service.output();

        context.close();
    }
}

程序運行結果如下:

姓名:³ÁĬÍõÈý 年紀:18

“糟糕,二哥!中文亂碼了!”

“不要怕,三妹,問題很容易解決。”

首先,查看 properties 文件的編碼方式。

如果不是 UTF-8 就改爲 UTF-8。同時,確保修改編碼方式後的 properties 文件中沒有中文亂碼。

然後,在 @PropertySource 註解中加入編碼格式。

@PropertySource(value = "classpath:value.properties",  encoding = "UTF-8")

再次運行程序後,亂碼就被風吹走了。

姓名:沉默王二 年紀:18

03、Bean 的初始化和銷燬

“二哥,據說在實際開發中,經常需要在 Bean 初始化和銷燬時加一些額外的操作,你能給我詳細說說怎麼實現嗎?”

“沒問題啊。”

1)init-method/destroy-method

新建一個 InitDestroyService 類,內容如下:

public class InitDestroyService {
    public InitDestroyService() {
        System.out.println("構造方法");
    }

    public void init() {
        System.out.println("初始化");
    }

    public void destroy {
        System.out.println("銷燬");
    }
}

InitDestroyService() 爲構造方法,init() 爲初始化方法,destroy() 爲銷燬方法。

新建 InitDestroyConfig 類,內容如下:

@Configuration
public class InitDestroyConfig {
    @Bean(initMethod = "init",destroyMethod = "destroy")
    public InitDestroyService initDestroyService() {
        return new InitDestroyService();
    }
}

@Bean 註解的 initMethod 用於指定 Bean 初始化的方法,destroyMethod 用於指定 Bean 銷燬時的方法。

新建 InitDestroyMain 類,內容如下:

public class InitDestroyMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(InitDestroyConfig.class);
        InitDestroyService service = context.getBean(InitDestroyService.class);
        System.out.println("準備關閉容器");
        context.close();
    }
}

程序運行結果如下:

構造方法
初始化
準備關閉容器
銷燬

也就是說,初始化方法在構造方法後執行,銷燬方法在容器關閉後執行。

2)@PostConstruct/@PreDestroy

新建一個 InitDestroyService 類,內容如下:

public class InitDestroyService {
    public InitDestroyService() {
        System.out.println("構造方法");
    }

    @PostConstruct
    public void init() {
        System.out.println("初始化");
    }

    @PreDestroy
    public void destroy() {
        System.out.println("銷燬");
    }
}

@PostConstruct 註解的作用和 @Bean 註解中 init-method 作用相同,用於指定 Bean 初始化後執行的方法。

@PreDestroy 註解的作用和 @Bean 註解中 destroyMethod 作用相同,用於指定 Bean 被容器銷燬後執行的方法。

新建 InitDestroyConfig 類,內容如下:

@Configuration
public class InitDestroyConfig {
    @Bean
    public InitDestroyService initDestroyService() {
        return new InitDestroyService();
    }
}

@Bean 註解中不需要再指定 init-method 和 destroyMethod 參數了。

新建 InitDestroyMain 類,內容如下:

public class InitDestroyMain {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(InitDestroyConfig.class);
        InitDestroyService service = context.getBean(InitDestroyService.class);
        System.out.println("準備關閉容器");
        context.close();
    }
}

程序運行結果如下:

構造方法
初始化
準備關閉容器
銷燬

結果符合我們的預期。

04、爲 Bean 配置不同的環境

“二哥,據說 Spring 開發中經常需要將 Bean 切換到不同的環境,比如說開發環境、測試環境、正式生產環境,你能給我具體說說怎麼實現的嗎?”

“沒問題啊。”

來考慮這樣一個常見的場景,我們需要爲開發環境和正式生產環境配置不同的數據源。

新建 Datasource 類,內容如下:

public class Datasource {
    private String dburl;

    public Datasource(String dburl) {
        this.dburl = dburl;
    }

    // getter/setter
}

dbname 用於指定不同環境下數據庫的連接地址。

新建 Config 類,內容如下:

@Configuration
public class Config {
    @Bean
    @Profile("dev")
    public Datasource devDatasource() {
        return new Datasource("開發環境");
    }

    @Bean
    @Profile("prod")
    public Datasource prodDatasource() {
        return new Datasource("正式生產環境");
    }
}

@Profile 註解用於標識不同環境下要實例化的 Bean。

新建 Main 類,內容如下:

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        ConfigurableEnvironment environment = context.getEnvironment();
        environment.setActiveProfiles("prod");
        context.register(Config.class);
        context.refresh();

        Datasource datasource = context.getBean(Datasource.class);
        System.out.println(datasource.getDburl());
        context.close();
    }
}

新建 AnnotationConfigApplicationContext 對象的時候不要指定配置類,等到調用 setActiveProfiles("prod") 方法將環境設置爲正式生產環境後再通過 register(Config.class) 方法將配置類註冊到容器當中,同時記得刷新容器。

運行程序,輸出以下內容:

正式生產環境

然後將 “prod” 更改爲 “dev”,再次運行程序,輸出以下內容:

開發環境

“二哥,這篇文章中的示例代碼你上傳到碼雲了嗎?最近 GitHub 訪問起來有點卡。”

“你到挺貼心啊,三妹。碼雲傳送門~

“二哥,你教得真不錯,我完全學會了,一點也不枯燥。”

“那必須得啊,期待下一篇吧?”

“那是當然啊,期待,非常期待,望穿秋水的感覺。”

請允許我熱情地吐槽一下,這篇文章我不希望再被噴了,看在我這麼辛苦搞原創(創意+乾貨+有趣)的份上,多鼓勵鼓勵好不好?別瞅了,點讚唄,你最美你最帥

如果覺得文章對你有點幫助,請微信搜索「 沉默王二 」第一時間閱讀,回覆【666】【1024】更有我爲你精心準備的 500G 高清教學視頻(已分門別類),以及大廠技術牛人整理的面經一份。

最最重要

實踐出真知,實踐出真知,實踐出真知。重要的話說三遍。

千萬不要遇到了一片好文章,就放到了收藏夾,我要等到某某天再去看…

千萬不要這樣,當你遇到了,就馬上就去實踐,去學習文章中的知識,你要等的某某天它是不會輕而易舉來的,你可能就會錯過一次最佳的學習機會。

不要覺得學習的知識很多,認準一個作者,跟着他的節奏,不要三心二意,認真跟一段時間,你就會發現真的學到了很多。

總結一下吧

所以我給大家的建議就是,學習不要盲從,也不要留給明天,留給下一次,該學習的內容一定不要放過。世界上有兩種痛苦,其一就是後悔的痛苦。

不要在評論區秀,說什麼“先收藏,然後不去學”,有意義嗎?你學到了就是自己的,就能領先別人一小步,積少成多,未來你就是引領別人學習的大牛。

看在熬夜寫作的份上,送我個讚唄,謝謝。

1、老鐵們,關注我的原創微信公衆號「沉默王二」,專注於有趣的 Java 技術和有益的程序人生。

2、給我點個讚唄,你最美你最帥,除此之外,還可以讓更多的人看到這篇文章,順便激勵下我,再次感謝。

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