Spring Boot中全註解下的Spring IoC

前言

對於Spring框架,其中IoC(控制翻轉)和AOP(面向切面編程)都是比較重要的概念,而在Spring Boot中主要使用全註解的方式來實現IoC和AOP的功能,因此本文以Spring Boot框架爲基礎,對其中全註解下的IoC和AOP的相關基礎性概念和使用進行介紹,以爲後續Spring Boot學習打下基礎。

Spring Boot中全註解下的Spring IoC

IoC容器簡介

IoC(Inversion of Control,控制反轉)是一種通過描述來生成或者獲取對象的技術,它不是Spring中獨有的。它的作用可以理解爲:開始學java創建對象時常使用的就是new關鍵字來創建對象,而在Spring中則是通過描述(XML或註解)來創建對象,而對象的創建和管理是由IoC容器(類似於工廠方法)來負責。而一個系統中不可能只有一個對象,而且對象之間還具有依賴的關係,這時就需要依賴注入的方式,通過描述來管理各個對象之間的關聯關係。

Spring中將每個需要管理的對象稱爲Bean,而Spring管理這些Bean的容器,被稱爲Spring IoC容器(簡稱爲IoC容器),IoC容器具有兩個基本功能;

  1. 通過描述來管理Bean,包括創建和獲取Bean。
  2. 通過描述來完成Bean之間的依賴關係的建立。

在Spring的定義中,要求所有的IoC容器都需要實現BeanFactory頂級容器接口,該接口的源碼如下:

package org.springframework.beans.factory;

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

public interface BeanFactory {
    // IoC容器管理的Bean名稱的前綴
    String FACTORY_BEAN_PREFIX = "&";

    // 一系列獲取Bean的方法
    Object getBean(String var1) throws BeansException;

    <T> T getBean(String var1, Class<T> var2) throws BeansException;

    Object getBean(String var1, Object... var2) throws BeansException;

    <T> T getBean(Class<T> var1) throws BeansException;

    <T> T getBean(Class<T> var1, Object... var2) throws BeansException;

    <T> ObjectProvider<T> getBeanProvider(Class<T> var1);

    <T> ObjectProvider<T> getBeanProvider(ResolvableType var1);

    // IoC容器中是否包含Bean
    boolean containsBean(String var1);

    // Bean是否爲單例
    boolean isSingleton(String var1) throws NoSuchBeanDefinitionException;

    // Bean是否爲原型
    boolean isPrototype(String var1) throws NoSuchBeanDefinitionException;

    // 是否匹配類型
    boolean isTypeMatch(String var1, ResolvableType var2) throws NoSuchBeanDefinitionException;

    boolean isTypeMatch(String var1, Class<?> var2) throws NoSuchBeanDefinitionException;

    // 獲取Bean的類型
    @Nullable
    Class<?> getType(String var1) throws NoSuchBeanDefinitionException;

    @Nullable
    Class<?> getType(String var1, boolean var2) throws NoSuchBeanDefinitionException;
    
    // 獲取Bean的別名
    String[] getAliases(String var1);
}

在這個頂級接口中對相關的方法進行了註釋。同時可以看到上面的代碼中包含了很多getBean的重載方法,包括類型(byType),根據名稱(byName)獲取Bean,這意味着在IoC容器中可以使用Bean類型或者名稱來獲取相應的Bean,這也是後面依賴注入(Dependency Injection,DI)的基礎。

isSingleton方法是判斷Bean在容器中是否爲單例。在IoC容器中,Bean默認爲代理存在的,也就是使用getBean方法返回的Bean都是同一個。相對應的如果isPrototype方法返回爲true,則表示以原型模式的方法創建Bean,那麼在使用getBean方法獲取Bean時,IoC容器會新建Bean進行返回。

由於BeanFactory爲IoC容器的頂層接口,Spring在此基礎上設計了更高級的接口ApplicationContext,現實中我們使用的大部分Spring IoC容器是ApplicationContext接口的實現類。在Spring Boot中,bean的裝備主要是通過註解來實現的,因此下面使用AnnotationConfigApplicationContext(基於註解的IoC容器)來簡單實現一個Bean的裝配與使用,這種過程實際上和Spring Boot實現的Bean的裝配和獲取的方法是一致的。

簡單的POJO:

@Data
public class UserBean {
    private Long id;
    private String userName;
    private Integer sex;
    private String note;

    public UserBean() {
    }

    public UserBean(Long id, String userName, Integer sex, String note) {
        this.id = id;
        this.userName = userName;
        this.sex = sex;
        this.note = note;
    }
}

定義Spring的配置類文件:

@Configuration
public class AppConfig {
    @Bean(name = "UserBean")
    public UserBean getInit() {
        return new UserBean(1L, "yitian", 1, "none");
    }
}

@Configuration註解表示這是一個Java配置文件,Spring容器會根據它來生成IoC容器去裝配Bean。

@Bean註解表示該方法返回的對象將作爲Bean裝配到IoC容器中,name屬性指明該Bean的名稱,如果沒有配置,則默認爲方法名。 

使用AnnotationConfigApplicationContext:

public class IoCTest {
    private static Logger logger = LoggerFactory.getLogger(IoCTest.class);

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        UserBean userBean = context.getBean(UserBean.class);
        logger.info(userBean.getUserName());
    }
}

運行該main方法,可以看到如下日誌輸出:

15:14:10.871 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@29444d75
15:14:10.886 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalConfigurationAnnotationProcessor'
15:14:10.974 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerProcessor'
15:14:10.976 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.event.internalEventListenerFactory'
15:14:10.977 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalAutowiredAnnotationProcessor'
15:14:10.978 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'org.springframework.context.annotation.internalCommonAnnotationProcessor'
15:14:10.984 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig'
15:14:10.988 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'UserBean'
15:14:11.018 [main] INFO cn.zyt.springbootlearning.ioc.IoCTest - yitian

將Bean裝配到IoC容器中

在傳統Spring框架中使用XML的方式來裝配Bean,而Spring Boot中主要使用全註解的方式將Bean裝配到IoC容器中。可以使用如下幾個不同的註解方式。

使用@Component或@ComponentScan

@Component註解指明哪個類被掃描到Spring IoC容器中,而@ComponentScan表示採用何種方式去掃描裝配Bean。例如對上述UserBean類,加入@Component註解,其所在package路徑爲:cn.zyt.springbootlearning.ioc.config.UserBean。

@Data
@Component("UserBean")
public class UserBean {
    @Value("1")
    private Long id;
 
    @Value("yitian")
    private String userName;
    
    @Value("1")
    private Integer sex;
    
    @Value("none")
    private String note;
    
    //...
}

此時爲了使Spring IoC容器裝配該類,改造AppConfig(所在package爲:cn.zyt.springbootlearning.ioc.config.AppConfig)如下:

@Configuration
@ComponentScan("cn.zyt.springbootlearning.ioc.*")
public class AppConfig {
//    @Bean(name = "UserBean")
//    public UserBean getInit() {
//        return new UserBean(1L, "yitian", 1, "none");
//    }
}

@ComponentScan註解標註了指定的掃描包和子包來裝備其中的所有Bean。如果不進行設置,則默認掃描該類當前所在的包和子包。對於指定要掃描的包,@ComponentScan註解還有其他其個屬性,可以從該註解的源碼中進行查看:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
    // 定義掃描的包
    @AliasFor("basePackages")
    String[] value() default {};
    
    // 同上
    @AliasFor("value")
    String[] basePackages() default {};

    // 定義掃描的類
    Class<?>[] basePackageClasses() default {};

    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

    Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;

    ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;

    String resourcePattern() default "**/*.class";

    boolean useDefaultFilters() default true;
    
    // 當滿足過濾條件時掃描
    ComponentScan.Filter[] includeFilters() default {};

    // 當不滿足過濾條件時掃描
    ComponentScan.Filter[] excludeFilters() default {};

    // 是否延遲初始化
    boolean lazyInit() default false;

    // 定義過濾器
    @Retention(RetentionPolicy.RUNTIME)
    @Target({})
    public @interface Filter {
        FilterType type() default FilterType.ANNOTATION;

        @AliasFor("classes")
        Class<?>[] value() default {};

        @AliasFor("value")
        Class<?>[] classes() default {};

        String[] pattern() default {};
    }
}

其中常用的屬性使用註釋進行了說明。配置向basePackages定義掃描的包名,basePackageClasses定義掃描的類名,所以上面的@ComponentScan註解還可以使用如下方式進行配置。此時運行IoCTest中的main方法,依然得到上述一樣的日誌輸出。

@ComponentScan(basePackages = "cn.zyt.springbootlearning.ioc.*")
或
@ComponentScan(basePackageClasses = {UserBean.class})

而上述的方式會將指定包下的所有Bean裝配到IoC容器中,而如果對於該指定包下的一些其他Bean不想裝配,則可以使用@ComponentScan註解中的includeFilters和excludeFilters屬性,例如,在上述cn.zyt.springbootlearning.ioc包下除了config包,還包括一個service包,其中定義了UserService類,該類使用了@Service註解,默認情況下也會被裝配到IoC容器中。如果此時僅想裝配UserBean而不裝配UserService,則可以使用excludeFilters屬性:

@ComponentScan(value = "cn.zyt.springbootlearning.ioc.*", 
        excludeFilters = {@ComponentScan.Filter(classes = {Service.class})})

這樣就可以僅掃描UserBean,而過濾掉@Service定義的Bean。 

裝配第三方的Bean

在現實中的應用中,常需要引入第三方的包來進行操作,此時需要將第三方包的對象裝配到IoC容器中進行使用,此時就可以使用@Bean註解完成這項功能。例如,在maven中引入如下的第三方依賴:

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.42</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-dbcp2</artifactId>
        </dependency>

此時需要根據引入的依賴創建數據源來連接數據庫,將如下代碼加入到AppConfig類中:

    @Bean("DbcpDataSource")
    public DataSource getDataSource() {
        Properties properties = new Properties();
        properties.setProperty("driver", "com.mysql.jdbc.Driver");
        properties.setProperty("url", "jdbc:mysql://localhost:3306/springboot_dev");
        properties.setProperty("username", "root");
        properties.setProperty("password", "123456");
        DataSource dataSource = null;

        try {
            dataSource = BasicDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }

通過@Bean就可以將這裏創建的DataSource對象裝配到IoC容器中了。

依賴注入

IoC容器的另一個作用爲管理Bean之間的依賴,下面以Person對象依賴Animal對象來實現一些服務的例子,說明IoC容器中Bean之間的依賴注入。首先創建Person接口和其實現類,Animal接口和其實現類。

public interface Person {
    /**
     * 人依賴於動物提供服務
     */
    void service();

    /**
     * 設置依賴的動物
     */
    void setAnimal(Animal animal);
}

public interface Animal {
    /**
     * 動物可以提供的服務
     */
    void use();
}

上面兩個接口的實現類如下:

@Component
public class BusinessPerson implements Person {

    @Autowired
    private Animal animal;

    @Override
    public void service() {
        this.animal.use();
    }

    @Override
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }
}

@Component
public class Dog implements Animal {
    @Override
    public void use() {
        System.out.println("DOG can save person!");
    }
}

注意BusinessPerson類中使用的@Autowired註解 ,它會根據屬性的類型(buType)找到對應的Bean進行注入,因爲Dog爲Animal的一種,所以這裏IoC容器會將Dog的Bean注入到BusinessPerson對象中,這樣BusinessPerson對象就可以通過注入的Dog對象進行相關的服務了。可以使用如下的代碼進行運行測試:

    @Test
    public void testPerson() {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        Person person = context.getBean(BusinessPerson.class);
        person.service();
    }

運行輸出如下:

...
17:50:49.712 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig'
17:50:49.745 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'dog'
DOG can save person!

@Autowired註解 

下面進一步探索一下@Autowired註解的使用,上面創建了Dog類,下面可以創建一個Cat同樣是實現了Animal接口:

@Component
public class Cat implements Animal {
    @Override
    public void use() {
        System.out.println("CAT can catch the mice!");
    }
}

此時在運行上面的測試代碼,發現會得到如下異常:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'businessPerson': Unsatisfied dependency expressed through field 'animal'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'cn.zyt.springbootlearning.ioc.injection.Animal' available: expected single matching bean but found 2: cat,dog

	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:643)
	at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:116)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:399)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1422)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:594)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:517)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:323)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:321)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:879)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:878)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:550)
	at org.springframework.context.annotation.AnnotationConfigApplicationContext.<init>(AnnotationConfigApplicationContext.java:89)
	at cn.zyt.springbootlearning.ioc.IoCTest.testPerson(IoCTest.java:30)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
	at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
	at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'cn.zyt.springbootlearning.ioc.injection.Animal' available: expected single matching bean but found 2: cat,dog
	at org.springframework.beans.factory.config.DependencyDescriptor.resolveNotUnique(DependencyDescriptor.java:220)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1265)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1207)
	at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:640)
	... 36 more

根據異常的信息,可以看出來這裏是因爲Animal有兩個實現類並都裝配成了Bean,@Autowired註解是根據byType的方式進行查找並注入Bean的,因此在IoC容器對Animal對象進行注入的時候就不知道注入哪一個了。

解決該問題其實很簡單,@Autowired註解首先是根據byType的方式getBean,如果找不到則會使用byName的方式進行查找。因此如果這裏需要使用的animal實際上是Dog,則將BusinessPerson中的屬性改爲如下即可:

@Component
public class BusinessPerson implements Person {
    @Autowired
    private Animal dog;

    @Override
    public void service() {
        this.dog.use();
    }

    @Override
    public void setAnimal(Animal animal) {
        this.dog = animal;
    }
}

此時運行測試代碼,得到的輸出和上述的輸出一樣,當想使用Cat對象進行服務的時候,則將屬性名稱該爲cat即可。需要注意的是@Autowired註解是一個默認必須找到對應Bean的註解,如果先後根據byType和byName的方式都沒有找到相應的Bean,則會拋出異常。如果允許某個Bean可以爲null,那麼可以爲@Autowired註解設置required=false屬性。

@Primary和Quelifier註解

此外,除了上面依賴於@Autowired註解根據bytype和byName先後順序進行Bean查找的方式,還有其他的方式可以解決上述的歧義性。@Primary註解可以修改Bean的優先級,當爲Cat類加上該註解時,說明Cat的優先級大於Dog,則這是IoC容器會優先將Cat對象進行注入,因此不會產生歧義:

@Component
@Primary
public class Cat implements Animal {
    @Override
    public void use() {
        System.out.println("CAT can catch the mice!");
    }
}

此時將BusinessPerson類中的屬性名改回animal,並運行測試方法可以恢復正常。但如果此時Dog類中也加入了@Primary註解,則歧義問題將會仍存在。

這時就需要使用@Qualifier註解,使用該註解可以設置其value屬性,爲需要注入的Bean的名稱,這樣IoC容器就會同時根據byTypeandName的方式來尋找相應的Bean並注入。此時也就是依賴於BeanFactory類中的如下重載方法:

<T> T getBean(String var1, Class<T> var2) throws BeansException;

在保留Cat類中的@Primary註解,並在BussinessPerson類中進行如下設置時:

    @Autowired
    @Qualifier("dog")
    private Animal animal;

運行測試方法沒有異常,並且輸出:

DOG can save person!

因此說明,該註解作用的優先級是大於@Primary註解的。 

Bean的生命週期

上述的內容僅是正確的將Bean裝配到IoC容器中,而沒有關心Bean如何創建和銷燬的過程。有時需要自定義Bean的初始化和銷燬方法,以滿足一些特殊Bean的要求。此時就需要對Bean的生命週期進行了解。

Spring IoC初始化和銷燬Bean的過程大致分爲如下四個步驟:

  1. Bean的定義。該過程包括如下幾個步驟。(1)資源定位,使用@ComponentScan註解定義的掃描路徑將帶有@Component的類找到。(2)通過找到的資源對Bean進行解析,此時還沒有初始化Bean,僅是解析的過程。(3)將Bean的定義發佈到IoC容器中,此時IoC容器中具有了Bean的定義,依然沒有Bean實例的創建。
  2. Bean的初始化。開始創建的Bean的實例,在默認情況下,Spring IoC容器會在容器初始化的時候執行Bean的實例化並進行依賴注入,也就是在獲取一個Bean之前,Bean的實例已經創建完成了。如果需要延遲初始化,可以在@ComponentScan註解中使用lazyInit屬性進行配置,該屬性默認爲false,改爲true即可以設置爲延遲初始化。
  3. Bean的生存期。
  4. Bean的銷燬。

Spring Bean的聲明週期過程圖:

將上述的BusinessPerson修改爲如下,對Bean的生命週期進行測試:

@Component
public class BusinessPerson implements Person, BeanNameAware, BeanFactoryAware,
        ApplicationContextAware, InitializingBean, DisposableBean {
    private Animal animal;

    @Override
    public void service() {
        this.animal.use();
    }

    @Override
    @Autowired
    @Qualifier("dog")
    public void setAnimal(Animal animal) {
        System.out.println("延遲依賴注入測試");
        this.animal = animal;
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("【" + this.getClass().getSimpleName() + "】調用BeanFactoryAware的setBeanFactory方法");

    }

    @Override
    public void setBeanName(String s) {
        System.out.println("【" + this.getClass().getSimpleName() + "】調用BeanNameAware的setBeanName方法");
    }

    @PostConstruct
    public void initCustom() {
        System.out.println("【" + this.getClass().getSimpleName() + "】調用@PostConstruct的initCustom方法");
    }

    @PreDestroy
    public void destroyCustom() {
        System.out.println("【" + this.getClass().getSimpleName() + "】調用@PreDestroy的destroyCustom方法");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("【" + this.getClass().getSimpleName() + "】調用DisposableBean的destroy方法");
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("【" + this.getClass().getSimpleName() + "】調用InitializingBean的afterPropertiesSet方法");
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println("【" + this.getClass().getSimpleName() + "】調用ApplicationContextAware的setApplicationContext方法");
    }
}

這樣找個Bean就試下了生命週期中單個Bean可以實現的所有接口,並且通過註解@PostConstruct定義了初始化方法,@PreDestroy定義了銷燬方法。爲了測試Bean的後置處理器,這裏創建一個BeanProcessorExample示例類:

@Component
public class BeanPostProcessorExample implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("BeanPostProcessor調用postProcessBeforeInitialization方法,參數【"
                + bean.getClass().getSimpleName() + "】【" + beanName + "】");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        System.out.println("BeanPostProcessor調用postProcessAfterInitialization方法,參數【"
                + bean.getClass().getSimpleName() + "】【" + beanName + "】");
        return null;
    }
}

運行如下代碼:

    @Test
    public void beanLifecycleTest() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        context.close();
    }

 得到的日誌輸出如下:

BeanPostProcessor調用postProcessBeforeInitialization方法,參數【AppConfig$$EnhancerBySpringCGLIB$$32ff2f1c】【appConfig】
BeanPostProcessor調用postProcessAfterInitialization方法,參數【AppConfig$$EnhancerBySpringCGLIB$$32ff2f1c】【appConfig】
21:19:06.041 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'UserBean'
BeanPostProcessor調用postProcessBeforeInitialization方法,參數【UserBean】【UserBean】
BeanPostProcessor調用postProcessAfterInitialization方法,參數【UserBean】【UserBean】
21:19:06.052 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'businessPerson'
21:19:06.086 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'dog'
BeanPostProcessor調用postProcessBeforeInitialization方法,參數【Dog】【dog】
BeanPostProcessor調用postProcessAfterInitialization方法,參數【Dog】【dog】
延遲依賴注入測試
【BusinessPerson】調用BeanNameAware的setBeanName方法
【BusinessPerson】調用BeanFactoryAware的setBeanFactory方法
【BusinessPerson】調用ApplicationContextAware的setApplicationContext方法
BeanPostProcessor調用postProcessBeforeInitialization方法,參數【BusinessPerson】【businessPerson】
【BusinessPerson】調用@PostConstruct的initCustom方法
【BusinessPerson】調用InitializingBean的afterPropertiesSet方法
BeanPostProcessor調用postProcessAfterInitialization方法,參數【BusinessPerson】【businessPerson】
21:19:06.087 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'cat'
BeanPostProcessor調用postProcessBeforeInitialization方法,參數【Cat】【cat】
BeanPostProcessor調用postProcessAfterInitialization方法,參數【Cat】【cat】
21:19:06.088 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'DbcpDataSource'
BeanPostProcessor調用postProcessBeforeInitialization方法,參數【BasicDataSource】【DbcpDataSource】
BeanPostProcessor調用postProcessAfterInitialization方法,參數【BasicDataSource】【DbcpDataSource】
21:19:06.125 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@402f32ff, started on Thu Feb 20 21:19:05 CST 2020
【BusinessPerson】調用@PreDestroy的destroyCustom方法
【BusinessPerson】調用DisposableBean的destroy方法

通過日誌可以看到,對於Bean的後置處理器BeanProcessor而言,其中的方法對所有的Bean生效。並且在BusinessPerson類中的方法執行順序與上述生命週期過程圖一致。

對於Bean生命週期的底層詳解可以參考:https://www.jianshu.com/p/1dec08d290c1

使用屬性配置文件

在默認application.properties配置文件中使用自定義屬性

Spring Boot項目中默認使用的是application.properties配置文件,其中添加的已經被定義的屬性會被Spring Boot自動讀取到上下文中。如果需要在application.properties配置文件中添加自定義的屬性,並在項目中引用,則需要首先加入如下的依賴:

        <!-- 自定義屬性配置依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>

這樣在application.properties文件中添加的自定義屬性就可以在項目中引用了。例如在application.properties配置文件中定義瞭如下屬性:

# 自定義的一些屬性
database.drivername=com.mysql.jdbc.Driver
database.url=jdbc:mysql://localhost:3306/springboot_dev
database.username=root
database.password=123456

使用Spring EL表達式將配置文件中的內容賦值到類屬性中:

@Component
public class DatabaseProperties {

    @Value("${database.drivername}")
    private String driverName;

    @Value("${database.url}")
    private String url;

    private String username;

    private String password;

    public String getDriverName() {
        return driverName;
    }

    public void setDriverName(String driverName) {
        System.out.println(driverName);
        this.driverName = driverName;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        System.out.println(url);
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    @Value("${database.username}")
    public void setUsername(String username) {
        System.out.println(username);
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    @Value("${database.password}")
    public void setPassword(String password) {
        System.out.println(password);
        this.password = password;
    }
}

啓動Spring Boot項目可以看到日誌輸出如下:

root
123456

此外,如果需要將自定義的屬性配置在其他配置文件中,例如將上述數據庫連接的屬性配置在了jdbc.properties文件中,那麼可以使用@PropertySource註解在Spring Boot啓動類中定義需要加載的自定義屬性文件,將他加載到Spring上下文中,例如:

@SpringBootApplication(scanBasePackages = {"cn.zyt.springbootlearning.*"})
@MapperScan(basePackages = "cn.zyt.springbootlearning.*", annotationClass = Repository.class)
@PropertySource(value = {"classpath:jdbc.properties"}, ignoreResourceNotFound = true)
public class SpringbootLearningApplication {
    // ...
}

此時已然可以找到在jdbc.properties配置文件中的自定義屬性並進行加載。而其中的ignoreResourceNotFound屬性設置爲true, 表示在該配置文件找不到時進行忽略而不會報錯。

application.yml默認配置文件

Spring Boot中除了默認的application.properties配置文件之外,還有一個application.yml配置文件,如果使用後者作爲配置文件可以將application.properties刪除,新建名爲application.yml文件即可。yml格式的配置文件支持樹型的配置格式,例如如下:

server:
  port: 8080
  session-timeout: 30
  tomcat.max-threads: 0
  tomcat.uri-encoding: UTF-8

spring:
  datasource:
    url : jdbc:mysql://localhost:3306/springboot
    username : root
    password : root
    driverClassName : com.mysql.jdbc.Driver

它是將application.properties文件中的配置項,使用樹形結構表示,比較清晰。 

條件裝配Bean

@Conditional註解支持Spring Boot中Bean的條件裝配,也就是在IoC容器在裝配Bean會對指定的一些條件進行檢查,從而避免一些Bean裝配的異常。例如如下代碼,在裝配DataSource Bean之前,先檢查是否存在相應的配置項:

    @Bean
    @Conditional(DatabaseConditional.class)
    public DataSource getDataSourceWithConditional(
            @Value("${database.drivername}") String driver,
            @Value("${database.url}") String url,
            @Value("${database.username}") String username,
            @Value("${database.password}") String password) {
        Properties properties = new Properties();
        properties.setProperty("driver", driver);
        properties.setProperty("url", url);
        properties.setProperty("username", username);
        properties.setProperty("password", password);

        DataSource dataSource = null;
        try {
            dataSource = BasicDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }

相應的DatabaseConditional類如下:

public class DatabaseConditional implements Condition {
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        // 獲取環境配置
        Environment environment = conditionContext.getEnvironment();
        // 檢查配置文件中是否包含相應的屬性
        return environment.containsProperty("database.drivername")
                && environment.containsProperty("database.url")
                && environment.containsProperty("database.username")
                && environment.containsProperty("database.password");
    }
}

在裝配Bean之前matches方法首先讀取其上下文環境,判斷配置文件中是否包含該Bean需要的屬性。如果返回爲false,則不裝配該Bean。

Bean的作用域

在一般的容器中,Bean存在單例(singleton)和原型(prototype)兩種作用域。而在Web容器中,則存在如下四種作用域:

  1. page,頁面
  2. request,請求
  3. session,會話
  4. application,應用

其中對於page,是針對JSP當前頁面的作用域,Spring是無法支持的。爲了滿足各類的作用域,在Spring的作用域中就存在瞭如下的幾種類型:

Bean的作用域類型 使用範圍 描述
singleton 所有Spring應用 默認作用域,IoC容器中只存在單例
prototype 所有Spring應用 每當從IoC容器中取出一個Bean時,則新創建一個Bean
session Spring Web應用 HTTP會話
application Spring Web應用 Web工程聲明週期
request Spring Web應用 Web工程單次請求(request)
globalSession Spring Web應用 在一個全局HTTP session中,一個Bean定義對應一個勝利。基本不使用

以上6周Bean的作用域中,常用的爲前四種,下面對單例和原型兩種作用域進行一些測試。

首先創建一個Bean,由於沒有任何作用域的設置,所以此時爲默認的作用域singleton:

@Component
public class ScopeBean {
}

使用如下測試代碼,先後從容器中獲取兩個該類型的Bean,然後進行比較:

    @Test
    public void beanScopeTest() {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        ScopeBean scopeBean = context.getBean(ScopeBean.class);
        ScopeBean scopeBean1 = context.getBean(ScopeBean.class);
        System.out.println(scopeBean == scopeBean1);
    }

運行得到的結果爲true,說明先後兩次得到的ScopeBean類型的Bean指向了同一個實例對象,所以默認在ioC容器中Bean只有一個。下面將ScopeBean修改爲:

@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ScopeBean {
}

運行同樣的測試,得到的結果爲false,說明在對個該Bean的作用域設置爲原型後,每次取出的Bean都是一個新創建的實例。

上面使用的爲ConfigurableBeanFactory,其只支持單例和原型兩種範圍,如果使用Spring MVC環境中的WebApplicationContext來定義作用域,其支持SCOPE_REQUEST, SCOPE_SESSION, SCOPE_APPLICATION的作用域。 

使用@Profile

在企業開發的過程中,項目往往會存在開發環境,測試環境,預發環境,和生產環境不同的應用環境,而每套環境中的上下文等是不同的,例如他們會有各自的數據源和相應的數據庫,這樣就需要在不同的數據庫之間進行切換。Spring @Profile註解爲此提供了支持。

下面假設存在dev和test兩個數據庫,可以使用@Profile註解來定義兩個不同的Bean:

@Bean("DevDataSource")
    @Profile("dev")
    public DataSource getDevDataSource() {
        Properties properties = new Properties();
        properties.setProperty("driver", "com.mysql.jdbc.Driver");
        properties.setProperty("url", "jdbc:mysql://localhost:3306/dev");
        properties.setProperty("username", "root");
        properties.setProperty("password", "123456");

        DataSource dataSource = null;
        try {
            dataSource = BasicDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }

    @Bean("TestDataSource")
    @Profile("test")
    public DataSource getTestDataSource() {
        Properties properties = new Properties();
        properties.setProperty("driver", "com.mysql.jdbc.Driver");
        properties.setProperty("url", "jdbc:mysql://localhost:3306/test");
        properties.setProperty("username", "root");
        properties.setProperty("password", "123456");

        DataSource dataSource = null;
        try {
            dataSource = BasicDataSourceFactory.createDataSource(properties);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }

在Spring中存在兩個參數來啓動並修改對應的profile,分別是spring.profiles.active和spring.profiles.default。這兩個屬性在默認沒有配置的情況下,Spring將不會啓動profile機制,這意味着被@Profile標註的Bean不會被裝配到IoC容器中。Spring會先判斷是否存在spring.profiles.active配置後,在去查找spring.profiles.default。

在Java啓動項目中,只需要加入如下的啓動配置,就能夠啓動相應環境的bean:

JAVA_OPTS="-Dspring.profiles.active=dev"

在IDEA中啓動時,可以在運行參數中對其進行設置(這裏是Java Application):

 

 在IoCTest類中的main方法進行測試:

    public static void main(String[] args) throws SQLException {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        context.getBean(DataSource.class);
    }

可以看到如下的日誌輸出:

12:05:35.209 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'DevDataSource'

此時將DevDataSource Bean進行了裝配,而沒有裝配TestDataSource。

此外,Spring Boot中支持在不同環境中配置文件的切換,對於dev環境下的配置文件可以新增application-dev.properties,然後在啓動參數中設置 -Dspring.profiles.active=dev時,就會對應的加載application-dev.properties的配置文件。

發佈了302 篇原創文章 · 獲贊 46 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章