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的配置文件。

发布了322 篇原创文章 · 获赞 64 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/yitian_z/article/details/104410670
今日推荐