I-o-C 一篇概覽

2023-05-05 12:02:05

一、ioC 容器和 Bean介紹

IoC(Inversion of Control )也被稱之為 DI(dependency injection),名稱側重點略有不同。

所謂控制翻轉即物件通過建構函式引數、工廠方法引數或者屬性欄位設定來定義依賴,然後容器在建立 bean 的時候注入依賴。這個過程和物件自己管理依賴是完全相反的。

org.springframework.beans 和 org.springframework.context 是 Spring 框架 IoC 容器的基礎包。

BeanFactory 介面提供了豐富的設定機制來管理各種型別的物件。

ApplicationContext 是  BeanFactory 的子介面,在其原有基礎上新增瞭如下特性:

  • 和 Spring AOP 整合更容易。

  • 訊息處理(國際化應用)

  • 事件分發

  • 特有的應用層 contexts,例如 web 應用中的 WebApplicationContext。

BeanFactory 提供了設定框架和基本的功能,ApplicationContext 在此基礎上新增了更多企業應用特性,是 BeanFactory 的超集

Spring 有很多物件,其中 IoC 容器管理的範例化的物件稱之為 bean。

二、容器總覽

org.springframework.context.ApplicationContext 介面相當於 Spring IoC 容器,負責範例化,設定及組裝 bean 物件。

設定後設資料可以是 XML、Java 註解或者 Java 程式碼。

如下圖,概括的展示了 Spring 的作用過程:

container magic
設定後設資料用於描述 bean 及其依賴的關係,IoC 容器基於此來範例化,設定及組裝 bean 物件。

設定後設資料和IoC 容器是相互獨立,彼此不耦合的,它可以有多種形式,包括基於 XML設定、基於註解設定基於Java設定等。

三、Bean 總覽

容器內部 bean 通過 BeanDefinition 定義。

BeanDefinition 包含如下後設資料:全限定類名(包含包名)、Bean 行為特性(作用域、生命週期回撥等)、依賴描述及其它設定。

這些後設資料可以通過如下一系列屬性來描述:類名、名稱、作用域、建構函式引數、屬性、Autowire、懶載入、初始化方法、銷燬方法。

四、Bean 作用域

a)singleton

預設,每個 IoC 容器一個 Bean 範例。應用於無狀態 Bean 場景

附:對於 singleton 型別 bean 依賴 prototype 型別 bean 的場景,因為容器範例化物件時只會處理一次依賴,所以 singleton 範例依賴的 prototype 物件只是其一

b)prototype

每次需要 Bean 物件時即建立新的範例。應用於有狀態 Bean 場景

附:對於 prototype 型別 bean,Spring 並沒有管理其完整地生命週期,容器只負責範例化、設定及組裝依賴。設定的銷燬,生命週期回撥並不會被呼叫。

可以通過自定義 bean post-processor 來處理。

c)request

Spring web 應用,對應每次 HTTP 請求生命週期,不同請求之間是隔離的

d)session

Spring web 應用,對應每次 HTTP Session 生命週期,不同 Session 之間是隔離的

e)application

Spring web 應用,對應 ServletContext 生命週期,不同 ServletContext 之間是隔離的

f)websocket

Spring web 應用,對應 WebSocket Session 生命週期。 

五、自定義 Bean 特性

1、生命週期回撥

可以通過實現 Spring InitializingBean 和 DisposableBean 介面來和容器 bean 生命週期管理過程進行互動。

  • bean 初始化:InitializingBean() -> afterPropertiesSet() 呼叫。
  • bean 銷燬:DisposableBean() -> destroy() 呼叫。

同註解應用:@PostConstruct、@PreDestroy

我們也可以通過實現 BeanPostProcessor 來處理任何回撥介面。

除了初始化和銷燬回撥,Spring 管理的物件也可以通過實現 Lifecycle 接口來參與啟動及關閉過程回撥。

a)Initialization Callbacks

org.springframework.beans.factory.InitializingBean 介面可以讓 bean 物件在容器設定完所有必要的屬性後執行初始化操作。他只有一個方法:

void afterPropertiesSet() throws Exception;

通常我們不建議使用此介面,因為它會使得我們的程式碼和 Spring 程式碼產生耦合。推薦使用 @PostConstruct 註解或者宣告一個 POLO 初始化方法。

初始化方法宣告如下:

<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
// 或者
@Bean(initMethod = "")

如果存在多種宣告機制,則執行順序為:@PostConstruct -> InitializingBean.afterPropertiesSet() -> 自定義初始化方法

b)Destruction Callbacks

org.springframework.beans.factory.DisposableBean 介面可以使得 bean 在容器銷燬時收到一個回撥,它也只有一個方法:

void destroy() throws Exception;

同樣不建議使用,原因如 InitializingBean,推薦使用 @PreDestroy 註解或者定義銷燬方法。如下:

<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>
或者
@Bean(destroyMethod = "")

如果存在多種宣告機制,則執行順序為:@PreDestroy -> DisposableBean.destroy() -> 自定義銷燬方法

c)Startup and Shutdown Callbacks

Lifecycle 介面定義如下:

public interface Lifecycle {
    void start();
    void stop();
    boolean isRunning();
}

ApplicationContext 容器在接收到啟動或者關閉訊號之後,會順序的把這一訊息傳遞給所有容器內 Spring 管理的實現了 Lifecycle 介面的物件。

這一過程會委託代理給 LifecycleProcessor 進行處理,其定義如下:

public interface LifecycleProcessor extends Lifecycle {
    void onRefresh();
    void onClose();
}

LifecycleProcessor 是對 Lifecycle 介面的拓展,引入了處理 context 重新整理及關閉的兩個方法。

對於存在依賴關係的不同物件,啟動及關閉的相應呼叫順序就要遵循一定的規則。

如果是直接依賴:依賴方要先於被依賴方啟動,並後於被依賴方關閉。

對於非直接依賴關係,如只知道一類型別的物件需要依賴另一類型別的物件,以上的介面將無法滿足使用。因此這裡需要引入另外一個介面 SmartLifecycle,它的定義如下:

public interface Phased {
    int getPhase();
}

public interface SmartLifecycle extends Lifecycle, Phased {
    boolean isAutoStartup();
    void stop(Runnable callback);
}

對於實現了 SmartLifecycle 介面的物件,啟動時,phase 小的先啟動,關閉時,phase 大的先關閉。

對於未實現 SmartLifecycle 介面的物件,我們可以認定它的 phase 為 0,如此,phase 小於 0 的物件則將先於其啟動,後於其關閉。

SmartLifecycle 介面 stop() 方法會接收一個回撥,所有實現此介面的物件都需要在其關閉過程執行完畢後呼叫一次 run() 方法。 

LifecycleProcessor 介面的預設實現 DefaultLifecycleProcessor, 會等待所有物件執行完回撥(可以通過 timeoutPerShutdownPhase 設定超時),藉由此機制,我們可以在需要的時候的時候實現應用非同步關閉邏輯。

d)非 web 應用 IoC 容器的優雅關閉

註冊 JVM shutdown hook:ConfigurableApplicationContext.registerShutdownHook(),範例如下:

public final class Boot {

    public static void main(final String[] args) throws Exception {
        ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("beans.xml");

        // add a shutdown hook for the above context...
        ctx.registerShutdownHook();

        // app runs here...

        // main method exits, hook is called prior to the app shutting down...
    }
}

2、ApplicationContextAware 和 BeanNameAware

a)ApplicationContextAware

物件可以通過實現 org.springframework.context.ApplicationContextAware 介面來獲取 ApplicationContext 資源。介面定義如下:

public interface ApplicationContextAware {
    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}

藉由此,ApplicationContext 建立的 bean 也可以反過來對它執行特定的操作。例如,對其它 bean 的存取。

這種機制雖然在某些場景會很有用,但是它引發了程式碼的耦合,破壞了 IoC 機制,因此並不推薦。

另外一種獲取 ApplicationContext 資源的方法是通過自動裝配的方式引入 ApplicationContext 物件依賴,如建構函式或者 setter。 推薦使用 @Autowired 註解,更加靈活方便。

b)BeanNameAware

介面定義如下:

public interface BeanNameAware {

    void setBeanName(String name) throws BeansException;
}

其方法呼叫時機為:bean 屬性組裝完畢,初始化方法(InitializingBean.afterPropertiesSet() 或自定義初始化方法)呼叫之前。

3、其它 Aware 資源介面

ApplicationEventPublisherAware、BeanClassLoaderAware、BeanFactoryAware、LoadTimeWeaverAware、MessageSourceAware、NotificationPublisherAware、ResourceLoaderAware、ServletConfigAware、ServletContextAware。

六、容器擴充套件

1、BeanPostProcessor 自定義 bean 特性

BeanPostProcessor 介面通過提供回撥方法來實現使用者自定義邏輯。可以根據需要設定多個不同的實現,呼叫順序通過 Ordered 設定。

org.springframework.beans.factory.config.BeanPostProcessor 介面包含兩個回撥方法:

package org.springframework.beans.factory.config;

import org.springframework.beans.BeansException;
import org.springframework.lang.Nullable;

public interface BeanPostProcessor {
    @Nullable
    default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Nullable
    default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}

分別對應初始化方法(InitializingBean.afterPropertiesSet() 或自定義初始化方法)前後需要執行的邏輯。

ApplicationContext 能夠自動檢測實現了 BeanPostProcessor 介面的 bean,並把它們註冊為前置處理器,以便之後在其它 bean 建立的時候呼叫。

需要注意的是:如果是使用 @Bean 註解工廠方法。那麼返回的結果需要是實現了 BeanPostProcessor 介面的類物件型別。否則 ApplicationContext 在建立它前無法自動檢測其型別。

附:程式設計方式註冊 BeanPostProcessor 範例(不推薦)

可以通過 addBeanPostProcessor 方法向 ConfigurableBeanFactory 註冊。通過此方式註冊範例 Ordered 介面作用將失效,會按照註冊的順序執行,並且優先於所有自動檢測註冊的前置處理器。

應用範例:AutowiredAnnotationBeanPostProcessor

2、BeanFactoryPostProcessor 自定義 bean 定義

BeanFactoryPostProcessor 用於操作 bean 設定後設資料

可以設定多個 BeanFactoryPostProcessor 範例並通過實現 Ordered 介面設定呼叫順序。

應用:PropertySourcesPlaceholderConfigurer、PropertyOverrideConfigurer

七、基於註解的容器設定

基於註解的注入先於 XML 設定。同樣的注入會產生覆蓋。

1、@Autowired 自動裝配

用在哪裡注入裝配哪裡。byType。

a)構造器:

public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

物件只有多個構造器的時候,用以標明哪個構造器供容器使用。

b)setter 方法:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Autowired
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}

c)任意方法,任意引數:

public class MovieRecommender {

    private MovieCatalog movieCatalog;

    private CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public void prepare(MovieCatalog movieCatalog,
            CustomerPreferenceDao customerPreferenceDao) {
        this.movieCatalog = movieCatalog;
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

d)屬性欄位:

public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    private MovieCatalog movieCatalog;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

e)獲取容器內特定型別物件集合

預設需要至少有一個特定型別物件,否則會發生裝配失敗。

資料和列表

public class MovieRecommender {

    @Autowired
    private MovieCatalog[] movieCatalogs;

    // ...
}

或

public class MovieRecommender {

    private Set<MovieCatalog> movieCatalogs;

    @Autowired
    public void setMovieCatalogs(Set<MovieCatalog> movieCatalogs) {
        this.movieCatalogs = movieCatalogs;
    }

    // ...
}

如果需要列表裡的元素排序,則可以對收集的物件應用 Ordered 介面,或者新增 @Order 註解。

Map 型別收集:key 為 bean 名稱,value 為 bean 物件。

public class MovieRecommender {

    private Map<String, MovieCatalog> movieCatalogs;

    @Autowired
    public void setMovieCatalogs(Map<String, MovieCatalog> movieCatalogs) {
        this.movieCatalogs = movieCatalogs;
    }

    // ...
}

f)預設裝配行為

對於方法及屬性欄位的註解預設行為為必須。可以通過設定變更:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Autowired(required = false)
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}
也可以通過 java.util.Optional(>= Java 8) 或者 @Nullable(Spring Framework 5.0) 來標示可選。
public class SimpleMovieLister {

    @Autowired
    public void setMovieFinder(Optional<MovieFinder> movieFinder) {
        ...
    }
}

或者

public class SimpleMovieLister {

    @Autowired
    public void setMovieFinder(@Nullable MovieFinder movieFinder) {
        ...
    }
}

g)裝配使用特定已知框架資源

例如 BeanFactory、ApplicationContext、Environment、ResourceLoader、ApplicationEventPublisher 及 MessageSource 等。

如上介面的擴充套件介面,如ConfigurableApplicationContext、ResourcePatternResolver 等。

如下:

public class MovieRecommender {

    @Autowired
    private ApplicationContext context;

    public MovieRecommender() {
    }

    // ...
}

h)附註

@Autowired、@Inject、@Value  及@Resource 註解都是通過 BeanPostProcessor 介面實現處理的。因此不能應用於自定義的 BeanPostProcessor 或者 BeanFactoryPostProcessor 型別實現。

2、@Primary

當存在多個同型別裝配物件時,可以通過 @Primary 來標示使用哪個物件。如下:

@Configuration
public class MovieConfiguration {

    @Bean
    @Primary
    public MovieCatalog firstMovieCatalog() { ... }

    @Bean
    public MovieCatalog secondMovieCatalog() { ... }

    // ...
}

。。。

public class MovieRecommender {

    @Autowired
    private MovieCatalog movieCatalog; //firstMovieCatalog
// ... }

3、Qualifiers

@Qualifier 縮小符合裝配的物件範圍。

public class MovieRecommender {

    @Autowired
    @Qualifier("main")
    private MovieCatalog movieCatalog;

    // ...
}

或

public class MovieRecommender {

    private final MovieCatalog movieCatalog;

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public void prepare(@Qualifier("main") MovieCatalog movieCatalog,
            CustomerPreferenceDao customerPreferenceDao) {
        this.movieCatalog = movieCatalog;
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

符合 Qualifier 值得物件可以不唯一。

4、@Resource

 JSR-250 註解。byName。

註解 bean 屬性欄位或者 setter 方法。

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Resource(name="myMovieFinder") 
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

如果不指定名稱,丟與屬性欄位則使用欄位名,對於 setter 方法,則使用 bean 屬性名。

首先通過名稱查詢,如果找不到則通過型別查詢。

5、@Value

用於注入外部設定。

組態檔:application.properties

catalog.name=MovieCatalog

設定:

@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig { }

使用:

@Component
public class MovieRecommender {

    private final String catalog;

    public MovieRecommender(@Value("${catalog.name}") String catalog) {
        this.catalog = catalog;
    }
}

八、基於 Java 程式碼的容器設定

1、核心概念

基於 @Configuration 註解的類 和 基於 @Bean 註解的方法。

@Bean 註解作用於方法,用於範例化,設定及初始化一個新物件,並且新物件將由 Spring 容器管理。其和基於 XML 設定的 <bean/> 元素作用相同。

@Bean 通常和 @Configuration 結合使用(也可以用在任何 Spring @Component 註解管理的物件內)。 

@Configuration 註解的類主要用作 bean 定義。包括相關依賴 bean 的定義。簡單範例如下:

@Configuration
public class AppConfig {

    @Bean
    public MyServiceImpl myService() {
        return new MyServiceImpl();
    }
} 

等同於:

<beans>
    <bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>

2、AnnotationConfigApplicationContext

ApplicationContext 的一種實現,Spring 3.0 引入。可以同時處理 @Configuration、@Component 及 JSR-330 註解的類。

@Configuration 註解的類:包括其內所有 @Bean 註解的方法都被註冊為 bean 定義。

對於 @Component 及 JSR-330 註解的類:除了類本身會被註冊為 bean 定義,並且會處理其內 @Autowired 或者 @Inject 註解相關的注入邏輯。

a)如下基於構造器範例:

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
} 

//

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}  

b)如下基於編碼註冊處理範例:

public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.register(AppConfig.class, OtherConfig.class);
    ctx.register(AdditionalConfig.class);
    ctx.refresh();
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}

c)基於自動掃描處理範例:

@Configuration
@ComponentScan(basePackages = "com.acme") (1)
public class AppConfig  {
    // ...
}

// 或

<beans>
    <context:component-scan base-package="com.acme"/>
</beans>

AnnotationConfigApplicationContext 暴露了相應的 scan 介面,用於編碼方式執行掃描操作:

public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.scan("com.acme");
    ctx.refresh();
    MyService myService = ctx.getBean(MyService.class);
}

d、AnnotationConfigWebApplicationContext

Spring web 應用。包括註冊 Spring ContextLoaderListener servlet listener、Spring MVC DispatcherServlet 等等。

3、@Bean 註解

a)bean 定義

註解方法,用於 bean 定義註冊。方法名稱預設為 bean 名稱。返回值型別為 bean 物件型別(具體實現類或者對應介面型別)。

範例見 1、基礎概念處。

也可以通過介面預設方法定義:

public interface BaseConfig {

    @Bean
    default TransferServiceImpl transferService() {
        return new TransferServiceImpl();
    }
}

@Configuration
public class AppConfig implements BaseConfig {

}

b)bean 依賴

bean 定義方法可以有任意多個引數,通過如下方式定義依賴:

@Configuration
public class AppConfig {

    @Bean
    public TransferService transferService(AccountRepository accountRepository) {
        return new TransferServiceImpl(accountRepository);
    }
} 

c)生命週期回撥

public class BeanOne {

    public void init() {
        // initialization logic
    }
}

public class BeanTwo {

    public void cleanup() {
        // destruction logic
    }
}

@Configuration
public class AppConfig {

    @Bean(initMethod = "init")
    public BeanOne beanOne() {
        return new BeanOne();
    }

    @Bean(destroyMethod = "cleanup")
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}

c)bean 作用域定義

@Configuration
public class MyConfiguration {

    @Bean
    @Scope("prototype")
    public Encryptor encryptor() {
        // ...
    }
} 

scope-procy:

@Configuration
public class MyConfiguration {

    @Bean
    @Scope(proxyMode = ScopedProxyMode.INTERFACES)
    public Encryptor encryptor() {
        // ...
    }
}

d)自定義 bean 名稱

定義單個名稱或者多個:

@Configuration
public class AppConfig {

    @Bean("myThing")
    public Thing thing() {
        return new Thing();
    }
}

//

@Configuration
public class AppConfig {

    @Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
    public DataSource dataSource() {
        // instantiate, configure and return DataSource bean...
    }
}

4、@Configuration 註解

如下:通過方法呼叫注入依賴

@Configuration
public class AppConfig {

    @Bean
    public ClientService clientService1() {
        ClientServiceImpl clientService = new ClientServiceImpl();
        clientService.setClientDao(clientDao());
        return clientService;
    }

    @Bean
    public ClientService clientService2() {
        ClientServiceImpl clientService = new ClientServiceImpl();
        clientService.setClientDao(clientDao());
        return clientService;
    }

    @Bean
    public ClientDao clientDao() {
        return new ClientDaoImpl();
    }
}

預設單例模式管理的 bean 定義。如上,兩次 clientDao() 方法呼叫並不會產生兩個 ClientDao 物件。在物件範例化時會首先檢查容器相應 bean 範例物件快取,然後再決定是否需要呼叫相應的範例化方法。

查詢方法注入:

public abstract class CommandManager {
    public Object process(Object commandState) {
        // grab a new instance of the appropriate Command interface
        Command command = createCommand();
        // set the state on the (hopefully brand new) Command instance
        command.setState(commandState);
        return command.execute();
    }

    // okay... but where is the implementation of this method?
    protected abstract Command createCommand();
}

//

@Bean
@Scope("prototype")
public AsyncCommand asyncCommand() {
    AsyncCommand command = new AsyncCommand();
    // inject dependencies here as required
    return command;
}

@Bean
public CommandManager commandManager() {
    // return new anonymous implementation of CommandManager with createCommand()
    // overridden to return a new prototype Command object
    return new CommandManager() {
        protected Command createCommand() {
            return asyncCommand();
        }
    }
}

5、Import 註解

用於引入設定類,如下:

@Configuration
public class ConfigA {

    @Bean
    public A a() {
        return new A();
    }
}
//
@Configuration @Import(ConfigA.class) public class ConfigB { @Bean public B b() { return new B(); } }

如上,容器範例化時,只需要處理 ConfigB 即可:

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);

    // now both beans A and B will be available...
    A a = ctx.getBean(A.class);
    B b = ctx.getBean(B.class);
}  

Spring Framework 4.2 開始,除了 @Configuration 註解物件,@Import 可以同時處理其它型別物件。

當需要精細處理物件依賴引入時,可以使用此註解,避免大包掃描。

九、環境抽象

Environment 介面為容器環境資源抽象,主要包括:profiles 和 properties 兩方面。

profile:一種命名的邏輯組。可以設定 bean 定義率屬於哪個特定 profile,從而在相應 profile 被啟用時進行註冊。profile 可以設定當前活躍及預設活躍。

Properties:資源服務介面,提供獲取及設定相應資源能力。 源包括:properties 檔案、JVM system properties、system environment variables、JNDI、servlet context parameters、ad-hoc Properties objects、Map objects等。

1、Bean Definition Profiles

提供機制實現容器不同環境註冊不同的 bean。藉由此,我們可以實現一些類似如下場景功能:

  • 測試環境使用基於記憶體的資料來源,QA 及 生產環境使用 JNDI 資料來源。

  • 只線上上環境啟動監控功能。

  • 針對不同使用者註冊不同的功能 bean 物件。

@Profile 註解

如下:JndiDataConfig 設定類只在 profile 為 production 時進行容器註冊,使用

@Configuration
@Profile("production")
public class JndiDataConfig {

    @Bean(destroyMethod = "") (1)
    public DataSource dataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

註解 value 可以使單一 profile 值,也可以是多個值得陣列,亦或者為組合表示式。

@Profile("production")
@Profile({"QA", "production"})
@Profile("QA&production")

 自定義 profile 環境註解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}

//等價於

@Profile("production") 

@Profile 作為頂層環境設定,控制所有組合使用的註解資源,如@Configuration、@Import等。

 profile 啟用

直接編碼方式設定:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();  

或環境變數屬性設定:

-Dspring.profiles.active="profile1,profile2"    

2、PropertySource 抽象

Spring Environment 抽象提供屬性查詢操作(基於可設定的,層級的屬性源),如下:

ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);

PropertySource:k-v 組設定資源抽象。

StandardEnvironment 基於兩類屬性源設定:JVM 系統屬性(System.getProperties())和系統環境變數(System.getenv())。

StandardServletEnvironment 除了上述兩項設定外,還包括 servlet config、servlet context parameters 及 JndiPropertySource(如果存在 JNDI 資源需求)。

搜尋操作是層級執行的,預設情況下,系統屬性優先於環境變數,如果同一個屬性在兩個地方都有設定,則系統屬性優先返回。其它的都會被忽略。

StandardServletEnvironment 的層次結構如下:

  1. ServletConfig parameters (if applicable — for example, in case of a DispatcherServlet context)

  2. ServletContext parameters (web.xml context-param entries)

  3. JNDI environment variables (java:comp/env/ entries)

  4. JVM system properties (-D command-line arguments)

  5. JVM system environment (operating system environment variables)

我們也可以新增自定義的 PropertySource 並將其新增當前環境 PropertySource 組,如下:

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

通過 MutablePropertySources 暴露的方法 addFirst(),將自定義的 MyPropertySource 新增到資源最上層位置,優先供給查詢。 

@PropertySource 使用 

app.properties 檔案:

testbean.name=myTestBean

註解引入:

@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {

    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

  

@PropertySource 位置 ${…​} 預留位置會使用環境內其它已註冊的 PropertySource 資源處理。如下:

@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {

    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

先查詢已註冊 PropertySource 資源,查詢不到則使用預設 "default/path",如果不存則丟擲異常 IllegalArgumentException。

十、ApplicationContext 擴充套件功能

1、MessageSource 國際化(i18n)

ApplicationContext 通過實現 MessageSource 介面來提供國際化(i18n)功能。

Spring 同時也提供了 HierarchicalMessageSource 介面,用以逐層獲取特定訊息。

標準方法:

String getMessage(String code, Object[] args, String default, Locale loc) 

ApplicationContext 載入時會自動查詢容器內定義的 MessageSource bean,且 bean 的名稱必須為 messageSource。

當執行訊息查詢獲取操作時,Spring 會將操作代理給命名為 messageSource 的 bean。如果不存在此 bean,則從父類別中查詢,如果找不到則範例化一個空的 DelegatingMessageSource 用以執行相應的方法操作。

Spring 提供了三種 MessageSource 介面實現:ResourceBundleMessageSource、ReloadableResourceBundleMessageSource 及 StaticMessageSource。它們也都實現了 HierarchicalMessageSource 介面用以執行巢狀訊息查詢。StaticMessageSource 很少使用,主要用於提供編碼方式新增訊息。

ResourceBundleMessageSource 使用如下:

定義:

@Bean
public ResourceBundleMessageSource resourceBundleMessageSource() {
    ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
    resourceBundleMessageSource.setBasenames("format", "exceptions", "windows")
return resourceBundleMessageSource; } // 或 <beans> <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basenames"> <list> <value>format</value> <value>exceptions</value> <value>windows</value> </list> </property> </bean> </beans>

format.properties:

message=Alligators rock!

exception.properties

argument.required=The {0} argument is required.  

使用:

public static void main(String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("message", null, "Default", Locale.ENGLISH);
    System.out.println(message);
}

對於不同的 Locale,則定義相應的不同資原始檔:

Locale 範例:

public final class Locale implements Cloneable, Serializable {

    static private final  Cache LOCALECACHE = new Cache();

    /** Useful constant for language.
     */
    static public final Locale ENGLISH = createConstant("en", "");

    /** Useful constant for language.
     */
    static public final Locale JAPANESE = createConstant("ja", "");

    /** Useful constant for language.
     */
    static public final Locale CHINESE = createConstant("zh", "");

    /** Useful constant for language.
     */
    static public final Locale SIMPLIFIED_CHINESE = createConstant("zh", "CN");

    /** Useful constant for language.
     */
    static public final Locale TRADITIONAL_CHINESE = createConstant("zh", "TW");

... ...

資原始檔範例:

exceptions_zh_CN.properties、exceptions_ja.properties

Spring MessageSource 是基於 JAVA MessageSource,所以並不會合併相同 basename 的 bundle。

Spring 另外提供了 ReloadableResourceBundleMessageSource 介面。除了可以提供如上基本功能外,它可以從任意 Spring 定義的資源位置讀取檔案,並且支援熱載入。

2、標準的及自定義的事件

ApplicationContext 通過 ApplicationEvent 類及 ApplicationListener 介面來處理事件。容器內實現了 ApplicationListener 介面的物件能夠獲取任何 ApplicationEvent 釋出的事件。 這種屬於標準的觀察者模式應用。

Spring 4.2 之後,事件框架做了顯著升級,包括基於註解的實現及事件物件不再需要顯式的繼承 ApplicationEvent。

如下為 Spring 提供的標準事件:

ContextRefreshedEvent、ContextStartedEvent、ContextStoppedEvent、ContextClosedEvent、RequestHandledEvent、ServletRequestHandledEvent。

自定義事件:

事件定義:

public class BlockedListEvent extends ApplicationEvent {

    private final String address;
    private final String content;

    public BlockedListEvent(Object source, String address, String content) {
        super(source);
        this.address = address;
        this.content = content;
    }

    // accessor and other methods...
}  

事件釋出:

public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blockedList;
    private ApplicationEventPublisher publisher;

    public void setBlockedList(List<String> blockedList) {
        this.blockedList = blockedList;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String content) {
        if (blockedList.contains(address)) {
            publisher.publishEvent(new BlockedListEvent(this, address, content));
            return;
        }
        // send email...
    }
} 

事件監聽:

public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

基於註解的事件監聽:

public class BlockedListNotifier {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    @EventListener
    public void processBlockedListEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

非同步監聽:

@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
    // BlockedListEvent is processed in a separate thread
}

順序監聽:

@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress...
}

3、應用啟動追蹤

ApplicationContext 負責管理 Spring 應用的生命週期。應用啟動追蹤主要用於測量不同啟動步驟時間花費。

AbstractApplicationContext 通過 ApplicationStartup 介面,收集各個啟動階段 StartupStep 資料:

  • application context lifecycle (base packages scanning, config classes management)

  • beans lifecycle (instantiation, smart initialization, post processing)

  • application events processing

AnnotationConfigApplicationContext 應用範例:

// create a startup step and start recording
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// add tagging information to the current step
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// perform the actual phase we're instrumenting
this.scanner.scan(basePackages);
// end the current step
scanPackages.end();

可以通過實現 ApplicationStartup 介面,自定義啟動追蹤類。如下:

.setApplicationStartup(new MyApplicationStartup())