天天看點

Spring Boot中全注解下的Spring IoC前言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的聲明周期過程圖:

Spring Boot中全注解下的Spring IoC前言Spring Boot中全注解下的Spring IoC

将上述的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.AnnotationConfigA[email protected], 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):

Spring Boot中全注解下的Spring IoC前言Spring Boot中全注解下的Spring IoC

 在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的配置檔案。

繼續閱讀