朱曄和你聊Spring系列S1E3:Spring咖啡罐裡的豆子

powerzhuye發表於2018-10-01

標題中的咖啡罐指的是Spring容器,容器裡裝的當然就是被稱作Bean的豆子。本文我們會以一個最基本的例子來熟悉Spring的容器管理和擴充套件點。

為什麼要讓容器來管理物件?

首先我們來聊聊這個問題,為什麼我們要用Spring來管理物件(的生命週期和物件之間的關係)而不是自己new一個物件呢?大家可能會回答是方便,為了解耦。我個人覺得除了這兩個原因之外,還有就是給予了我們更多可能性。如果我們以容器為依託來管理所有的框架、業務物件,那麼不僅僅我們可以無侵入調整物件的關係,還有可能無侵入隨時調整物件的屬性甚至悄悄進行物件的替換。這就給了我們無限多的可能性,大大方便了框架的開發者在程式背後實現一些擴充套件。不僅僅Spring Core本身以及Spring Boot大量依賴Spring這套容器體系,一些外部框架也因為這個原因可以和Spring進行無縫整合。 Spring可以有三種方式來配置Bean,分別是最早期的XML方式、後來的註解方式以及現在最流行的Java程式碼配置方式。

Bean的回撥事件

在前文parent模組(空的一個SpringBoot應用程式)的基礎上,我們先來建立一個beans模組:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>me.josephzhu</groupId>
    <artifactId>spring101-beans</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring101-beans</name>
    <description></description>

    <parent>
        <groupId>me.josephzhu</groupId>
        <artifactId>spring101</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </parent>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
複製程式碼

然後來建立我們的豆子:

package me.josephzhu.spring101beans;

import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

@Component
public class MyService implements InitializingBean, DisposableBean {

    public int increaseCounter() {
        this.counter++;
        return counter;
    }

    public int getCounter() {
        return counter;
    }

    public void setCounter(int counter) {
        this.counter = counter;
    }

    private int counter=0;

    public MyService(){
        counter++;
        System.out.println(this + "#constructor:" + counter);
    }

    public String hello(){
        return this + "#hello:" + counter;
    }

    @PreDestroy
    public void preDestroy() {
        System.out.println(this + "#preDestroy:" + counter);

    }

    @Override
    public void afterPropertiesSet() {
        counter++;
        System.out.println(this + "#afterPropertiesSet:" + counter);
    }

    @PostConstruct
    public void postConstruct(){
        counter++;
        System.out.println(this + "#postConstruct:" + counter);
    }

    @Override
    public void destroy() {
        System.out.println(this + "#destroy:" + counter);

    }
}
複製程式碼

這裡可以看到,我們的服務中有一個counter欄位,預設是0。這個類我們實現了InitializingBean介面和DisposableBean介面,同時還建立了兩個方法分別加上了@PostConstruct和@PreDestroy註解。這兩套實現方式都可以在物件的額外初始化功能和釋放功能,註解的實現不依賴Spring的介面,侵入性弱一點。 接下去,我們建立一個Main類來測試一下:

package me.josephzhu.spring101beans;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

import javax.annotation.Resource;

@SpringBootApplication
public class Spring101BeansApplication implements CommandLineRunner {

    @Autowired
    private ApplicationContext applicationContext;
    @Resource
    private MyService helloService;
    @Autowired
    private MyService service;

    public static void main(String[] args) {
        SpringApplication.run(Spring101BeansApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        System.out.println("====================");
        applicationContext.getBeansOfType(MyService.class).forEach((name, service)->{
            System.out.println(name + ":" + service);
        });

        System.out.println("====================");
        System.out.println(helloService.hello());
        System.out.println(service.hello());
    }
}
複製程式碼

ApplicationContext直接注入即可,不一定需要用ApplicationContextAware方式來獲取。執行程式後可以看到輸出如下:

me.josephzhu.spring101beans.MyService@7fb4f2a9#constructor:1
me.josephzhu.spring101beans.MyService@7fb4f2a9#postConstruct:2
me.josephzhu.spring101beans.MyService@7fb4f2a9#afterPropertiesSet:3
====================
myService:me.josephzhu.spring101beans.MyService@7fb4f2a9
====================
me.josephzhu.spring101beans.MyService@7fb4f2a9#hello:3
me.josephzhu.spring101beans.MyService@7fb4f2a9#hello:3
me.josephzhu.spring101beans.MyService@7fb4f2a9#preDestroy:3
me.josephzhu.spring101beans.MyService@7fb4f2a9#destroy:3
複製程式碼

這裡我們使用@Resource註解和@Autowired註解分別引用了兩次物件,可以看到由於Bean預設配置為singleton單例,所以容器中MyService型別的物件只有一份,程式碼輸出也可以證明這點。此外,我們也通過輸出看到了構造方法以及兩套Bean回撥的次序是:

  1. 類自己的構造方法
  2. @PostConstruct註釋的方法
  3. InitializingBean介面實現的方法
  4. @PreDestroy註釋的方法
  5. DisposableBean介面實現的方法

Java 程式碼方式建立Bean

從剛才的輸出中可以看到,在剛才的例子中,我們為Bean打上了@Component註解,容器為我們建立了名為myService的MyService型別的Bean。現在我們再來用Java程式碼方式來建立相同型別的Bean,建立如下的檔案:

package me.josephzhu.spring101beans;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class ApplicationConfig {

    @Bean(initMethod = "init")
    public MyService helloService(){
        MyService myService = new MyService();
        myService.increaseCounter();
        return myService;
    }

}
複製程式碼

這裡可以看到在定義Bean的時候我們關聯了一個initMethod,因此我們需要修改Bean加上這個方法:

public void init() {
    counter++;
    System.out.println(this + "#init:" + counter);

}
複製程式碼

現在我們執行程式碼看看結果,得到了如下錯誤:

Field service in me.josephzhu.spring101beans.Spring101BeansApplication required a single bean, but 2 were found:
	- myService: defined in file [/Users/zyhome/IdeaProjects/spring101/spring101-beans/target/classes/me/josephzhu/spring101beans/MyService.class]
	- helloService: defined by method 'helloService' in class path resource [me/josephzhu/spring101beans/ApplicationConfig.class]
複製程式碼

出現錯誤的原因是@Autowired了一個MyService,@Resource註解因為使用Bean的名稱來查詢Bean,所以並不會出錯,而@Autowired因為根據Bean的型別來查抄Bean找到了兩個匹配所有出錯了,解決方式很簡單,我們在多個Bean裡選一個作為主Bean。我們修改一下MyService加上註解:

@Component
@Primary
public class MyService implements InitializingBean, DisposableBean
複製程式碼

這樣,我們的@Resource根據名字匹配到的是我們@Configuration出來的Bean,而@Autowired根據型別+Primary匹配到了@Component註解定義的Bean,重新執行程式碼來看看是不是這樣:

me.josephzhu.spring101beans.MyService@6cd24612#constructor:1
me.josephzhu.spring101beans.MyService@6cd24612#postConstruct:3
me.josephzhu.spring101beans.MyService@6cd24612#afterPropertiesSet:4
me.josephzhu.spring101beans.MyService@6cd24612#init:5
me.josephzhu.spring101beans.MyService@7486b455#constructor:1
me.josephzhu.spring101beans.MyService@7486b455#postConstruct:2
me.josephzhu.spring101beans.MyService@7486b455#afterPropertiesSet:3
====================
myService:me.josephzhu.spring101beans.MyService@7486b455
helloService:me.josephzhu.spring101beans.MyService@6cd24612
====================
me.josephzhu.spring101beans.MyService@6cd24612#hello:5
me.josephzhu.spring101beans.MyService@7486b455#hello:3
me.josephzhu.spring101beans.MyService@7486b455#preDestroy:3
me.josephzhu.spring101beans.MyService@7486b455#destroy:3
me.josephzhu.spring101beans.MyService@6cd24612#preDestroy:5
me.josephzhu.spring101beans.MyService@6cd24612#destroy:5
複製程式碼

從輸出中我們注意到幾點:

  1. 先輸出的的確是helloService,說明@Resource引入的是我們Java程式碼配置的MyService,helloService由於在我們配置的多呼叫了一次increaseCounter()以及關聯的initMethod,所以counter的值是5
  2. initMethod執行的順序在@PostConstruct註釋的方法和InitializingBean介面實現的方法之後
  3. 雖然我們的MySerive的兩種Bean的定義都是單例,但是這不代表我們的Bean就是一套,在這裡我們通過程式碼配置和註解方式在容器內建立了兩套MyService型別的Bean,它們都經歷了自己的初始化過程。通過@Resource和@Autowired引入到了是不同的Bean,當然也就是不同的物件 你還可以試試在使用@Autowired引入MyService的時候直接指定需要的Bean:
@Autowired
@Qualifier("helloService")
private MyService service;
複製程式碼

兩個重要的擴充套件點

我們來繼續探索Spring容器提供給我們的兩個有關Bean的重要擴充套件點。

  • 用於修改Bean定義的BeanFactoryPostProcessor。所謂修改定義就是修改Bean的後設資料,後設資料有哪些呢?如下圖所示,型別、名字、例項化方式、構造引數、屬性、Autowire模式、懶初始化模式、初始析構方法。實現了這個介面後,我們就可以修改這些已經定義的後設資料,實現真正的動態配置。這裡需要注意,我們不應該在這個介面的實現中去例項化Bean,否則這相當於提前進行了例項化會破壞Bean的生命週期。

朱曄和你聊Spring系列S1E3:Spring咖啡罐裡的豆子

  • 用於修改Bean例項的BeanPostProcessor。在這個階段其實Bean已經例項化了,我們可以進行一些額外的操作對Bean進行修改。如下圖,我們可以清晰的看到Bean的生命週期如下(BeanPostProcessor縮寫為BPP):
  1. Bean定義載入
  2. BeanFactoryPostProcessor來修改Bean定義
  3. Bean逐一例項化
  4. BeanPostProcessor預處理
  5. Bean初始化
  6. BeanPostProcessor後處理

朱曄和你聊Spring系列S1E3:Spring咖啡罐裡的豆子
好,我們現在來實現這兩種型別的處理器,首先是用於修改Bean定義的處理器:

package me.josephzhu.spring101beans;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.stereotype.Component;

@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        BeanDefinition beanDefinition = configurableListableBeanFactory.getBeanDefinition("helloService");
        if (beanDefinition != null) {
            beanDefinition.setScope("prototype");
            beanDefinition.getPropertyValues().add("counter", 10);
        }
        System.out.println("MyBeanFactoryPostProcessor");
    }
}
複製程式碼

這裡,我們首先找到了我們的helloService(Java程式碼配置的那個Bean),然後修改了它的屬性和Scope(還記得嗎,在之前的圖中我們可以看到,這兩項都是Bean的定義,定義相當於類描述,例項當然就是類例項了)。 然後,我們再來建立一個修改Bean例項的處理器:

package me.josephzhu.spring101beans;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

@Component
public class MyBeanPostProcessor implements BeanPostProcessor {
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof MyService) {
            System.out.println(bean + "#postProcessAfterInitialization:" + ((MyService)bean).increaseCounter());
        }
        return bean;
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if (bean instanceof MyService) {
            System.out.println(bean + "#postProcessBeforeInitialization:" + ((MyService)bean).increaseCounter());
        }
        return bean;
    }
}
複製程式碼

實現比較簡單,在這個處理器的兩個介面我們都呼叫了一次增加計數器的操作。我們執行程式碼來看一下這兩個處理器執行的順序是否符合剛才那個圖的預期:

MyBeanFactoryPostProcessor
me.josephzhu.spring101beans.MyService@41330d4f#constructor:1
me.josephzhu.spring101beans.MyService@41330d4f#postProcessBeforeInitialization:11
me.josephzhu.spring101beans.MyService@41330d4f#postConstruct:12
me.josephzhu.spring101beans.MyService@41330d4f#afterPropertiesSet:13
me.josephzhu.spring101beans.MyService@41330d4f#init:14
me.josephzhu.spring101beans.MyService@41330d4f#postProcessAfterInitialization:15
me.josephzhu.spring101beans.MyService@6f36c2f0#constructor:1
me.josephzhu.spring101beans.MyService@6f36c2f0#postProcessBeforeInitialization:11
me.josephzhu.spring101beans.MyService@6f36c2f0#postConstruct:12
me.josephzhu.spring101beans.MyService@6f36c2f0#afterPropertiesSet:13
me.josephzhu.spring101beans.MyService@6f36c2f0#init:14
me.josephzhu.spring101beans.MyService@6f36c2f0#postProcessAfterInitialization:15
me.josephzhu.spring101beans.MyService@3b35a229#constructor:1
me.josephzhu.spring101beans.MyService@3b35a229#postProcessBeforeInitialization:2
me.josephzhu.spring101beans.MyService@3b35a229#postConstruct:3
me.josephzhu.spring101beans.MyService@3b35a229#afterPropertiesSet:4
me.josephzhu.spring101beans.MyService@3b35a229#postProcessAfterInitialization:5
====================
me.josephzhu.spring101beans.MyService@6692b6c6#constructor:1
me.josephzhu.spring101beans.MyService@6692b6c6#postProcessBeforeInitialization:11
me.josephzhu.spring101beans.MyService@6692b6c6#postConstruct:12
me.josephzhu.spring101beans.MyService@6692b6c6#afterPropertiesSet:13
me.josephzhu.spring101beans.MyService@6692b6c6#init:14
me.josephzhu.spring101beans.MyService@6692b6c6#postProcessAfterInitialization:15
myService:me.josephzhu.spring101beans.MyService@3b35a229
helloService:me.josephzhu.spring101beans.MyService@6692b6c6
====================
me.josephzhu.spring101beans.MyService@41330d4f#hello:15
me.josephzhu.spring101beans.MyService@6f36c2f0#hello:15
me.josephzhu.spring101beans.MyService@3b35a229#preDestroy:5
me.josephzhu.spring101beans.MyService@3b35a229#destroy:5
複製程式碼

這個輸出結果有點長,第一行就輸出了MyBeanFactoryPostProcessor這是預料之中,Bean定義的修改肯定是最先發生的。我們看下輸出的規律,1、11、12、13、14、15出現了三次,之所以從1跳到了11是因為我們的BeanFactoryPostProcessor修改了其中的counter屬性的值為10。這說明了,我們的helloService的初始化進行了三次:

  • 第一套指標地址是5a7fe64f,對應輸出第一個hello(),這是我們@Resource引入的

朱曄和你聊Spring系列S1E3:Spring咖啡罐裡的豆子

  • 第二套指標地址是69ee81fc,對應輸出第二個hello(),這是我們@Autowird+@Qualifier引入的(剛才一節最後我們指定了helloService)

朱曄和你聊Spring系列S1E3:Spring咖啡罐裡的豆子

  • 第三套指標地址是29f7cefd,這是我們getBeansOfType的時候建立的,對應下面Key-Value的輸出:

朱曄和你聊Spring系列S1E3:Spring咖啡罐裡的豆子
這裡的輸出說明了幾點:

  • 我們的BeanFactoryPostProcessor生效了,不但修改了helloService的Scope為prototype而且修改了它的counter屬性
  • 對於Scope=ptototype的Bean,顯然在每次使用Bean的時候都會新建一個例項
  • BeanPostProcessor兩個方法的順序結合一開始說的Bean事件回撥的順序整體如下:
  1. 類自己的構造方法
  2. BeanFactoryPostProcessor介面實現的postProcessBeforeInitialization()方法
  3. @PostConstruct註釋的方法
  4. InitializingBean介面實現的afterPropertiesSet()方法
  5. Init-method定義的方法
  6. BeanFactoryPostProcessor介面實現的postProcessAfterInitialization()方法
  7. @PreDestroy註釋的方法
  8. DisposableBean介面實現的destroy()方法

最後,我們可以修改BeanFactoryPostProcessor中的程式碼把prototype修改為singleton看看是否我們的helloService這個Bean恢復為了單例:

MyBeanFactoryPostProcessor
me.josephzhu.spring101beans.MyService@51891008#constructor:1
me.josephzhu.spring101beans.MyService@51891008#postProcessBeforeInitialization:11
me.josephzhu.spring101beans.MyService@51891008#postConstruct:12
me.josephzhu.spring101beans.MyService@51891008#afterPropertiesSet:13
me.josephzhu.spring101beans.MyService@51891008#init:14
me.josephzhu.spring101beans.MyService@51891008#postProcessAfterInitialization:15
me.josephzhu.spring101beans.MyService@49c90a9c#constructor:1
me.josephzhu.spring101beans.MyService@49c90a9c#postProcessBeforeInitialization:2
me.josephzhu.spring101beans.MyService@49c90a9c#postConstruct:3
me.josephzhu.spring101beans.MyService@49c90a9c#afterPropertiesSet:4
me.josephzhu.spring101beans.MyService@49c90a9c#postProcessAfterInitialization:5
====================
myService:me.josephzhu.spring101beans.MyService@49c90a9c
helloService:me.josephzhu.spring101beans.MyService@51891008
====================
me.josephzhu.spring101beans.MyService@51891008#hello:15
me.josephzhu.spring101beans.MyService@51891008#hello:15
me.josephzhu.spring101beans.MyService@49c90a9c#preDestroy:5
me.josephzhu.spring101beans.MyService@49c90a9c#destroy:5
me.josephzhu.spring101beans.MyService@51891008#preDestroy:15
me.josephzhu.spring101beans.MyService@51891008#destroy:15
複製程式碼

本次輸出結果的hello()方法明顯是同一個bean,結果中也沒出現三次1、11、12、13、14、15。

總結

本文以探索的形式討論了下面的一些知識點:

  1. 容器管理物件的意義是什麼
  2. Bean的生命週期回撥事件
  3. Spring提供的Bean的兩個重要擴充套件點
  4. @Resource和@Autowired的區別
  5. 註解方式和程式碼方式配置Bean
  6. @Primary和@Qualifier註解的作用
  7. Bean的不同型別的Scope

相關文章