全注解下的Spring IOC

写在前面

IOC是一种通过描述来生成和获取对象的技术,这里的描述可以是xml的配置文件,也可以是java config,异或可以是标注了某些特定注解的类,如@Component,@Service等,最终将描述所表达的bean信息放到容器中,这个容器我们称之为spring IOC容器,对应的顶层接口是org.springframework.beans.factory.BeanFactory。通过IOC我们就有了一组对象,那么对象之间的关系如何解决呢,即某个对象依赖于另一个或者几个对象,怎么办呢?这就要依赖注入了,依赖注入简单将就是自动的从容器中获取某个spring bean依赖,并自动为其设置值。当然,这种依赖关系也是通过描述来完成的。

1:IOC容器简介

IOC容器很简单,就是要存储bean,获取bean,这是最基本的功能,为了表述这个最基本的功能,spring定义了一个顶层接口org.springframework.beans.factory.BeanFactory,就描述了这些行文,为了有更加清晰的认知,我们来通过源码看下其定义的方法:

// org.springframework.beans.factory.BeanFactory
public interface BeanFactory {
	// 工厂bean前缀
	String FACTORY_BEAN_PREFIX = "&";
	// 各种获取bean的方法,byName,byType等
	Object getBean(String name) throws BeansException;
	<T> T getBean(String name, Class<T> requiredType) throws BeansException;
	Object getBean(String name, Object... args) throws BeansException;
	<T> T getBean(Class<T> requiredType) throws BeansException;
	<T> T getBean(Class<T> requiredType, Object... args) throws BeansException;
	// 是否为单例。默认为true,即spring bean默认是单例的
	boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
	// 是否为原型,即每次获取bean都返回不同的实例
	boolean isPrototype(String name) throws NoSuchBeanDefinitionException;
	// 类型是否匹配
	boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;
	boolean isTypeMatch(String name, Class<?> typeToMatch) throws NoSuchBeanDefinitionException;
	// 获取bean的类型
	@Nullable
	Class<?> getType(String name) throws NoSuchBeanDefinitionException;
}

可以看到主要是定义了bean的获取相关操作,这是IOC最核心的功能,但是显然对于实际的场景,这些功能远远不够,比如国际化能力,资源加载能力,事件处理能力,为此,spring又定义了ApplicationContext接口,这也是我们在实际工作中直接接触和使用最多的接口,依然通过其源码来看下其能力:

public interface ApplicationContext 
extends EnvironmentCapable, 
ListableBeanFactory, 
HierarchicalBeanFactory,
MessageSource, 
ApplicationEventPublisher, 
ResourcePatternResolver {}

可以看到其实现了定义各种功能的接口,如国际化接口MessageSource,资源解析接口ResourcePatternResolver。因此ApplicationContext的能力要比BeanFactory强得多。接下来我们通过注解的方式来看下如何使用ApplicationContext来定义和获取spring bean,首先我们需要定义一个POJO,源码如下:

public class User {
    private Long id;
    private String userName;
    private String note;
	
	// 省略get set
}

然后定义javaconfig类来描述User为spring bean:

@Configuration
public class AppConfig {

    @Bean(name = "user")
    public User initUser() {
        User user = new User();
        user.setId(1L);
        user.setUserName("user_name_1");
        return user;
    }
}

其中的@Configuration代表这是一个java config类,作用同使用标签定义的xml文件,@Bean(name = "user")中@Bean代表是将该方法返回的结果作为spring bean,其中bean名称使用user,如果是不指定name的话,则使用方法名称作为bean名称,如下获取spring bean代码:

public class IoCTest {

    public static void main(String[] args) {
        ApplicationContext ac
                = new AnnotationConfigApplicationContext(AppConfig.class);
        // byType方式获取
        User user = ac.getBean(User.class);
        System.out.println(user.getId());
    }
}

运行输出1则说明成功了,此时我们要注意到一个问题,那就是如果是每个我们想要定义为spring bean的类都这么写的话未免也太麻烦了,为了解决这个问题spring 提供了扫描自动注册为springbean的功能,具体我们通过2:装配你的bean来一起看下。

2:装配你的bean

为了能够自动扫描类并注册为spring bean,spring提供了@Component,和@ComponentScan注解,其中用来确定扫描谁,@ComponentScan用来确定扫描哪里

2.1:通过扫描装配你的bean

先来定义一个需要被扫描的类,放到dongshi.daddy.springioc.three_two.config包中,源码如下:

@Component("user")
public class User {
    @Value("11")
    private Long id;
    @Value("user_name_1")
    private String userName;
    @Value("note_1")
    private String note;
    
    // 省略读写方法
}

定义配置类,放到dongshi.daddy.springioc.three_two.config中,源码如下:

@Configuration
// 默认扫描当前类所在包以及其子包下的@Component注解
@ComponentScan
public class AppConfig {
}

定义测试类,源码如下:

public class AppConfigTest {
    public static void main(String[] args) {
        ApplicationContext ac
                = new AnnotationConfigApplicationContext(AppConfig.class);
        System.out.println(ac.getBean("user", User.class).getId());
    }
}

运行:

11

注意到我们在AppConifg类中只是定义了@ComponentScan注解,但是并没有设置扫描哪里,但是却扫描到我们的类,这是因为,@ComponentScan注解的默认扫描路径就是所在包以及其子包,但是如果是为了能够扫描到而将bean都定义在config包下,显然是不合理的,基于这个点,@ComponentScan允许我们自定义要扫描的路径,为了更加清晰的了解如何自定义路径,先来看下@ComponentScan注解的源码:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
	// 通过basePackages
	@AliasFor("basePackages")
	String[] value() default {};
	
	// 设置要扫描的包路径
	@AliasFor("value")
	String[] basePackages() default {};

	// 基于给定的class所在包作为基础扫描路径
	Class<?>[] basePackageClasses() default {};

    // 当满足过滤器的条件时扫描
	Filter[] includeFilters() default {};
	
	// 当不满足过滤器的条件时过滤
	Filter[] excludeFilters() default {};

	// 扫描到的bean是否延迟初始化
	boolean lazyInit() default false;
}

此时我们将User类从dongshi.daddy.springioc.three_two.config移动到dongshi.daddy.springioc.three_two.pojo,此时再运行程序就会报如下错误了:

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: 
No bean named 'user' available

因为按照默认的扫描规则已经无法扫描到了,此时我们将AppConfig中的@ComponentScan修改为@ComponentScan(basePackages = { "dongshi.daddy.springioc.three_two.*" }),再次运行程序就正常了。此时我们在dongshi.daddy.springioc.three_two.service包中定义UserService。如下:

@Service
public class UserService {
}

@Service注解组合了@Component注解,所以效果同@Component,其它类似的注解还有@Controller,@Repository等。

此时因为我们定义的扫描路径是dongshi.daddy.springioc.three_two.*,因此UserService也会被扫描并注册为spring bean,如果不希望扫描,可以使用@ExcludeFilters进行特定排除。修改为如下:

@ComponentScan(basePackages = { "dongshi.daddy.springioc.three_two.*" },
        excludeFilters = { @ComponentScan.Filter(classes = {Service.class})})

excludeFilters = { @ComponentScan.Filter(classes = {Service.class})})表达的含义就是排除使用了org.springframework.stereotype.Service注解的类。
其实springboot和核心注解@SpringbootApplication 也组合了@ComponentScan注解,并进行了一些扩展,来满足自己特定的需求,来看下@SpringbootApplication的源码,如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
	// 别名:org.springframework.boot.autoconfigure.EnableAutoConfiguration#exclude
	@AliasFor(annotation = EnableAutoConfiguration.class)
	Class<?>[] exclude() default {};
	
	// 别名:EnableAutoConfiguration#excludeName
	@AliasFor(annotation = EnableAutoConfiguration.class)
	String[] excludeName() default {};

    // 别名:ComponentScan#basePackages
	@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
	String[] scanBasePackages() default {};

	// 别名:ComponentScan#basePackageClasses
	@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
	Class<?>[] scanBasePackageClasses() default {};
}

在组合了@ComponentScan注解定义bean扫描相关配置的同时,增加了自动配置扫描相关的包含和排除的配置。另外需要注意类中提供的exclude和excludeName只对自动配置生效,并不会影响扫描,如果是还想要自己排除某些bean的扫描,还是需要显式的使用@ComponentScan注解,如:

@SpringBootApplication
@ComponentScan(basePackages = { "dongshi.daddy.springioc.three_two.*" },
        excludeFilters = { @ComponentScan.Filter(classes = {Service.class})})

在项目中不可避免的需要引入第三方类,然后有时需要将部分类注册为spring bean,我们接下来看下这种情况应该怎么做。

2.2:自定义第三方bean

假设我们要引入DHCP数据源,首先增加如下的pom依赖:

<dependency>
    <groupId>org.apache.commonsgroupId>
    <artifactId>commons-dbcp2artifactId>
    <version>2.7.0version>
dependency>
<dependency>
    <groupId>mysqlgroupId>
    <artifactId>mysql-connector-javaartifactId>
    <version>5.1.47version>
dependency>

然后通过@Bean注解开发如下代码就可以了:

@Bean("dataSource")
public DataSource getDataSource() {
    Properties properties = new Properties();
    properties.setProperty("driver", "com.mysql.jdbc.Driver");
    properties.setProperty("url", "jdbc:mysql://localhost:3306/xxx");
    properties.setProperty("username", "root");
    properties.setProperty("password", "111111");
    DataSource dataSource = null;
    try {
        dataSource = BasicDataSourceFactory.createDataSource(properties);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return dataSource;
}

2.3:依赖注入

spring IOC主要提供了2个核心的功能,第一个是将spring bean装配到IOC容器中,另一个作用就是处理bean之间的依赖,其中第一个我们在前面已经进行了分享,本部分看下第二个功能,在spring的世界里这个处理bean之间依赖的功能叫做依赖注入(DI)。我们通过一个人类使用动物的例子来看下,首先根据依赖倒置的原则,定义人接口和动物接口,包路径为dongshi.daddy.springioc.three.three_three.bean

/**
 * 动物接口
 */
public interface Animal {
    /**
     * 使用动物功能
     */
    void use();
}
/**
 * 人类接口
 */
public interface Person {
    /**
     * 使用动物服务
     */
    void service();

    /**
     * 设置动物
     * @param animal
     */
    void setAnimal(Animal animal);
}

可以看到人类接口有一个需要spring IOC注入的动物引用属性,分别定义二者的实现类,如下:

/**
 * 动物实现类,狗
 */
@Component
public class Dog implements Animal {
    @Override
    public void use() {
        System.out.println("狗【" + Dog.class.getSimpleName() + "】 是看门用的");
    }
}
/**
 * 人类实现类
 */
@Component("businessPerson")
public class BusinessPerson implements Person {
    @Autowired
    private Animal animal = null;

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

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

注意到使用了@Autowired,这是使用的byType的方式来注入对象值。然后定义java config类,包路径为dongshi.daddy.springioc.three.three_three.config,如下:


@Configuration
@ComponentScan(basePackages = { "dongshi.daddy.springioc.three.three_three.bean" })
public class AppConfig {
}

定义测试类:

public class MyTest {
    public static void main(String[] args) {
        ApplicationContext ac
                = new AnnotationConfigApplicationContext(AppConfig.class);
        ac.getBean("businessPerson", Person.class).service();
    }
}

运行:

狗【Dog】 是看门用的

在上述例子中我们使用了@Autowired注解,在实际工作中该注解我们也使用的比较多,接下来详细看下该注解的用法,

2.3.1:注解@Autowired

该注解默认通过byType的方式来注入对象,因此当目标类型的bean只有一个的时候,是可以正常工作的,但是当有大于1个的时候就会报错了,比如我们再定义一个动物的实现类Cat,Cat可以用来抓,如下:

/**
 * 动物实现类,
 */
@Component
public class Cat implements Animal {
    @Override
    public void use() {
        System.out.println("猫【" + Cat.class.getSimpleName() + "】 是用来抓老鼠的");
    }
}

再次运行测试则会报如下错误:

Unsatisfied dependency expressed through field 'animal'; nested exception is 
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying 
bean of type 'dongshi.daddy.springioc.three.three_three.bean.Animal' available: 
expected single matching bean but found 2: cat,dog

报错的原因就是根据类型Animal从ioc容器中找到了两个spring bean,想要解决这个问题我们需要先来了解下@Autowired注解获取bean的方式,首先是byType方式获取,如果没有找到,或者是不是找到唯一的一个,则会根据属性的名称从IOC容器中查找,因此我们可以将属性的名称修改为cat和dog中的一个,比如修改为如下代码:

 @Autowired
 private Animal cat = null;

再次运行:

猫【Cat】 是用来抓老鼠的

但是这种方式不够灵活,不够抽象,当然我们可以通过更好的方法来解决这个问题,具体看下2.3.2:消除歧义性---@Primary和@Qualifier

2.3.2:消除歧义性—@Primary和@Qualifier

当@Autowired基于byType的方式来注入bean时,但是却发现了超过1个的spring bean,这种情况我们叫做歧义性,那么如何通过@Primary和@Qualifier来消除歧义性呢,先来看@Primary,该注解用来定义多个满足的spring bean的优先级,即选择优先级最高的那个作为目标bean来进行注入,比如我们修改Dog类为如下:

@Component
@Primary
public class Dog implements Animal {
    @Override
    public void use() {
        System.out.println("狗【" + Dog.class.getSimpleName() + "】 是看门用的");
    }
}

就可以解决问题了,但是如果在Cat类上也增加@Primary注解,就又有歧义性了,因此@Primary注解并不能从根本上解决问题,此时我们需要引入@Qualifier注解,该注解的意思是指定bean名称,即从多个候选bean中找到bean名称为指定名称spring bean作为目标bean,执行的过程是byType确定一组,byName从一组确定唯一一个,对应的BeanFactory的方法为:

<T> T getBean(String name, Class<T> requiredType) throws BeansException;

因此可以修改注入方式为:

@Autowired
@Qualifier("cat")
private Animal animal = null;
2.3.3:基于构造函数注入

除了直接将@Autowired注解使用在构造函数的参数上,如下:

/**
 * 人类实现类
 */
@Component("businessPersonWithConstructor")
public class BusinessPersonWithConstructor implements Person {
    private Animal animal = null;

    public BusinessPersonWithConstructor(@Autowired @Qualifier("dog") Animal animal) {
        this.animal = animal;
    }

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

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

截止到目前我们只是知道了如何使用spring bean,但是这些spring bean到底是怎么来的,即生命周期是什么我们还没有了解,接下来通过2.4:生命周期来一起看下。

2.4:生命周期

我们有时候也需要自定义bean的初始化的销毁的过程,以满足特定场景下的特定需求,比如在前面定义的dataource数据源bean,在销毁bean的时候,我们希望能够通过调用其close方法来释放数据库的连接等资源,这种需求我们在项目中也经常遇到。那么的bea的生命周期是什么呢,从大的方面来说有以下四个部分:

1:bean的定义
2:bean的初始化
3:bean的生存期
4:bean的销毁

我们先来看bean的定义

1:首先根据配置,如@ComponentScan定义的路径扫描配置,即资源定位
2:加载到bean的配置后,将配置解析为bean定义
3:将bean定义发布到spring IOC容器中,用于后续的初始化。

默认的,在将bean定义发布到IOC容器中后,会进行初始化和依赖注入,但是呢,这种其实就是默认的不延迟初始化,如果想要延迟初始化的话,可以通过在@ComponentScan中设置lazyInit=true,定义如下的类:

@Component
public class BusinessPersonLazyInitFalse implements Person {
    private Animal animal;

    @Override
    public void service() {
    }

    @Override
    @Autowired @Qualifier("dog")
    public void setAnimal(Animal animal) {
        System.out.println("依赖注入了。。。");
        this.animal = animal;
    }
}

此时通过代码new AnnotationConfigApplicationContext(AppConfig.class);初始化容器的话,在加载完BusinessPersonLazyInitFalse对应的bean定义后,会直接进行初始化,并依赖注入,因此会输出如下内容:

依赖注入了。。。

此时其实我们并没有获取BusinessPersonLazyInitFalse对应的spring bean,但是也初始化和依赖注入了,有时候为了提高性能,我们可能希望不这样,而是获取的时候才初始化和注入,则可以修改AppConfig类为如下:

@Configuration
@ComponentScan(basePackages = { "dongshi.daddy.springioc.three.three_three.bean" }, lazyInit = true)
public class AppConfig {
}

此时只有在获取bean的时候才会初始化了,此时当我们通过代码new AnnotationConfigApplicationContext(AppConfig.class).getBean("businessPersonLazyInitFalse", Person.class).service();;获取bean的时候就又会输出依赖注入了。。。 了。

那么,当spring bean实例化,并依赖注入之后就结束了吗,并不是,因为spring还提供了很多很多的扩展接口,让我们干预到spring bean的初始化过程中,比如我们有这样的需求,在bean的属性都设置完毕后我们要自定义逻辑来验证设置的bean的属性是否符合要求,此时就需要了解spring提供的各种Aware接口了,spring默认提供的初始化操作和各种Aware接口加在一起才是spring bean的完整的生命周期,具体如下图:

全注解下的Spring IOC_第1张图片

为什么要延迟初始化呢,首先是影响启动性能,其次初始化的bean对象占用堆内存空间。

接下来我通过实际的例子来看下,定义如下的bean:

@Component("businessPersonLifecycle")
public class BusinessPersonLifecycle implements
        Person,
        BeanNameAware,
        BeanFactoryAware,
        ApplicationContextAware,
        InitializingBean,
        DisposableBean {

    private Animal animal = null;

    public BusinessPersonLifecycle(@Autowired @Qualifier("dog") Animal animal) {
        System.out.println("实例化和注入");
        this.animal = animal;
    }


    @Override
    public void service() {

    }

    @Override
    public void setAnimal(Animal animal) {

    }

    private String beanName;
    @Override
    public void setBeanName(String name) {
        System.out.println("设置bean name");
        this.beanName = name;
    }

    private BeanFactory beanFactory;
    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        System.out.println("设置beanFactory");
        this.beanFactory = beanFactory;
    }

    private ApplicationContext applicationContext;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        System.out.println("设置applicationContext");
        this.applicationContext = applicationContext;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        System.out.println("属性设置完毕了!!!");
    }

    @Override
    public void destroy() throws Exception {
        System.out.println("bean销毁了!!!");
    }

    @PostConstruct
    public void init() {
        System.out.println("@PostConstruct注解的初始化方法");
    }

    @PreDestroy
    public void predestory1() {
        System.out.println("@PreDestroy定义的销毁方法");
    }
}

然后使用如下代码测试:

public class MyTest {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ac
                = new AnnotationConfigApplicationContext(AppConfig.class);
        ac.getBean("businessPersonLifecycle", Person.class).service();
        ac.close();
    }
}

输出如下:

实例化和注入
设置bean name
设置beanFactory
设置applicationContext
@PostConstruct注解的初始化方法
属性设置完毕了!!!
@PreDestroy定义的销毁方法
bean销毁了!!!

此时我还没有添加全局的后置bean处理器(注意影响所有的bean,所以慎用!!!),如下:

@Component
public class MyBeanPostProcessorExample implements BeanPostProcessor {
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        if ("businessPersonLifecycle".equalsIgnoreCase(beanName))
            System.out.println("后置bean处理器初始化前");
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if ("businessPersonLifecycle".equalsIgnoreCase(beanName))
            System.out.println("后置bean处理器初始化后");
        return bean;
    }
}

再次运行测试代码输出如下:

实例化和注入
设置bean name
设置beanFactory
设置applicationContext
后置bean处理器初始化前
@PostConstruct注解的初始化方法
属性设置完毕了!!!
后置bean处理器初始化后
@PreDestroy定义的销毁方法
bean销毁了!!!

2.5:使用属性文件

本部分一起来看下如何读取配置文件中的属性,鉴于springboot使用较多,直接以springboot来看。

不介绍springboot基础环境的搭建,感性的朋友可以参考springboot的入门小栗子 。

springboot默认的配置文件是application.properties,因为我们先来定义如下的文件:

database.driverName=com.mysql.jdbc.Driver
database.url=jdbc:mysql://localhost:3306:chapter3
database.username=root
database.password=123456

然后定义使用该配置信息的bean:

@Component(value = "databaseProperties")
public class DatabaseProperties {
    @Value("${database.driverName}")
    private String driverName = null;

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

    private String username = null;

    private String password = null;

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

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


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

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

为了获取配置的值,我们定义一个bean并实现ApplicationContextAware接口并使用@PostConstruct注解来自定义初始化方法获取DatabaseProperties并输出其信息,如下:

@Component
public class MyBean implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @PostConstruct
    public void printSth() {
        DatabaseProperties properties = this.applicationContext.getBean(DatabaseProperties.class);
        System.out.println(properties.getDriverName());
        System.out.println(properties.getUrl());
        System.out.println(properties.getUsername());
        System.out.println(properties.getPassword());
    }
}

最后定义springboot main函数。如下:

@SpringBootApplication public class MySpringIocApplication {
    public static void main(String[] args) {
        SpringApplication.run(MySpringIocApplication.class);
    }
}

运行输出如下:

com.mysql.jdbc.Driver
jdbc:mysql://localhost:3306:chapter3
root
123456

这里我们需要注意到一个问题,那就是@Value注解在源码中出现了多次,同样database.注解也出现了多次,我们想要解决这个问题就需要使用到@EnableConfigurationProperties注解,修改为如下:

@Component(value = "databaseProperties")
@ConfigurationProperties("database")
public class DatabaseProperties {
//    @Value("${database.driverName}")
    private String driverName = null;

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

    private String username = null;

    private String password = null;

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

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


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

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

    public String getDriverName() {
        return driverName;
    }

    public String getUrl() {
        return url;
    }

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }

}

匹配的规则就是使用@ConfigurationProperties注解设置的值和属性名称拼接作为要从application.properties文件中获取的的配置项,如@ConfigurationProperties("database")+private String driverName = null;就是获取database.driverName=com.mysql.jdbc.Driver的值,运行结果不变。

我们再来思考另外一个问题,如果是我们将所有的配置都放到application.properties配置文件中会使得该文件非常臃肿,难以维护,此时可以考虑使用单独的配置文件来定义单独的业务,为了满足这个需求,我们需要使用@PropertySource注解来引入这些非默认的配置文件,比如我们
将上述配置移动到jdbc.properties配置文件中,然后将application.properties文件内容清空,如下:
jdbc.properties

database.driverName=com.mysql.jdbc.Driver
database.url=jdbc:mysql://localhost:3306:chapter3
database.username=root
database.password=123456

接着使用@PropertySource注解定义jdbc.properties,如下:

@PropertySource(value = { "classpath:jdbc.properties" }, ignoreResourceNotFound = true)
@SpringBootApplication public class MySpringIocApplication {
    public static void main(String[] args) {
        SpringApplication.run(MySpringIocApplication.class);
    }
}

设置的ignoreResourceNotFound = true是不存在指定的文件时忽略,默认是报错。

2.6:条件装配bean

我们经常会有这样的需求,对于bean的装配,当满足了特定的条件时才进行,比如数据库的数据源对象,当我们没有数据库连接相关的配置时就不装配数据源的bean。spring提供了@Conditional注解和Condition接口来完成对应的功能。这部分就看下如何实现这种功能。首先定义如下类:

@Configuration
public class AppConfig {

    /**
     * database.driverName=com.mysql.jdbc.Driver
     * database.url=jdbc:mysql://localhost:3306:chapter3
     * database.username=root
     * database.password=123456
     * @return
     */
    @Bean(name = "dataSource", destroyMethod = "close")
    @Conditional(DatabaseCondition.class)
    public DataSource getDataSource(
            @Value("database.driverName") String driverName,
            @Value("database.url") String url,
            @Value("database.username") String username,
            @Value("database.password") String password
    ) {
        Properties properties = new Properties();
        properties.setProperty("driver", driverName);
        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;
    }
}

其中DatabaseCondition是需要我们定义的条件类,如下:

public class DatabaseCondition implements Condition  {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment env = context.getEnvironment();
        /**
         * database.driverName=com.mysql.jdbc.Driver
         * database.url=jdbc:mysql://localhost:3306:chapter3
         * database.username=root
         * database.password=123456
         * @return
         */
        return env.containsProperty("database.driverName")
                && env.containsProperty("database.url")
                && env.containsProperty("database.username")
                && env.containsProperty("database.password");
    }
}

只有matches方法返回true。才会装配DataSource的bean,使用如下代码尝试获取bean:

@Component
public class MyBean implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @PostConstruct
    public void printSth() {
        this.applicationContext.getBean("dataSource", DataSource.class);
    }
}

当满足要求可以正常启动,下面我们修改配置文件的database.driverNamedatabase.driverName1,这样matches方法就返回false,不会装配bean,运行程序则会报如下错误:

***************************
APPLICATION FAILED TO START
***************************

Description:

A component required a bean named 'dataSource' that could not be found.

2.7:bean的作用域

我们在前面分析IOC的顶层接口BeanFactory时,可以看到定义了如下的两个方法:

boolean isSingleton(String name) throws NoSuchBeanDefinitionException;
boolean isPrototype(String name) throws NoSuchBeanDefinitionException;

其中方法isSingleton如果是返回true,则说明bean是单例的,如果isPrototype返回true则说明bean是原型的,其中单例的意思就是bean只存在一个实例,即每次调用getBean返回的都是同一个实例,如果是原型的则每次调用getBean返回的都是不同的实例,这就是bean的作用域,其中单例是默认的作用域。

你可能感兴趣的:(spring,ioc,依赖注入,BeanFactory)