Spring 注解学习笔记

1. 准备工作

创建一个 maven 项目

添加依赖

xml 复制代码
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.1.3</version>
</dependency>

<!-- https://mvnrepository.com/artifact/junit/junit -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.2</version>
    <scope>test</scope>
</dependency>

2. 注册组件

2.1 XML 方式

在工程的 bean 包下创建一个 Person 类

java 复制代码
public class Person {
    private String name;
    private Integer age;
}

在工程的 src/main/resources 目录下创建 Spring 的配置文件,例如 beans.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
    xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans 
                    http://www.springframework.org/schema/beans/spring-beans-4.2.xsd">
    
    <!-- 注册组件 -->
    <bean id="person" class="com.projectname.bean.Person">
        <property name="age" value="18"></property>
        <property name="name" value="liayun"></property>
    </bean>
            
</beans>

在工程的 根目录 创建一个 MainTest 类来进行测试

java 复制代码
public class MainTest {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
        Person person = (Person) applicationContext.getBean("person");
        System.out.println(person);
    } 
}

2.2 注解方式

@Configuration

@Bean

在项目的 config 包 下创建一个 MainConfig 类,并在该类上添加 @Configuration 注解来标注该类是一个Spring的配置类,最后通过 @Bean 注解将Person类注入到Spring的IOC容器中。

java 复制代码
/**
 * 以前配置文件的方式被替换成了配置类,即配置类==配置文件
 *
 */
// 这个配置类也是一个组件 
@Configuration // 告诉Spring这是一个配置类
public class MainConfig {

    // @Bean注解是给IOC容器中注册一个bean,类型自然就是返回值的类型,id默认是用方法名作为id
    @Bean
    public Person person() {
        return new Person("liayun", 20);
    }
        
}
java 复制代码
public class MainTest {

    public static void main(String[] args) {
            
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
        Person person = applicationContext.getBean(Person.class);
        System.out.println(person);
        
    }
        
}
  • 使用注解注入 JavaBean 时,bean 在 IOC 容器中的名称默认就是使用 @Bean 注解标注的方法名称
  • 如果要给 bean 单独指定名称,只要在@Bean注解中明确指定名称就可以了。比如将person01()方法上的@Bean注解修改成了@Bean("person")注解。

3. 包扫描

3.1 XML 方式

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
        xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans 
                                                http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
                                                http://www.springframework.org/schema/context 
                                                http://www.springframework.org/schema/context/spring-context-4.2.xsd">
        
        <!-- 包扫描:只要是标注了我们熟悉的@Controller、@Service、@Repository、@Component这四个注解中的任何一个的组件,它就会被自动扫描,并加进容器中 -->
        <context:component-scan base-package="com.projectname"></context:component-scan>
        
        <!-- 注册组件 -->
        <bean id="person" class="com.projectname.bean.Person">
                <property name="age" value="18"></property>
                <property name="name" value="liayun"></property>
        </bean>
        
</beans>

这样配置以后,只要在com.projectname包下,或者com.projectname的子包下标注了 @Repository、@Service、@Controller、@Component 注解的类都会被扫描到,并自动注入到Spring容器中。

此时,我们分别创建 BookDao、BookService 以及 BookController 这三个类,并在这三个类中分别添加@Repository、@Service、@Controller 注解

java 复制代码
@Repository
public class BookDao {
    
}
java 复制代码
@Service
public class BookService {
    
}
java 复制代码
@Controller
public class BookController {
    
}

写一个测试类

java 复制代码
public class IOCTest {
        
    @SuppressWarnings("resource")
    @Test
    public void test() {
        ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext("beans.xml");
        // 我们现在就来看一下IOC容器中有哪些bean,即容器中所有bean定义的名字
        String[] definitionNames = applicationContext.getBeanDefinitionNames();
        for (String name : definitionNames) {
                System.out.println(name);
        }
    }

}

除了输出我们自己创建的bean的名称之外,也输出了Spring内部使用的一些重要的bean的名称。

3.2 注解方式

使用 @ComponentScan 注解之前我们先将 beans.xml 配置文件中的下述配置注释掉。

xml 复制代码
<context:component-scan base-package="com.projectname"></context:component-scan>

在我们的 MainConfig 类上添加 @ComponentScan 注解,并将扫描的包指定为com.projectname即可。

java 复制代码
/**
 * 以前配置文件的方式被替换成了配置类,即配置类 == 配置文件
 *
 */
// 这个配置类也是一个组件 
@ComponentScan(value="com.projectname") // value指定要扫描的包
@Configuration // 告诉Spring这是一个配置类
public class MainConfig {

        // @Bean注解是给IOC容器中注册一个bean,类型自然就是返回值的类型,id默认是用方法名作为id
        @Bean("person")
        public Person person01() {
                return new Person("liayun", 20);
        }
        
}

写个测试类

java 复制代码
@Test
public void test01() {
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
    // 我们现在就来看一下IOC容器中有哪些bean,即容器中所有bean定义的名字
    String[] definitionNames = applicationContext.getBeanDefinitionNames();
    for (String name : definitionNames) {
        System.out.println(name);
    }
}

@ComponentScan

@Filter

ComponentScan类中的如下两个方法

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documentde
@Repeatable(ComponentScans.class)
public @interface ComponentScan{

    @AliasFor("basePackages")
    String[] value() default {};

    Filter[] includeFilters() default {};
    
    Filter[] excludeFilters() default {};
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target({})
    @interface Filter{
    
        FilterTpye type() default FilterType.ANNOTATION;
        
        @AliasFor("classes")
        Class<?>[] value() default {};
        
        @AliasFor("value")
        Class<?>[] value() default {};
        
        String[] pattern() default {};
    }
    
}

includeFilters() 方法指定 Spring 扫描的时候按照什么规则只需要包含哪些组件

excludeFilters() 方法指定 Spring 扫描的时候按照什么规则排除哪些组件

两个方法的返回值都是 Filter[] 数组

3.3 排除指定组件

需求:排除 @Controller 和 @Service 这俩注解标注的组件

实现:在 MainConfig 类上通过 @ComponentScan 注解的 excludeFilters() 方法实现

java 复制代码
@ComponentScan(
    /*
     * value 指定要扫描的包
     */
    value="com.projectname", 
    /*
     * excludeFilters 指定要排除的组件
     */
    excludeFilters={
        /*
         * type:指定你要排除的规则,是按照注解进行排除,还是按照给定的类型进行排除,还是按照正则表达式进行排除,等等
         * classes:排除@Controller和@Service这俩注解标注的组件。
         */
        @Filter(type=FilterType.ANNOTATION, classes={Controller.class, Service.class})
    }
)

3.4 只包含指定组件

注意:使用 includeFilters() 方法来指定只包含哪些注解标注的类时,需要 禁用 默认的过滤规则。

使用 xml 方式时,需要先配置好 use-default-filters="false",禁用默认的过滤规则,因为默认的过滤规则就是扫描所有的,只有我们禁用默认的过滤规则之后,只包含才能生效。

xml 复制代码
<context:component-scan base-package="com.projectname" use-default-filters="false"></context:component-scan>

需求:只包含 @Controller 注解标注的类

实现:在 MainConfig 类上添加 @ComponentScan 注解,设置只包含 @Controller 注解标注的类,并禁用掉默认的过滤规则

java 复制代码
@ComponentScan(
    value="com.projectname", 
    includeFilters={
        /*
         * type:指定你要排除的规则,是按照注解进行排除,还是按照给定的类型进行排除,还是按照正则表达式进行排除,等等
         * classes:我们需要Spring在扫描时,只包含@Controller注解标注的类
         */
        @Filter(type=FilterType.ANNOTATION, classes={Controller.class})
    }, 
    useDefaultFilters=false
) 

3.5 重复注解

ComponentScan 注解类

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documentde
@Repeatable(ComponentScans.class)
public @interface ComponentScan {

}

@ComponentScans

ComponentScans 注解类

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documentde
public @interface ComponentScans {

    ComponentScan[] value();

}

可以看到,在ComponentScans注解类的内部只声明了一个返回ComponentScan[]数组的value()方法,说到这里,大家是不是就明白了,没错,这在Java 8中是一个重复注解。

如果你用的是Java 8,那么@ComponentScan 注解就是一个重复注解,也就是说我们可以在一个类上重复使用这个注解

java 复制代码
@ComponentScan(value="com.projectname", includeFilters={
    @Filter(type=FilterType.ANNOTATION, classes={Controller.class})
}, useDefaultFilters=false)
    
@ComponentScan(value="com.projectname", includeFilters={
    @Filter(type=FilterType.ANNOTATION, classes={Service.class})
}, useDefaultFilters=false) 
    
@Configuration
public class MainConfig {

    @Bean("person")
    public Person person01() {
        return new Person("liayun", 20);
    }

}

如果你使用的是 Java 8 之前的版本,那也没有问题,虽然我们再也不能直接在类上写多个@ComponentScan注解了,但是我们可以在类上使用 @ComponentScans 注解,同样也可以指定多个 @ComponentScan

java 复制代码
@ComponentScans(value={
    @ComponentScan(value="com.projectname", includeFilters={
        @Filter(type=FilterType.ANNOTATION, classes={Controller.class})
    }, useDefaultFilters=false), 
    @ComponentScan(value="com.projectname", includeFilters={
        @Filter(type=FilterType.ANNOTATION, classes={Service.class})
    }, useDefaultFilters=false) 
})
@Configuration
public class MainConfig {

    @Bean("person")
    public Person person01() {
        return new Person("liayun", 20);
    }

}

3.6 过滤规则

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documentde
@Repeatable(ComponentScans.class)
public @interface ComponentScan{

    @AliasFor("basePackages")
    String[] value() default {};

    Filter[] includeFilters() default {};
    
    Filter[] excludeFilters() default {};
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target({})
    @interface Filter{
    
        FilterTpye type() default FilterType.ANNOTATION;
        
        @AliasFor("classes")
        Class<?>[] value() default {};
        
        @AliasFor("value")
        Class<?>[] value() default {};
        
        String[] pattern() default {};
    }
    
}

@Filter注解中的 type 属性是一个 FilterType 枚举类

java 复制代码
public enum FilterType {

    ANNOTATION,
    
    ASSIGNABLE_TYPE,
    
    ASPECTJ,
    
    REGEX,
    
    CUSTOM

}

1. ANNOTATION

按照注解进行包含或者排除

比如:按照注解只包含标注了 @Controller 注解的组件

java 复制代码
@ComponentScan(value="com.projectname", includeFilters={
    @Filter(type=FilterType.ANNOTATION, classes={Controller.class})
}, useDefaultFilters=false) 

2. ASSIGNABLE_TYPE

按照给定的类型进行包含或者排除

比如:按照给定的类型只包含 BookService 类(接口)或其子类(实现类或子接口)的组件

java 复制代码
@ComponentScan(value="com.projectname", includeFilters={
    // 只要是BookService这种类型的组件都会被加载到容器中,不管是它的子类还是它的实现类。
    @Filter(type=FilterType.ASSIGNABLE_TYPE, classes={BookService.class})
}, useDefaultFilters=false)

3. ASPECTJ

按照ASPECTJ表达式进行包含或者排除

java 复制代码
@ComponentScan(value="com.projectname", includeFilters={
    @Filter(type=FilterType.ASPECTJ, classes={AspectJTypeFilter.class})
}, useDefaultFilters=false)

这种过滤规则基本上不怎么用!

4. REGEX

按照正则表达式进行包含或者排除

java 复制代码
@ComponentScan(value="com.projectname", includeFilters={
    @Filter(type=FilterType.REGEX, classes={RegexPatternTypeFilter.class})
}, useDefaultFilters=false)

这种过滤规则基本上也不怎么用!

5. 自定义规则

创建一个自定义规则的类

实现org.springframework.core.type.filter.TypeFilter接口

java 复制代码
public class MyTypeFilter implements TypeFilter {

    /**
     * 参数:
     * metadataReader:读取到的当前正在扫描的类的信息
     * metadataReaderFactory:可以获取到其他任何类的信息的(工厂)
     */
    @Override
    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {


        // 获取当前类注解的信息
        AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
        // 获取当前正在扫描的类的类信息,比如说它的类型是什么啊,它实现了什么接口啊之类的
        ClassMetadata classMetadata = metadataReader.getClassMetadata();
        // 获取当前类的资源信息,比如说类的路径等信息
        Resource resource = metadataReader.getResource();
        // 获取当前正在扫描的类的类名
        String className = classMetadata.getClassName();
        System.out.println("--->" + className);


        // 现在来指定一个规则
        if (className.contains("er")) {
                return true;
        }
          
                
        // 返回 true 表示符合规则, false 表示不符合规则
        return false; 

    }

}

使用@ComponentScan注解进行如下配置

java 复制代码
@ComponentScan(value="com.projectname", includeFilters={
    // 指定自定义的过滤规则
    @Filter(type=FilterType.CUSTOM, classes={MyTypeFilter.class})
}, useDefaultFilters=false) 

我们现在扫描的是com.projectname包,该包下的每一个类都会进到这个自定义规则里面进行匹配,若匹配成功,则就会被包含在容器中。

4. 组件作用域

Spring 容器中的组件默认是单例的,在 Spring 启动时就会实例化并初始化这些对象,并将其放到 Spring 容器中,之后,每次获取对象时,直接从 Spring 容器中获取,而不再创建对象。如果每次从Spring容器中获取对象时,都要创建一个新的实例对象,那么该如何处理呢?此时就需要使用@Scope注解来设置组件的作用域了。

4.1 基本使用

java 复制代码
@Configuration
public class MainConfig {

    @Scope("prototype")
    @Bean
    public Person person() {
        return new Person("liayun", 20);
    }

}

4.2 @Scope

从@Scope注解类的源码中可以看出,在@Scope注解中可以设置如下值:

ConfigurableBeanFactory#SCOPE_PROTOTYPE

ConfigurableBeanFactory#SCOPE_SINGLETON

org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST

org.springframework.web.context.WebApplicationContext#SCOPE_SESSION

看一下 ConfigurableBeanFactory 接口

java 复制代码
public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {
    String SCOPE_SINGLETON = "singleton";
    String SCOPE_PROTOTYPE = "prototype";
}    

可以发现,SCOPE_SINGLETON就是singleton,而SCOPE_PROTOTYPE就是prototype。

当我们使用 Web 容器来运行 Spring 应用时,在 @Scope 注解中可以设置 WebApplicationContext 类中的 SCOPE_REQUEST 和SCOPE_SESSION这俩的值,而 SCOPE_REQUEST 的值就是 request ,SCOPE_SESSION 的值就是 session。

1. singleton

  • 表示组件在 Spring 容器中是单实例的,这个是 spring 的默认值
  • Spring 在启动时会将组件进行实例化并加载的 spring 容器中,之后,每次从Spring 容器中获取组件时,直接将实例对象返回,而不会创建新的实例对象。

2. prototype

  • 表示组件在 Spring 容器中是多实例的
  • Spring 在启动时并不会将组件进行实例化操作,而是每次从Spring 容器中获取组件对象时,都会创建新的实例对象并返回。

3. request

  • Spring 容器 必须在 web 环境中
  • 每次请求都会创建一个新的实例对象,同一次请求只创建一个实例对象

4. session

  • Spring 容器 必须在 web 环境中
  • 在同一个session范围内,只创建一个实例对象

5. application

  • Spring 容器 必须在 web 环境中,全局web应用级别的作用域
  • 一个web应用程序对应一个bean实例,通常情况下和 singleton 效果类似。不过也有不一样的地方,singleton 是每个spring容器只有一个bean实例,当一个应用程序中有多个spring容器时,不同的容器可以存在同名的bean。而对于 application ,不管应用程序中有多少个Spring容器,这个应用程序中同名的bean只有一个

其中,request 和 session 作用域是需要 Web 环境来支持的,这两个值基本上使用不到。当我们使用 Web 容器来运行 Spring 应用时,如果需要将组件的实例对象的作用域设置为 request 和 session ,那么我们通常会使用

request.setAttribute("key", object); 和 session.setAttribute("key", object);

这两种形式来将对象实例设置到 request 和 session 中,而不会使用 @Scope 注解来进行设置。

6. 自定义

第一步,实现Scope接口。

第二步,将自定义Scope注册到容器中。

第三步,使用自定义的作用域。也就是在定义bean的时候,指定bean的scope属性为自定义的作用域名称。

java 复制代码
package org.springframework.beans.factory.config;

import org.springframework.beans.factory.ObjectFactory;

public interface Scope {

    /**
     * 返回当前作用域中 name 对应的 bean 对象
     * @param name 需要检索的 bean 对象的名称
     * @param objectFactory 如果name对应的 bean 对象在当前作用域中没有找到,那么可以调用这个 objectFactory来创建这个对象
     */
    Object get(String name, ObjectFactory<?> objectFactory);

    /**
     * 将 name 对应的 bean 对象从当前作用域中移除
     */
    Object remove(String name);

    /**
     * 用于注册销毁回调,若想要销毁相应的对象,则由Spring容器注册相应的销毁回调,而由自定义作用域选择是不是要销毁相应的对象
     */
    void registerDestructionCallback(String name, Runnable callback);

    /**
     * 用于解析相应的上下文数据,比如 request 作用域将返回 request 中的属性
     */
    Object resolveContextualObject(String key);

    /**
     * 作用域的会话标识,比如 session 作用域的会话标识是 sessionId
     */
    String getConversationId();

}

举个例子,我们来实现一个线程级别的bean作用域,同一个线程中同名的bean是同一个实例,不同的线程中的bean是不同的实例。

这里,要求bean在线程中是共享的,所以我们可以通过ThreadLocal来实现,ThreadLocal可以实现线程中数据的共享。

首先,我们在com.projectname.scope包下新建一个ThreadScope类

在ThreadScope类中,我们定义了一个THREAD_SCOPE常量,该常量是在定义bean的时候给scope使用的。

java 复制代码
package com.atguigu.scope;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * 自定义本地线程级别的 bean 作用域
 * 不同的线程中的 bean是不同的实例,同一个线程中同名的 bean是同一个实例
 *
 */
public class ThreadScope implements Scope {

    public static final String THREAD_SCOPE = "thread";

    private ThreadLocal<Map<String, Object>> beanMap = new ThreadLocal() {

        @Override
        protected Object initialValue() {
            return new HashMap<String,Object>();
        }

    };

    /**
     * 返回当前作用域中name对应的bean对象
     * @param name:需要检索的bean对象的名称
     * @param objectFactory:如果name对应的bean对象在当前作用域中没有找到,那么可以调用这个objectFactory来创建这个bean对象
     */
    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Object bean = beanMap.get().get(name);
        if (Objects.isNull(bean)) {
            bean = objectFactory.getObject();
            beanMap.get().put(name, bean);
        }
        return bean;
    }

    /**
     * 将name对应的 bean对象从当前作用域中移除
     */
    @Override
    public Object remove(String name) {
        return this.beanMap.get().remove(name);
    }

    /**
     * 用于注册销毁回调,若想要销毁相应的对象,则由Spring容器注册相应的销毁回调,而由自定义作用域选择是不是要销毁相应的对象
     * bean作用域范围结束的时候调用的方法,用于bean的清理
     */
    @Override
    public void registerDestructionCallback(String name, Runnable callback) {
        System.out.println(name);
    }

    /**
     * 用于解析相应的上下文数据,比如request作用域将返回request中的属性
     */
    @Override
    public Object resolveContextualObject(String key) {
        return null;
    }

    /**
     * 作用域的会话标识,比如session作用域的会话标识是sessionId
     */
    @Override
    public String getConversationId() {
        return Thread.currentThread().getName();
    }
}

然后,我们在com.meimeixia.config包下创建一个配置类,例如MainConfig2,并使用@Scope("thread")注解标注Person对象的作用域为Thread范围,如下所示。

java 复制代码
@Configuration
public class MainConfig2 {

    @Scope("thread")
    @Bean
    public Person person() {
        System.out.println("给容器中添加咱们这个Person对象...");
        return new Person("testThread", 100);
    }
}

接着,我们在IOCTest类中创建一个test04()方法,我们所要做的事情就是在该方法中创建Spring容器,并向Spring容器中注册ThreadScope对象。最后,使用循环创建两个Thread线程,并分别在每个线程中获取两个Person对象,如下所示。

java 复制代码
@Test
public void test04() {
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig2.class);
    // 向容器中注册自定义的 Scope
    applicationContext.getBeanFactory().registerScope(ThreadScope.THREAD_SCOPE, new ThreadScope());

    // 使用容器获取 bean
    for (int i = 0; i < 2; i++) {
        new Thread(() -> {
            System.out.println(Thread.currentThread() + "," + applicationContext.getBean("person"));
            System.out.println(Thread.currentThread() + "," + applicationContext.getBean("person"));
        }).start();
    }
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

5. 懒加载

Spring在启动时,默认会将单实例bean进行实例化,并加载到Spring容器中去。也就是说,单实例bean默认是在Spring容器启动的时候创建对象,并且还会将对象加载到Spring容器中。如果我们需要对某个bean进行延迟加载,那么该如何处理呢?此时,就需要使用到@Lazy注解了。

5.1 @Lazy

java 复制代码
@Configuration
public class MainConfig {

    @Lazy
    @Bean
    public Person person() {
        return new Person("liayun", 20);
    }

}

5.2 总结

  • 使用@Lazy注解标注后,单实例bean对象只是在第一次从Spring容器中获取时被创建,以后每次获取bean对象时,直接返回创建好的对象。
  • 懒加载,也称延时加载,仅针对单实例bean生效。
  • 当多次获取bean的时候并不会重复加载,只是在第一次获取的时候才会加载,这不是延迟加载的特性,而是单实例bean的特性。

6. @Conditional

6.1 概述

@Conditional注解是由Spring Framework提供的一个注解,它位于 org.springframework.context.annotation包内,定义如下。

java 复制代码
package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {

    /**
     * All {@link Condition}s that must {@linkplain Condition#matches match}
     * in order for the component to be registered.
     */
    Class<? extends Condition>[] value();

}

从@Conditional注解的源码来看,@Conditional注解不仅可以添加到类上,也可以添加到方法上。在@Conditional注解中,还存在着一个Condition类型或者其子类型的Class对象数组,Condition是个啥呢?我们点进去看一下。

java 复制代码
package org.springframework.context.annotation;

import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.core.type.AnnotatedTypeMetadata;

public interface Condition {

    /**
     * Determine if the condition matches.
     * @param context the condition context
     * @param metadata metadata of the {@link org.springframework.core.type.AnnotationMetadata class}
     * or {@link org.springframework.core.type.MethodMetadata method} being checked.
     * @return {@code true} if the condition matches and the component can be registered
     * or {@code false} to veto registration.
     */
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);

}

可以看到,它是一个接口。所以,我们使用@Conditional注解时,需要写一个类来实现Spring提供的Condition接口,它会匹配@Conditional所符合的方法,然后我们就可以使用我们在@Conditional注解中定义的类来检查了。

6.2 使用场景

  • 可以作为类级别的注解直接或间接的与@Component 相关联,包括@Configuration类
  • 可以作为元注解,用于自动编写构造性注解
  • 作为方法级别的注解,作用在任何@Bean方法上

6.3 使用方法

需求:如果当前操作系统是Windows操作系统,那么就向Spring容器中注册名称为bill的Person对象;如果当前操作系统是Linux操作系统,那么就向Spring容器中注册名称为linus的Person对象。要想实现这个需求,我们就得要使用@Conditional注解了。

创建了两个实现Condition接口的类,它们分别是 LinuxCondition 和 WindowsCondition

java 复制代码
package com.atguigu.condition;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
* 判断操作系统是否是Linux系统
*
*/
public class LinuxCondition implements Condition {

    /**
    * ConditionContext:判断条件能使用的上下文(环境)
    * AnnotatedTypeMetadata:当前标注了@Conditional注解的注释信息
    */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 判断操作系统是否是Linux系统
        Environment environment = context.getEnvironment();
        String property = environment.getProperty("os.name");
        if (property.contains("linux")) {
            return true;
        }
        return false;
    }

}
java 复制代码
package com.atguigu.condition;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
* 判断操作系统是否是Windows系统
*
*/
public class WindowsCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment environment = context.getEnvironment();
        String property = environment.getProperty("os.name");
        if (property.contains("Windows")) {
            return true;
        }
        return false;
    }

}

配置类

java 复制代码
package com.atguigu.config;

import com.atguigu.bean.Person;
import com.atguigu.condition.LinuxCondition;
import com.atguigu.condition.WindowsCondition;
import org.springframework.context.annotation.*;

@Configuration
public class MainConfig {

    @Conditional({WindowsCondition.class})
    @Bean("bill")
    public Person person01() {
        return new Person("Bill Gates", 62);
    }

    @Conditional({LinuxCondition.class})
    @Bean("linus")
    public Person person02() {
        return new Person("linus", 48);
    }
}

写测试

java 复制代码
@Test
public void testConditional(){
    AnnotationConfigApplicationContext applicationContext1 = new AnnotationConfigApplicationContext(MainConfig.class);

    // 我们现在就来看一下IOC容器中Person这种类型的bean都有哪些
    String[] namesForType = applicationContext1.getBeanNamesForType(Person.class);

    for (String name : namesForType) {
        System.out.println(name);
    }

    // 找到这个Person类型的所有bean
    Map<String, Person> persons = applicationContext1.getBeansOfType(Person.class);
    System.out.println(persons);
    
}

输出:

bill
{bill=Person{name='Bill Gates', age=62}}

7. @Import

  • 我们自己写的类,可以通过 包扫描 + 给组件标注注解(@Controller、@Servcie、@Repository、@Component)的形式将其注册到 IOC 容器中
  • 如果不是我们自己写的类(比如说我们在项目中会经常引入一些第三方的类库),我们需要将这些第三方类库中的类注册到 Spring 容器中,该怎么办呢?此时,我们就可以使用 @Bean 和 @Import 注解将这些类快速的导入Spring容器中。

接下来,我们来一起探讨下如何使用 @Import 注解给容器中快速导入一个组件。

7.1 注册bean的方式

向 Spring 容器中注册 bean 通常有以下几种方式:

  • 包扫描+给组件标注注解(@Controller、@Servcie、@Repository、@Component),但这种方式仅限于我们自己写的类
  • @Bean注解,通常用于导入第三方包中的组件
  • @Import注解,快速向Spring容器中导入一个组件

7.2 @Import注解概述

  • Spring 3.0 之前,创建 bean 可以通过 XML 配置文件与扫描特定包下面的类来将类注入到 Spring IOC 容器内。
  • Spring 3.0 之后提供了 JavaConfig 的方式,也就是将 IOC 容器里面 bean 的元信息以 Java 代码的方式进行描述,然后我们可以通过 @Configuration 与 @Bean 这两个注解配合使用来将原来配置在 XML 文件里面的 bean 通过 Java 代码的方式进行描述。
  • @Import 注解提供了 @Bean 注解的功能,同时还有 XML 配置文件里面标签组织多个分散的 XML 文件的功能,当然在这里是组织多个分散的 @Configuration,因为一个配置类就约等于一个 XML 配置文件。

我们先看一下 @Import 注解的源码,如下所示。

java 复制代码
package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {

    /**
     * {@link Configuration}, {@link ImportSelector}, {@link ImportBeanDefinitionRegistrar}
     * or regular component classes to import.
     */
    Class<?>[] value();

}

从源码里面可以看出 @Import 可以配合 Configuration、ImportSelector 以及 ImportBeanDefinitionRegistrar 来使用,下面的 or 表示也可以把 Import 当成普通的 bean 来使用。

注意:@Import 注解只允许放到类上面,不允许放到方法上。

7.3 使用方式

首先,我们创建一个 Color 类,这个类是一个空类,没有成员变量和方法,假设这是一个第三方类,我们不能在修改这个类的内容

java 复制代码
package com.atguigu.bean;

public class Color {

}

然后,我们在 IOCTest 类中创建一个 testImport() 方法,在其中输出 Spring 容器中所有 bean 定义的名字,来查看是否存在 Color 类对应的 bean 实例,以此来判断 Spring 容器中是否注册有 Color 类对应的 bean 实例。

java 复制代码
@Test
public void testImport(){
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
    String[] definitionNames = applicationContext.getBeanDefinitionNames();
    for (String name : definitionNames) {
        System.out.println(name);
    }
}

可以看到 Spring 容器中并没有 Color 类对应的 bean 实例。

1. class 数组

在 MainConfig 配置类的类名上使用@Import

java 复制代码
package com.atguigu.config;

import com.atguigu.bean.Color;
import com.atguigu.bean.Person;
import com.atguigu.condition.LinuxCondition;
import com.atguigu.condition.WindowsCondition;
import org.springframework.context.annotation.*;

//@ComponentScan("com.atguigu")
@Configuration
@Import(Color.class) // @Import快速地导入组件,id默认是组件的全类名
public class MainConfig {

    @Bean
    public Person person() {
        return new Person("zhangsan", 20);
    }

    @Conditional({WindowsCondition.class})
    @Bean("bill")
    public Person person01() {
        return new Person("Bill Gates", 62);
    }

    @Conditional({LinuxCondition.class})
    @Bean("linus")
    public Person person02() {
        return new Person("linus", 48);
    }
}

重新测试,可以看到,输出结果中打印了com.projectname.bean.Color,说明使用@Import注解快速地导入组件时,容器中就会自动注册这个组件,并且 id 默认是组件的全类名。

同时导入多个类

java 复制代码
@Import({Color.class, Red.class})

2. ImportSelector 接口

接口介绍

ImportSelector 接口是 Spring 中导入外部配置的核心接口,在 Spring Boot 的自动化配置和 @EnableXXX(功能性注解)都有它的存在。我们先来看一下 ImportSelector 接口的源码,如下所示。

java 复制代码
package org.springframework.context.annotation;

import org.springframework.core.type.AnnotationMetadata;

public interface ImportSelector {

    /**
     * Select and return the names of which class(es) should be imported based on
     * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
     */
    String[] selectImports(AnnotationMetadata importingClassMetadata);

}

该接口文档上说的明明白白,其主要作用是收集需要导入的配置类,selectImports() 方法的返回值就是我们向 Spring 容器中导入的类的全类名。如果该接口的实现类同时实现 EnvironmentAware,BeanFactoryAware,BeanClassLoaderAware 或者 ResourceLoaderAware,那么在调用其 selectImports() 方法之前先调用上述接口中对应的方法,如果需要在所有的 @Configuration 处理完再导入时,那么可以实现 DeferredImportSelector 接口。

在 ImportSelector 接口的 selectImports() 方法中,存在一个 AnnotationMetadata 类型的参数,这个参数能够获取到当前标注 @Import 注解的类的所有注解信息,也就是说不仅能获取到 @Import 注解里面的信息,还能获取到其他注解的信息。

使用方法

首先,我们创建一个 MyImportSelector 类实现 ImportSelector 接口

java 复制代码
package com.atguigu.condition;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

/**
 * 自定义逻辑,返回需要导入的组件
 */
public class MyImportSelector implements ImportSelector {

    /**
     * @param importingClassMetadata 当前标注@Import注解的类的所有注解信息,也就是说不仅能获取到@Import注解里面的信息,还能获取到其他注解的信息
     * @return 就是要导入到容器中的组件的全类名
     */
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.atguigu.bean.Blue"};
    }

}

然后,在 MainConfig 配置类的 @Import 注解中,导入 MyImportSelector 类,如下所示。

java 复制代码
@Configuration
@Import({Color.class, Red.class, MyImportSelector.class}) 
public class MainConfig {

}

至于使用 MyImportSelector 类要导入哪些 bean,就需要你在 MyImportSelector 类的 selectImports()方法中进行设置了,只须在 MyImportSelector 类的 selectImports() 方法中返回要导入的类的全类名(包名 + 类名)即可。

3. ImportBeanDefinitionRegistrar 接口

接口介绍

我们先来看看 ImportBeanDefinitionRegistrar 是个什么鬼,点击进入 ImportBeanDefinitionRegistrar 源码

java 复制代码
package org.springframework.context.annotation;

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.core.type.AnnotationMetadata;

public interface ImportBeanDefinitionRegistrar {

    /**
     * Register bean definitions as necessary based on the given annotation metadata of
     * the importing {@code @Configuration} class.
     * <p>Note that {@link BeanDefinitionRegistryPostProcessor} types may <em>not</em> be
     * registered here, due to lifecycle constraints related to {@code @Configuration}
     * class processing.
     * @param importingClassMetadata annotation metadata of the importing class
     * @param registry current bean definition registry
     */
    public void registerBeanDefinitions(
          AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);

}

由源码可以看出,ImportBeanDefinitionRegistrar 本质上是一个接口。在 ImportBeanDefinitionRegistrar 接口中,有一个 registerBeanDefinitions() 方法,通过该方法,我们可以向 Spring 容器中注册 bean 实例。

Spring 官方在动态注册 bean 时,大部分套路其实是使用 ImportBeanDefinitionRegistrar 接口。

所有实现了该接口的类都会被 ConfigurationClassPostProcessor 处理,ConfigurationClassPostProcessor 实现了 BeanFactoryPostProcessor 接口,所以 ImportBeanDefinitionRegistrar 中动态注册的 bean 是优先于依赖其的 bean 初始化的,也能被 aop、validator 等机制处理。

ImportBeanDefinitionRegistrar 需要配合 @Configuration 和 @Import 这俩注解

@Configuration 注解定义 Java 格式的 Spring 配置文件

@Import 注解导入实现了 ImportBeanDefinitionRegistrar 接口的类。

使用方法

创建一个 MyImportBeanDefinitionRegistrar 类,去实现 ImportBeanDefinitionRegistrar 接口,在 registerBeanDefinitions() 方法里面自定义一些逻辑

java 复制代码
package com.atguigu.condition;

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

/**
 * @author longlong 
 */
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    /**
     * 手动注册所有需要添加到容器中的 bean
     * 
     * @param importingClassMetadata 当前类的注解信息
     * @param registry BeanDefinition注册类
     */
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        
        boolean definition = registry.containsBeanDefinition("com.atguigu.bean.Red");
        boolean definition2 = registry.containsBeanDefinition("com.atguigu.bean.Blue");
        if (definition && definition2) {
            // 指定 bean的定义信息,包括 bean的类型、作用域等等
            // RootBeanDefinition是 BeanDefinition接口的一个实现类
            // bean的定义信息
            RootBeanDefinition beanDefinition = new RootBeanDefinition(RainBow.class); 
            // 注册一个bean,并且指定 bean的名称
            registry.registerBeanDefinition("rainBow", beanDefinition);
        }
        
    }

}

然后,我们在 MainConfig 配置类上的 @Import 注解中,添加 MyImportBeanDefinitionRegistrar 类

java 复制代码
@Configuration
@Import({Color.class, Red.class, MyImportSelector.class, MyImportBeanDefinitionRegistrar.class}) 
public class MainConfig {

}

接着,创建一个 RainBow 类,作为测试 ImportBeanDefinitionRegistrar 接口的 bean 来使用

java 复制代码
package com.atguigu.bean;

public class RainBow {
}

写测试

java 复制代码
@Test
public void testImport(){
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
    String[] definitionNames = applicationContext.getBeanDefinitionNames();
    for (String name : definitionNames) {
        System.out.println(name);
    }
}

8. FactoryBean

8.1 概述

一般情况下,Spring 是通过反射机制利用 bean 的 class 属性指定实现类来实例化 bean 的。在某些情况下,实例化 bean 过程比较复杂,如果按照传统的方式,那么则需要在标签中提供大量的配置信息,配置方式的灵活性是受限的,这时采用编码的方式可以得到一个更加简单的方案。Spring 为此提供了一个 org.springframework.bean.factory.FactoryBean 的工厂类接口,用户可以通过实现该接口定制实例化 bean 的逻辑。

FactoryBean 接口对于 Spring 框架来说占有非常重要的地位,Spring 自身就提供了 70 多个 FactoryBean 接口的实现。它们隐藏了实例化一些复杂 bean 的细节,给上层应用带来了便利。从 Spring 3.0 开始,FactoryBean 开始支持泛型,即接口声明改为 FactoryBean 的形式。

在Spring 4.3.12.RELEASE这个版本中,FactoryBean接口的定义如下所示。

java 复制代码
package org.springframework.beans.factory;

public interface FactoryBean<T> {
    T getObject() throws Exception;

    Class<?> getObjectType();

    boolean isSingleton();
}
  • T getObject():返回由FactoryBean创建的bean实例,如果isSingleton()返回true,那么该实例会放到Spring容器中单实例缓存池中
  • boolean isSingleton():返回由FactoryBean创建的bean实例的作用域是singleton还是prototype
  • Class getObjectType():返回FactoryBean创建的bean实例的类型

注意:当配置文件中标签的 class 属性配置的实现类是 FactoryBean 时,通过 getBean()方法返回的不是 FactoryBean 本身,而是 FactoryBean#getObject() 方法所返回的对象,相当于 FactoryBean#getObject()代理了 getBean() 方法。

8.2 使用方法

首先,创建一个ColorFactoryBean类,它得实现FactoryBean接口,如下所示。

java 复制代码
package com.atguigu.bean;

import org.springframework.beans.factory.FactoryBean;

/**
 * 创建一个Spring定义的FactoryBean
 * T(泛型):指定我们要创建什么类型的对象
 * @author longlong
 */
public class ColorFactoryBean implements FactoryBean<Color> {

    /**
     * @return 返回一个Color对象,这个对象会添加到容器中
     * @throws Exception
     */
    @Override
    public Color getObject() throws Exception {
       System.out.println("ColorFactoryBean...getObject...");
       return new Color();
    }

    /**
     * 
     * @return 返回这个对象的类型
     */
    @Override
    public Class<?> getObjectType() {
       return Color.class;
    }


    /**
     * 是否为单例
     * @return true-单实例 false-多实例
     */
    @Override
    public boolean isSingleton() {
       return false;
    }

}

然后,我们在MainConfig配置类中加入ColorFactoryBean的声明,如下所示。

java 复制代码
@Configuration
@Import({Color.class, Red.class, MyImportSelector.class, MyImportBeanDefinitionRegistrar.class}) // @Import快速地导入组件,id默认是组件的全类名
public class MainConfig {

    @Bean
    public ColorFactoryBean colorFactoryBean() {
        return new ColorFactoryBean();
    }

}

注意:我在这里使用@Bean注解向Spring容器中注册的是ColorFactoryBean对象。

那现在我们就来看看Spring容器中到底都有哪些bean,发现 输出了一个 colorFactoryBean,然后根据这个名字去获取实例

java 复制代码
@Test
public void testFactory(){
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
    String[] definitionNames = applicationContext.getBeanDefinitionNames();
    for (String name : definitionNames) {
        System.out.println(name);
    }
    Object colorFactoryBean = applicationContext.getBean("colorFactoryBean");
    System.out.println(colorFactoryBean.getClass());
}

可以看到,虽然我在代码中使用 @Bean 注解注入的是 ColorFactoryBean 对象,但是实际上从 Spring 容器中获取到的 bean 对象却是调用 ColorFactoryBean 类中的 getObject() 方法获取到的 Color 对象。

在 ColorFactoryBean 类中,如果把 Color 对象设置为单实例 bean,那么多次获取的对象为同一对象

如果把 Color 对象修改成多实例 bean ,那么每次获取对象都会创建新的实例

8.3 获取 FactoryBean 对象

之前,我们使用 @Bean 注解向 Spring 容器中注册的是 ColorFactoryBean,获取出来的却是 Color 对象。那么,小伙伴们可能会问了,我就想获取 ColorFactoryBean 实例,那么该怎么办呢?

其实,这也很简单,只需要在获取工厂 Bean 本身时,在 id 前面加上 & 符号即可,例如 &colorFactoryBean。

Object colorFactoryBean = applicationContext.getBean("&colorFactoryBean");

为什么在 id 前面加上 & 符号就会获取到 ColorFactoryBean 实例对象呢?

答案在 BeanFactory 接口中,查看其源码。

java 复制代码
package org.springframework.beans.factory;

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

public interface BeanFactory {

    String FACTORY_BEAN_PREFIX = "&";

}

BeanFactory 接口中定义了一个 & 前缀,只要我们使用 bean 的 id 来从 Spring 容器中获取 bean 时,Spring 就会知道我们是在获取 FactoryBean 本身。

9. 生命周期

通常意义上讲的 bean 的生命周期,指的是 bean 从创建到初始化,经过一系列的流程,最终销毁的过程。只不过,在 Spring 中,bean 的生命周期是由 Spring 容器来管理的。在 Spring 中,我们可以自己来指定 bean 的初始化和销毁的方法。我们指定了 bean 的初始化和销毁方法之后,当容器在 bean 进行到当前生命周期的阶段时,会自动调用我们自定义的初始化和销毁方法。

9.1 XML方式

在标签中指定bean的初始化和销毁方法

xml 复制代码
<bean id="person" class="com.meimeixia.bean.Person" init-method="init" destroy-method="destroy">
    <property name="age" value="18"></property>
    <property name="name" value="liayun"></property>
</bean>

在我们自己写的Person类中,需要存在init()方法和destroy()方法。而且Spring中还规定,这里的init()方法和destroy()方法必须是无参方法,但可以抛出异常。

9.2 @Bean 注解方式

通过 @Bean 注解指定初始化和销毁方法。

在 MainConfig 配置类的 @Bean 注解中指定 initMethod 属性和 destroyMethod 属性

java 复制代码
@Configuration
public class MainConfig {

    @Bean(initMethod = "",destroyMethod = "")
    public Person person() {
        return new Person("zhangsan", 20);
    }

}

在Spring容器中,先是调用了Car类的构造方法来创建Car对象,接下来便是调用了Car对象的init()方法来进行初始化。

bean的销毁方法是在容器关闭的时候被调用的。

java 复制代码
@Test
public void test() {
        // 创建IOC容器
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig.class);
        System.out.println("容器创建完成");
        
        // 关闭容器
        applicationContext.close();
}

使用场景

数据源的管理

在配置数据源时,在初始化的时候,会对很多的数据源的属性进行赋值操作;

在销毁的时候,我们需要对数据源的连接等信息进行关闭和清理。

这个时候,我们就可以在自定义的初始化和销毁方法中来做这些事情了!

调用时机

初始化方法和销毁方法是在什么时候被调用的呢?

  • 初始化方法:对象创建完成,如果对象中存在一些属性,并且这些属性也都赋好值之后,那么就会调用 bean 的初始化方法。
    • 对于单实例 bean 来说,在 Spring 容器创建完成后,Spring 容器会自动调用 bean 的初始化方法;
    • 对于多实例 bean 来说,在每次获取 bean 对象的时候,调用 bean 的初始化方法。
  • 销毁方法
    • 对于单实例 bean 来说,在容器关闭的时候,会调用 bean 的销毁方法;
    • 对于多实例 bean 来说,Spring 容器不会管理这个 bean,也就不会自动调用这个 bean 的销毁方法了。不过,小伙伴们可以手动调用多实例 bean 的销毁方法。

9.3 InitializingBean 接口

接口概述

Spring 中提供了一个 InitializingBean 接口,该接口为 bean 提供了属性初始化后的处理方法,它只包括 afterPropertiesSet 方法,凡是继承该接口的类,在 bean 的属性初始化后都会执行该方法。

InitializingBean 接口的源码如下所示

java 复制代码
package org.springframework.beans.factory;

public interface InitializingBean {
    void afterPropertiesSet() throws Exception;
}

根据 InitializingBean 接口中提供的 afterPropertiesSet() 方法的名字不难推断出,afterPropertiesSet() 方法是在属性赋好值之后调用的。

调用时机

我们定位到 Spring 中的 org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory 这个类里面的 invokeInitMethods() 方法中,来查看 Spring 加载 bean 的方法。

我们来到 AbstractAutowireCapableBeanFactory 类中的 invokeInitMethods() 方法处,如下所示。

java 复制代码
package org.springframework.beans.factory.support;

public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory implements AutowireCapableBeanFactory {
    
    
    protected void invokeInitMethods(String beanName, final Object bean, RootBeanDefinition mbd) throws Throwable {
        // 判断 bean 是否实现了 InitializingBean 接口
        // 如果实现了 就调用 bean 的 afterPropertiesSet 方法
        boolean isInitializingBean = bean instanceof InitializingBean;
        if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Invoking afterPropertiesSet() on bean with name '" + beanName + "'");
            }
    
            if (System.getSecurityManager() != null) {
                try {
                    AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
                        public Object run() throws Exception {
                            // 调用 bean 的 afterPropertiesSet 方法
                            ((InitializingBean)bean).afterPropertiesSet();
                            return null;
                        }
                    }, this.getAccessControlContext());
                } catch (PrivilegedActionException var6) {
                    throw var6.getException();
                }
            } else {
                // 调用 bean 的 afterPropertiesSet 方法
                ((InitializingBean)bean).afterPropertiesSet();
            }
        }
    
        if (mbd != null) {
            String initMethodName = mbd.getInitMethodName();
            if (initMethodName != null && (!isInitializingBean || !"afterPropertiesSet".equals(initMethodName)) && !mbd.isExternallyManagedInitMethod(initMethodName)) {
                // 通过反射调用 init-Method
                this.invokeCustomInitMethod(beanName, bean, mbd);
            }
        }
    
    }
    
    
}

分析上述代码后,我们可以初步得出如下信息:

  • Spring 为 bean 提供了两种初始化的方式
    • 实现 InitializingBean 接口(也就是要实现该接口中的 afterPropertiesSet 方法)
    • 在配置文件或 @Bean 注解中通过 init-method 来指定
    • 两种方式可以同时使用,同时使用先调用 afterPropertiesSet 方法,后执行 init-method 指定的方法。
  • 实现 InitializingBean 接口是直接调用 afterPropertiesSet() 方法,与通过反射调用 init-method 指定的方法相比,效率相对来说要高点。但是 init-method 方式消除了对 Spring 的依赖。
  • 如果调用 afterPropertiesSet 方法时出错,那么就不会调用 init-method 指定的方法了。

9.4 DisposableBean 接口

接口概述

实现 org.springframework.beans.factory.DisposableBean 接口的 bean 在销毁前,Spring 将会调用 DisposableBean 接口的 destroy() 方法。也就是说我们可以实现 DisposableBean 这个接口来定义咱们这个销毁的逻辑。

我们先来看下 DisposableBean 接口的源码

java 复制代码
package org.springframework.beans.factory;

public interface DisposableBean {
    void destroy() throws Exception;
}

可以看到,在 DisposableBean 接口中只定义了一个 destroy() 方法。

在 bean 生命周期结束前调用 destroy()方法做一些收尾工作,亦可以使用 destroy-method。

  • DisposableBean 接口与 Spring 耦合高,使用类型强转. 方法名 (),效率高;
  • destroy-method 方法与 Spring 耦合低,使用反射,效率相对来说较低。
  • 两个方法同时使用,先调用 DisposableBean 接口中的方法,再调用 destroy-method 方法

注意事项

多实例 bean 的生命周期不归 Spring 容器来管理,这里的 DisposableBean 接口中的方法是由 Spring 容器来调用的,所以如果一个多实例 bean 实现了 DisposableBean 接口是没有啥意义的,因为相应的方法根本不会被调用,当然了,在 XML 配置文件中指定了 destroy 方法,也是没有任何意义的。所以,在多实例 bean 情况下,Spring 是不会自动调用 bean 的销毁方法的。

9.5 @PostConstruct 注解

注解概述

@PostConstruct 注解好多人以为是 Spring 提供的,其实它是 Java 自己的注解,是 JSR-250 规范里面定义的一个注解。我们来看下 @PostConstruct 注解的源码

java 复制代码
package javax.annotation;

import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

@Documented
@Retention (RUNTIME)
@Target(METHOD)
public @interface PostConstruct {
}

从源码可以看出,@PostConstruct 注解是 Java 中的注解,并不是 Spring 提供的注解。

调用顺序

通常我们是会在 Spring 框架中使用到 @PostConstruct 注解的,该注解的方法在整个 bean 初始化中的执行顺序如下

  1. Constructor(构造方法)
  2. @Autowired(依赖注入)
  3. @PostConstruct(注释的方法)
  4. 实现了 InitializingBean 接口的 afterPropertiesSet() 方法。
  5. 最后,如果 bean 在配置中指定了 init() 方法,Spring 会调用该方法。

使用方法

java 复制代码
public class Car implements InitializingBean, DisposableBean {

    public Car() {
        System.out.println("car constructor...");
    }

    @PostConstruct
    public void init1(){
        System.out.println("PostConstruct...");
    }

    @PreDestroy
    public void destory1(){
        System.out.println("PreDestroy...");
    }
}

9.6 @PreDestroy 注解

注解概述

@PreDestroy注解同样是Java提供的,它也是JSR-250规范里面定义的一个注解。看下它的源码

java 复制代码
package javax.annotation;

import java.lang.annotation.*;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.*;

@Documented
@Retention (RUNTIME)
@Target(METHOD)
public @interface PreDestroy {
}

调用顺序

在 Spring 容器关闭时,销毁 bean 的顺序如下:

  1. 首先,Spring 会调用被 @PreDestroy 注解修饰的方法。
  2. 接下来,Spring 会调用实现了 DisposableBean 接口的 destroy() 方法。
  3. 最后,如果 bean 在配置中指定了 destroy 方法,Spring 会调用该方法。

使用方法

java 复制代码
public class Car implements InitializingBean, DisposableBean {

    public Car() {
        System.out.println("car constructor...");
    }

    @PostConstruct
    public void init1(){
        System.out.println("PostConstruct...");
    }

    @PreDestroy
    public void destory1(){
        System.out.println("PreDestroy...");
    }
}

9.7 BeanPostProcessor

1. 后置处理器概述

看下 BeanPostProcessor 的源码

java 复制代码
package org.springframework.beans.factory.config;

import org.springframework.beans.BeansException;

public interface BeanPostProcessor {
    Object postProcessBeforeInitialization(Object var1, String var2) throws BeansException;

    Object postProcessAfterInitialization(Object var1, String var2) throws BeansException;
}

从源码可以看出,BeanPostProcessor 是一个接口,其中有两个方法,即 postProcessBeforeInitialization 和 postProcessAfterInitialization 这两个方法,这两个方法分别是在 Spring 容器中的 bean 初始化前后执行,所以 Spring 容器中的每一个 bean 对象初始化前后,都会执行 BeanPostProcessor 接口的实现类中的这两个方法。

也就是说,postProcessBeforeInitialization 方法会在 bean 实例化和属性设置之后,自定义初始化方法之前被调用,而 postProcessAfterInitialization 方法会在自定义初始化方法之后被调用。当容器中存在多个 BeanPostProcessor 的实现类时,会按照它们在容器中注册的顺序执行。对于自定义的 BeanPostProcessor 实现类,还可以让其实现 Ordered 接口自定义排序。

因此我们可以在每个 bean 对象初始化前后,加上自己的逻辑。实现方式是自定义一个 BeanPostProcessor 接口的实现类,例如 MyBeanPostProcessor,然后在该类的 postProcessBeforeInitialization 和 postProcessAfterInitialization 这俩方法中写上自己的逻辑。

2. 使用方法

创建一个MyBeanPostProcessor类,实现BeanPostProcessor接口,如下所示。

java 复制代码
package com.atguigu.bean;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.stereotype.Component;

/**
 * 后置处理器,在初始化前后进行处理工作
 * 
 * @author longlong
 */
@Component
public class MyBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
       System.out.println("postProcessBeforeInitialization..." + beanName + "=>" + bean);
       return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
       System.out.println("postProcessAfterInitialization..." + beanName + "=>" + bean);
       return bean;
    }

}

测试一下

java 复制代码
@Test
public void testInitial(){
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig1.class);
    String[] definitionNames = applicationContext.getBeanDefinitionNames();
    for (String name : definitionNames) {
        System.out.println(name);
    }
    applicationContext.close();
}

3. 执行顺序

  1. Constructor(构造方法)
  2. @Autowired(依赖注入)
  3. 后置处理器 初始化前方法 postProcessBeforeInitialization
  4. @PostConstruct(注释的方法)
  5. 实现了 InitializingBean 接口的 afterPropertiesSet() 方法。
  6. bean 在配置中指定的 init() 方法
  7. 后置处理器 初始化后方法 postProcessAfterInitialization

4. 多个后置处理器顺序

Spring 容器中可以存在多个后置处理器,当容器初始化 bean 时,会按照这些后置处理器的执行顺序依次调用它们的 postProcessBeforeInitialization() 和 postProcessAfterInitialization() 方法。

实现了 Ordered 接口的后置处理器可以通过 getOrder() 方法返回一个整数值,表示其执行顺序的优先级,数值越小,优先级越高,即越先执行。如果不实现 Ordered 接口,默认的执行顺序为 Integer.MAX_VALUE。

java 复制代码
package com.atguigu.bean;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * 后置处理器,在初始化前后进行处理工作
 *
 * @author longlong
 */
@Component
public class MyBeanPostProcessor implements BeanPostProcessor, Ordered {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
       System.out.println("My "+"postProcessBeforeInitialization..." + beanName + "=>" + bean);
       return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
       System.out.println("My "+"postProcessAfterInitialization..." + beanName + "=>" + bean);
       return bean;
    }


    @Override
    public int getOrder() {
       return 3;
    }
}

5. Spring 提供的实现类

后置处理器可用于 bean 对象初始化前后进行逻辑增强。Spring 提供了 BeanPostProcessor 接口的很多实现类

  • ApplicationContextAwareProcessor 向组件中注入IOC容器
  • AnnotationAwareAspectJAutoProxyCreator 用于 Spring AOP 的动态代理
  • BeanValidationPostProcessor 用来为bean进行校验操作
  • InitDestroyAnnotationBeanPostProcessor 处理 @PostConstruct 注解和 @PreDestroy 注解
  • AutowiredAnnotationBeanPostProcessor 用于 @Autowired 注解的实现
ApplicationContextAwareProcessor

org.springframework.context.support.ApplicationContextAwareProcessor 是 BeanPostProcessor 接口的一个实现类,这个类的作用是可以向组件中注入IOC容器

大致的源码如下所示

package org.springframework.context.support;

java 复制代码
class ApplicationContextAwareProcessor implements BeanPostProcessor {

    private final ConfigurableApplicationContext applicationContext;

    private final StringValueResolver embeddedValueResolver;


    /**
     * Create a new ApplicationContextAwareProcessor for the given context.
     */
    public ApplicationContextAwareProcessor(ConfigurableApplicationContext applicationContext) {
       this.applicationContext = applicationContext;
       this.embeddedValueResolver = new EmbeddedValueResolver(applicationContext.getBeanFactory());
    }


    @Override
    public Object postProcessBeforeInitialization(final Object bean, String beanName) throws BeansException {
       AccessControlContext acc = null;

       if (System.getSecurityManager() != null &&
             (bean instanceof EnvironmentAware || bean instanceof EmbeddedValueResolverAware ||
                   bean instanceof ResourceLoaderAware || bean instanceof ApplicationEventPublisherAware ||
                   bean instanceof MessageSourceAware || bean instanceof ApplicationContextAware)) {
          acc = this.applicationContext.getBeanFactory().getAccessControlContext();
       }

       if (acc != null) {
          AccessController.doPrivileged(new PrivilegedAction<Object>() {
             @Override
             public Object run() {
                invokeAwareInterfaces(bean);
                return null;
             }
          }, acc);
       }
       else {
          invokeAwareInterfaces(bean);
       }

       return bean;
    }

    private void invokeAwareInterfaces(Object bean) {
       if (bean instanceof Aware) {
          if (bean instanceof EnvironmentAware) {
             ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());
          }
          if (bean instanceof EmbeddedValueResolverAware) {
             ((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver);
          }
          if (bean instanceof ResourceLoaderAware) {
             ((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext);
          }
          if (bean instanceof ApplicationEventPublisherAware) {
             ((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext);
          }
          if (bean instanceof MessageSourceAware) {
             ((MessageSourceAware) bean).setMessageSource(this.applicationContext);
          }
          if (bean instanceof ApplicationContextAware) {
             ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
          }
       }
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
       return bean;
    }

}

要想使用ApplicationContextAwareProcessor类向组件中注入IOC容器,我们就不得不提Spring中的另一个接口了,即ApplicationContextAware。如果需要向组件中注入IOC容器,那么可以让组件实现ApplicationContextAware接口。

例如,我们创建一个 Dog 类,使其实现 ApplicationContextAware 接口,此时,我们需要实现 ApplicationContextAware 接口中的 setApplicationContext()方法,在 setApplicationContext() 方法中有一个 ApplicationContext 类型的参数,这个就是 IOC 容器对象,我们可以在 Dog 类中定义一个 ApplicationContext 类型的成员变量,然后在 setApplicationContext() 方法中为这个成员变量赋值,此时就可以在 Dog 类中的其他方法中使用 ApplicationContext 对象了

java 复制代码
package com.atguigu.bean;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class Cat implements ApplicationContextAware {

    private ApplicationContext applicationContext;

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

用于 Spring AOP 的动态代理

我们都知道 spring AOP 的实现原理是动态代理,最终放入容器的是代理类的对象,而不是 bean 本身的对象,那么 Spring 是什么时候做到这一步的呢?就是在 AnnotationAwareAspectJAutoProxyCreator 后置处理器的 postProcessAfterInitialization 方法中,即 bean 对象初始化完成之后,后置处理器会判断该 bean 是否注册了切面,若是,则生成代理对象注入到容器中。这一部分的关键代码是在哪儿呢?我们定位到 AbstractAutoProxyCreator 抽象类中的 postProcessAfterInitialization 方法处便能看到了,如下所示。

java 复制代码
/**
 * 在 bean 初始化后进行处理的方法。
 * 
 * @param bean 初始化后的 bean 实例
 * @param beanName bean 的名称
 * @return 处理后的 bean 实例
 * @throws BeansException 如果处理过程中发生异常
 */
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    // 如果 bean 不为 null
    if (bean != null) {
        // 获取 bean 的缓存键
        Object cacheKey = this.getCacheKey(bean.getClass(), beanName);
        // 如果缓存中不包含这个 bean 的早期代理引用
        if (!this.earlyProxyReferences.contains(cacheKey)) {
            // 对 bean 进行包装处理,如果需要的话
            return this.wrapIfNecessary(bean, beanName, cacheKey);
        }
    }

    // 返回处理后的 bean 实例
    return bean;
}
BeanValidationPostProcessor

org.springframework.validation.beanvalidation.BeanValidationPostProcessor类主要是用来为bean进行校验操作的,当我们创建bean,并为bean赋值后,我们可以通过BeanValidationPostProcessor类为bean进行校验操作。

BeanValidationPostProcessor类的源码

java 复制代码
package org.springframework.validation.beanvalidation;

import java.util.Iterator;
import java.util.Set;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanPostProcessor;

public class BeanValidationPostProcessor implements BeanPostProcessor, InitializingBean {

    private Validator validator;

    private boolean afterInitialization = false;


    /**
     * Set the JSR-303 Validator to delegate to for validating beans.
     * <p>Default is the default ValidatorFactory's default Validator.
     */
    public void setValidator(Validator validator) {
       this.validator = validator;
    }

    /**
     * Set the JSR-303 ValidatorFactory to delegate to for validating beans,
     * using its default Validator.
     * <p>Default is the default ValidatorFactory's default Validator.
     * @see javax.validation.ValidatorFactory#getValidator()
     */
    public void setValidatorFactory(ValidatorFactory validatorFactory) {
       this.validator = validatorFactory.getValidator();
    }

    /**
     * Choose whether to perform validation after bean initialization
     * (i.e. after init methods) instead of before (which is the default).
     * <p>Default is "false" (before initialization). Switch this to "true"
     * (after initialization) if you would like to give init methods a chance
     * to populate constrained fields before they get validated.
     */
    public void setAfterInitialization(boolean afterInitialization) {
       this.afterInitialization = afterInitialization;
    }

    @Override
    public void afterPropertiesSet() {
       if (this.validator == null) {
          this.validator = Validation.buildDefaultValidatorFactory().getValidator();
       }
    }


    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
       if (!this.afterInitialization) {
          doValidate(bean);
       }
       return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
       if (this.afterInitialization) {
          doValidate(bean);
       }
       return bean;
    }


    /**
     * Perform validation of the given bean.
     * @param bean the bean instance to validate
     * @see javax.validation.Validator#validate
     */
    protected void doValidate(Object bean) {
       Set<ConstraintViolation<Object>> result = this.validator.validate(bean);
       if (!result.isEmpty()) {
          StringBuilder sb = new StringBuilder("Bean state is invalid: ");
          for (Iterator<ConstraintViolation<Object>> it = result.iterator(); it.hasNext();) {
             ConstraintViolation<Object> violation = it.next();
             sb.append(violation.getPropertyPath()).append(" - ").append(violation.getMessage());
             if (it.hasNext()) {
                sb.append("; ");
             }
          }
          throw new BeanInitializationException(sb.toString());
       }
    }

}

在 postProcessBeforeInitialization()方法和 postProcessAfterInitialization() 方法都会根据 afterInitialization 这个布尔类型的成员变量的值来判断是否执行校验操作。

  • 如果 afterInitialization 的值为 false,则在 postProcessBeforeInitialization()方法中调用 doValidate() 方法对 bean 进行校验;
  • 如果 afterInitialization 的值为 true,则在 postProcessAfterInitialization()方法中调用 doValidate() 方法对 bean 进行校验。
InitDestroyAnnotationBeanPostProcessor

org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor 类主要用来处理 @PostConstruct 注解和 @PreDestroy 注解

源码分析

java 复制代码
package org.springframework.beans.factory.annotation;

@SuppressWarnings("serial")
public class InitDestroyAnnotationBeanPostProcessor
       implements DestructionAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor, PriorityOrdered, Serializable {


    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
       LifecycleMetadata metadata = findLifecycleMetadata(bean.getClass());
       try {
          metadata.invokeInitMethods(bean, beanName);
       }
       catch (InvocationTargetException ex) {
          throw new BeanCreationException(beanName, "Invocation of init method failed", ex.getTargetException());
       }
       catch (Throwable ex) {
          throw new BeanCreationException(beanName, "Failed to invoke init method", ex);
       }
       return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
       return bean;
    }

}
AutowiredAnnotationBeanPostProcessor

org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor类主要是用于处理标注了@Autowired注解的变量或方法。

AutowiredAnnotationBeanPostProcessor 是 Spring 框架中的一个核心类,其主要作用是处理 @Autowired 注解,实现自动装配(autowiring)的功能。

具体来说,AutowiredAnnotationBeanPostProcessor 主要完成以下几个任务:

  1. 处理 @Autowired 注解:扫描容器中的 bean,找到标注了 @Autowired 注解的字段、构造方法、setter 方法等,并尝试自动装配这些标注了 @Autowired 注解的元素。
  2. 解析依赖:根据 @Autowired 注解指定的依赖查找策略(例如 byType、byName 等),自动解析和注入依赖。
  3. 处理 @Value 注解:除了处理 @Autowired 注解外,AutowiredAnnotationBeanPostProcessor 还处理 @Value 注解,用于从外部配置文件中读取属性值并注入到 bean 中。
  4. 实现 Aware 接口:AutowiredAnnotationBeanPostProcessor 本身也是一个 BeanPostProcessor,因此可以在 bean 初始化前后执行一些额外的处理工作,例如对 bean 进行校验、增强、代理等。

6. 底层原理

populateBean(beanName, mbd, instanceWrapper); // 给bean进行属性赋值
initializeBean(beanName, exposedObject, mbd)
{
        applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
        invokeInitMethods(beanName, wrappedBean, mbd); // 执行自定义初始化
        applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

在 Spring 中,调用 initializeBean()方法之前,调用了 populateBean() 方法为 bean 的属性赋值,为 bean 的属性赋好值之后,再调用 initializeBean() 方法进行初始化。

在 initializeBean()中,调用自定义的初始化方法(即 invokeInitMethods())之前,调用了 applyBeanPostProcessorsBeforeInitialization()方法,而在调用自定义的初始化方法之后,又调用了 applyBeanPostProcessorsAfterInitialization() 方法。至此,整个 bean 的初始化过程就这样结束了。

10. 属性赋值

10.1 直接注入

@Value 注解

java 复制代码
package org.springframework.beans.factory.annotation;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Value {

    /**
     * The actual value expression: e.g. "#{systemProperties.myProp}".
     */
    String value();

}

从@Value注解的源码中我们可以看出,@Value注解可以标注在字段、方法、参数以及注解上,而且在程序运行期间生效。

使用方法

java 复制代码
package com.atguigu.bean;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

@Component
public class Book {

    /**
     * 注入普通字符串
     */
    @Value("zhangsan")
    private String name;

    /**
     * 注入操作系统属性
     */
    @Value("#{systemProperties['os.name']}")
    private String systemPropertiesName;

    /**
     * 注入SpEL表达式结果
     */
    @Value("#{ T(java.lang.Math).random() * 100.0 }")
    private double randomNumber;

    /**
     * 注入其他bean中属性的值,即注入person对象的name属性中的值
     */
    @Value("#{person.name}")
    private String username;

    /**
     * 注入文件资源
     */
    @Value("classpath:/config.properties")
    public Resource resourceFile;

    /**
     * 注入URL资源
     */
    @Value("http://www.baidu.com")
    private Resource url;
    
}

10.2 配置文件

@PropertySource

@PropertySource注解是Spring 3.1开始引入的配置类注解。通过@PropertySource注解可以将properties配置文件中的key/value存储到Spring的Environment中,Environment接口提供了方法去读取配置文件中的值,参数是properties配置文件中定义的key值。当然了,也可以使用@Value注解用${}占位符为bean的属性注入值。

@PropertySource注解

java 复制代码
package org.springframework.context.annotation;

import org.springframework.core.io.support.PropertySourceFactory;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(PropertySources.class)
public @interface PropertySource {

    /**
     * Indicate the name of this property source. If omitted, a name will
     * be generated based on the description of the underlying resource.
     * @see org.springframework.core.env.PropertySource#getName()
     * @see org.springframework.core.io.Resource#getDescription()
     */
    String name() default "";

    /**
     * Indicate the resource location(s) of the properties file to be loaded.
     * For example, {@code "classpath:/com/myco/app.properties"} or
     * {@code "file:/path/to/file"}.
     * <p>Resource location wildcards (e.g. *&#42;/*.properties) are not permitted;
     * each location must evaluate to exactly one {@code .properties} resource.
     * <p>${...} placeholders will be resolved against any/all property sources already
     * registered with the {@code Environment}. See {@linkplain PropertySource above}
     * for examples.
     * <p>Each location will be added to the enclosing {@code Environment} as its own
     * property source, and in the order declared.
     */
    String[] value();

    /**
     * Indicate if failure to find the a {@link #value() property resource} should be
     * ignored.
     * <p>{@code true} is appropriate if the properties file is completely optional.
     * Default is {@code false}.
     * @since 4.0
     */
    boolean ignoreResourceNotFound() default false;

    /**
     * A specific character encoding for the given resources, e.g. "UTF-8".
     * @since 4.3
     */
    String encoding() default "";

    /**
     * Specify a custom {@link PropertySourceFactory}, if any.
     * <p>By default, a default factory for standard resource files will be used.
     * @since 4.3
     * @see org.springframework.core.io.support.DefaultPropertySourceFactory
     * @see org.springframework.core.io.support.ResourcePropertySource
     */
    Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;

}
  • 从 @PropertySource 的源码中可以看出,我们可以通过 @PropertySource 注解指定多个 properties 文件,使用的形式如下所示。
    • @PropertySource(value={"classpath:/person.properties", "classpath:/car.properties"})
  • 细心的读者可以看到,在 @PropertySource 注解的上面标注了如下的注解信息。
    • @Repeatable(PropertySources.class)
    • 可以使用 @PropertySources 注解来指定 properties 配置文件。

@PropertySources 注解

java 复制代码
package org.springframework.context.annotation;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PropertySources {

    PropertySource[] value();

}

使用方法

java 复制代码
@PropertySources(value={
    @PropertySource(value={"classpath:/person.properties"}),
    @PropertySource(value={"classpath:/car.properties"}),
})

示例

在项目的src/main/resources目录下新建一个属性文件,例如book.properties

book.nickName=zhangsan

从配置文件中取值并注入

java 复制代码
@Component
public class Book {
    
    @Value("${book.nickName}")
    private String nickName;
    
}

在配置类上使用 @PropertySource 注解读取外部配置文件

java 复制代码
@ComponentScan("com.atguigu.bean")
@PropertySource(value={"classpath:/book.properties"})
@Configuration
public class MainConfig3 {
    
}

#{···}

  • #{···}:用于执行SpEl表达式,并将内容赋值给属性
java 复制代码
// SpEL:调用字符串Hello World的concat方法
@Value("#{'Hello World'.concat('!')}")
private String helloWorld;

// SpEL:调用字符串的getBytes方法,然后再调用其length属性
@Value("#{'Hello World'.bytes.length}")
private String helloWorldBytes;

${···}

  • ${···}:主要用于加载外部属性文件中的值

{}里面的内容必须符合SpEL表达式,通过 @Value("${spelDefault.value}") 获取属性文件中对应的值,可以设置默认值

java 复制代码
@Value("${author.name:John}")
private String name;

上述代码的含义是表示向bean的属性中注入属性文件中的author.name属性所对应的值,如果属性文件中没有author.name这个属性,那么便向bean的属性中注入默认值John。

注意:KaTeX parse error: Expected 'EOF', got '#' at position 9: {···} 和 #̲{···}可以混合使用,但是必...{}在里面,因为Spring 先执行${...},后执行#{...}

ConfigurableEnvironment 取值

java 复制代码
@Test
public void testField(){
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig3.class);
    
    ConfigurableEnvironment environment = applicationContext.getEnvironment();
    String name = environment.getProperty("nickName");
    System.out.println(name);

}

10.3 XML 方式

从 XML 配置文件中获取 person.properties 文件中的值

  • 在 Spring 的 XML 配置文件中引入 context 名称空间
  • 使用 context 命名空间导入 person.properties 文件
  • 在 bean 的属性字段中使用 ${person.nickName} 的方式取值
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
        xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans 
                                                http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
                                                http://www.springframework.org/schema/context 
                                                http://www.springframework.org/schema/context/spring-context-4.2.xsd">

        <context:property-placeholder location="classpath:person.properties" />
        
        <!-- 注册组件 -->
        <bean id="person" class="com.meimeixia.bean.Person">
                <property name="age" value="18"></property>
                <property name="name" value="John"></property>
                <property name="nickName" value="${person.nickName}"></property>
        </bean>
        
</beans>

11. 自动装配

Spring组件的自动装配就是Spring利用依赖注入(DI),完成对 IOC 容器中各个组件的依赖关系赋值。

11.1 @Autowired 注解

@Autowired注解可以对类成员变量、方法和构造函数进行标注,完成自动装配的工作。@Autowired注解可以放在类、接口以及方法上。

@Autowired 注解的源码

java 复制代码
package org.springframework.beans.factory.annotation;

@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Autowired {

    /**
     * Declares whether the annotated dependency is required.
     * <p>Defaults to {@code true}.
     */
    boolean required() default true;

}

这儿对 @Autowired 注解说明一下:

  • @Autowired 注解默认是优先按照类型去容器中找对应的组件,相当于是调用了如下这个方法 applicationContext.getBean(类名.class); 若找到则就赋值。
  • 如果找到多个相同类型的组件,那么是将属性名称作为组件的 id,到 IOC 容器中进行查找,这时就相当于是调用了如下这个方法:applicationContext.getBean("组件的 id");
  • required 属性
    • 默认值为 true ,如果在赋值的时候找不到对应的组件,就会报错
    • false,如果在赋值的时候找不到对应的组件,则属性值为 null ,不会报错

标注在属性上

java 复制代码
package com.atguigu.controller;

@Controller
public class BookController {

    @Autowired
    private BookService bookService;

}

标注在构造方法上

  • 参数位置的组件自动从IOC容器中获取
  • 如果只有一个有参构造方法,@Autowired 可以省略
java 复制代码
@Component
public class Boss {
        
    private Car car;
    
    @Autowired
    public Boss(Car car) {
        this.car = car;
        System.out.println("Boss...有参构造器");
    }
     
}

标注在 Set 方法上

  • 参数位置的组件自动从IOC容器中获取
java 复制代码
package com.atguigu.controller;

@Controller
public class BookController {

    private BookService bookService;

    @Autowired
    public void setBookService(BookService bookService) {
        this.bookService = bookService;
    }
    
}

标注在参数位置

java 复制代码
package com.atguigu.controller;

@Controller
public class BookController {

    private BookService bookService;

    public BookController(@Autowired BookService bookService) {
        this.bookService = bookService;
        System.out.println("有参构造方法");
    }

}

标注在注册组件时的方法上

  • 此时 @Autowired 可以省略
java 复制代码
package com.atguigu.config;

@ComponentScan("com.atguigu")
@Configuration
public class MainConfig4 {

    @Bean
    @Primary
    public BookService service(){
        BookService bookService = new BookService();
        bookService.setTag("123");
        return bookService;
    }

    @Bean
    public BookController bookController(@Autowired Blue blue){
        return new BookController(blue);
    }

    @Bean
    public Blue blue(){
        return new Blue();
    }


}

11.2 @Qualifier 注解

@Autowired是根据类型进行自动装配的,如果需要按名称进行装配,那么就需要配合@Qualifier注解来使用了。

@Qualifier注解的源码

java 复制代码
package org.springframework.beans.factory.annotation;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Qualifier {

    String value() default "";

}

11.3 @Primary 注解

在 Spring 中使用注解时,常常会使用到 @Autowired 这个注解,它默认是根据类型 Type 来自动注入的。但有些特殊情况,对同一个接口而言,可能会有几种不同的实现类,而在默认只会采取其中一种实现的情况下,就可以使用 @Primary 注解来标注优先使用哪一个实现类。

@Primary 注解的源码

java 复制代码
package org.springframework.context.annotation;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Primary {

}

示例

controller

java 复制代码
package com.atguigu.controller;

@Controller
public class BookController {

    @Autowired
    private BookService bookService;

}

service

java 复制代码
package com.atguigu.service;

@Service
public class BookService {

    private String tag = "456";
    
}

配置类

java 复制代码
package com.atguigu.config;

@ComponentScan("com.atguigu")
@Configuration
public class MainConfig4 {

    @Bean
    public BookService service(){
        BookService bookService = new BookService();
        bookService.setTag("123");
        return bookService;
    }
    
}

测试类

java 复制代码
@Test
public void testAutowired(){

    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig4.class);
    BookController bookController = (BookController) applicationContext.getBean("bookController");
    System.out.println(bookController);

}

注意:

  • 我们在 BookController 类中使用@Autowired注解注入的 BookService 对象和直接从IOC容器中获取的BookService 对象是同一个对象
  • 如果找到多个相同类型的组件,那么再将属性名作为组件的 id ,到IOC容器中进行查找

如果想按名称进行装配

java 复制代码
package com.atguigu.controller;

@Controller
public class BookController {

    @Qualifier("service")
    @Autowired
    private BookService bookService;

}

使用 @Primary 注解来标注优先使用哪一个实现类

java 复制代码
package com.atguigu.config;

@ComponentScan("com.atguigu")
@Configuration
public class MainConfig4 {

    @Bean
    @Primary
    public BookService service(){
        BookService bookService = new BookService();
        bookService.setTag("123");
        return bookService;
    }

}

11.4 @Resource 注解

@Resource 注解是 Java 规范里面的,也可以说它是 JSR250 规范里面定义的一个注解

该注解默认按照名称进行装配,名称可以通过 name 属性进行指定

如果没有指定 name 属性,当注解写在字段上时,那么默认取字段名将其作为组件的名称在 IOC 容器中进行查找

如果注解写在 setter 方法上,那么默认取属性名进行装配

当找不到与名称匹配的 bean 时才按照类型进行装配

但是需要注意的一点是,如果 name 属性一旦指定,那么就只会按照名称进行装配

@Resource 注解的源码

java 复制代码
package javax.annotation;

@Target({TYPE, FIELD, METHOD})
@Retention(RUNTIME)
public @interface Resource {

    String name() default "";

    String lookup() default "";

    Class<?> type() default java.lang.Object.class;

    enum AuthenticationType {
            CONTAINER,
            APPLICATION
    }

    AuthenticationType authenticationType() default AuthenticationType.CONTAINER;

    boolean shareable() default true;

    String mappedName() default "";

    String description() default "";
}

注意:

  • @Resource 注解不支持@Primary注解优先注入的功能
  • @Resource 注解没有 required=false 属性

示例

java 复制代码
package com.atguigu.controller;

@Controller
public class BookController {

    @Resource(name = "service")
    private BookService bookService;

}

11.5 @Inject 注解

@Inject 注解也是 Java 规范里面的,也可以说它是 JSR330 规范里面定义的一个注解

该注解默认是根据参数名去寻找 bean 注入,支持 Spring 的 @Primary 注解优先注入

@Inject 注解还可以增加 @Named 注解指定要注入的 bean

@Inject 注解的源码

xml 复制代码
<dependency>
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
    <version>1</version>
</dependency>
java 复制代码
package javax.inject;

@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Inject {
}

注意:

  • @Inject注解支持 @Primary 注解优先注入
  • @Inject注解没有 required=false属性

示例

java 复制代码
package com.atguigu.controller;

@Controller
public class BookController {

    @Inject
    private BookService bookService;

}

当你使用 @Inject 注解进行依赖注入时,如果存在多个类型相同的实例,需要通过 @Named 注解指定具体要注入的实例。以下是一个使用 @Inject 和 @Named 注解的简单示例:

java 复制代码
import javax.inject.Inject;
import javax.inject.Named;

public class ExampleService {

    private final DataService dataService;

    @Inject
    public ExampleService(@Named("database") DataService dataService) {
        this.dataService = dataService;
    }

    public void doSomething() {
        dataService.connect();
        // do something with the data service
    }
}

在上面的示例中,ExampleService 类依赖于 DataService 接口的一个实现。但是,如果有多个实现 DataService 接口的类,并且你想注入名为 "database" 的特定实现,你可以在构造函数参数上使用 @Named 注解:

java 复制代码
import javax.inject.Singleton;

@Named("database")
@Singleton
public class DatabaseDataService implements DataService {

    @Override
    public void connect() {
        // Logic to connect to a database
        System.out.println("Connecting to the database...");
    }
}
java 复制代码
import javax.inject.Singleton;

@Named("file")
@Singleton
public class FileDataService implements DataService {

    @Override
    public void connect() {
        // Logic to connect to a file system
        System.out.println("Connecting to the file system...");
    }
}

在上面的示例中,我们创建了两个实现 DataService 接口的类:DatabaseDataService 和 FileDataService。通过在类上使用 @Named("database") 和 @Named("file") 注解,我们为它们提供了唯一的标识符。然后,在 ExampleService 中使用 @Inject 和 @Named("database"),我们告诉依赖注入容器我们需要注入名为 "database" 的 DataService 实例。

这样,当你创建 ExampleService 实例时,依赖注入容器会自动注入名为 "database" 的 DataService 实现。

比较

  • @Autowired 是 Spring 中的专有注解,而 @Resource 是 Java 中 JSR250 规范里面定义的一个注解,@Inject 是 Java 中 JSR330 规范里面定义的一个注解
  • @Autowired 支持参数 required=false,而 @Resource 和 @Inject 都不支持
  • @Autowired 和 @Inject 支持 @Primary 注解优先注入,而 @Resource 不支持
  • @Autowired 通过 @Qualifier 指定注入特定 bean,@Resource 可以通过参数 name 指定注入 bean,而 @Inject 需要通过 @Named 注解指定注入 bean

11.6 Aware 接口

自定义的组件要想使用 Spring 容器底层的一些组件,比如 ApplicationContext(IOC 容器)、底层的 BeanFactory 等等,那么只需要让自定义组件实现 XxxAware 接口即可。此时,Spring 在创建对象的时候,会调用 XxxAware 接口中定义的方法注入相关的组件

前面在讲 后置处理器 的时候提到了 ApplicationContextAware 接口

前情回顾

我们创建一个 Dog 类,使其实现 ApplicationContextAware 接口,此时,我们需要实现 ApplicationContextAware 接口中的 setApplicationContext()方法,在 setApplicationContext() 方法中有一个 ApplicationContext 类型的参数,这个就是 IOC 容器对象,我们可以在 Dog 类中定义一个 ApplicationContext 类型的成员变量,然后在 setApplicationContext() 方法中为这个成员变量赋值,此时就可以在 Dog 类中的其他方法中使用 ApplicationContext 对象了

在Spring中,类似于 ApplicationContextAware 接口的设计有很多,本质上,Spring 中形如 XxxAware 这样的接口都继承了 Aware 接口,我们来看下 Aware 接口的源码

java 复制代码
package org.springframework.beans.factory;

/**
 * @author Chris Beams
 * @since 3.1
 */
public interface Aware {

}
可以看到,Aware接口是Spring 3.1版本中引入的接口,在Aware接口中,并未定义任何方法。
ApplicationContextAware 接口
通过ApplicationContextAware接口我们可以获取到 IOC 容器
public class Color implements ApplicationContextAware {

    private ApplicationContext applicationContext;

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

BeanNameAware 接口

通过 BeanNameAware 接口获取到当前 bean 在 Spring 容器中的名称

java 复制代码
public class Color implements BeanNameAware {

    /**
     * 参数name:IOC容器创建当前对象时,为这个对象起的名字
     */
    @Override
    public void setBeanName(String name) {
            System.out.println("当前bean的名字:" + name);
    }
}

EmbeddedValueResolverAware 接口

通过 EmbeddedValueResolverAware 接口能够获取到 String 值解析器

java 复制代码
public class Color implements EmbeddedValueResolverAware{

    /**
     * 参数resolver:IOC容器启动时会自动地将这个String值的解析器传递过来给我们
     */
    @Override
    public void setEmbeddedValueResolver(StringValueResolver resolver) {
            String resolveStringValue = resolver.resolveStringValue("你好,${os.name},我的年龄是#{20*18}");
            System.out.println("解析的字符串:" + resolveStringValue);
    }
    
}

原理

XxxAware 接口的底层原理是由 XxxAwareProcessor 实现类实现的,也就是说每一个 XxxAware 接口都有它自己对应的 XxxAwareProcessor 实现类。 例如,我们这里以 ApplicationContextAware 接口为例,ApplicationContextAware 接口的底层原理就是由 ApplicationContextAwareProcessor 类实现的。从 ApplicationContextAwareProcessor 类的源码可以看出,其实现了 BeanPostProcessor 接口,本质上是一个后置处理器。

java 复制代码
package org.springframework.context.support;

/**
 * @author Juergen Hoeller
 * @author Costin Leau
 * @author Chris Beams
 * @since 10.10.2003
 * @see org.springframework.context.EnvironmentAware
 * @see org.springframework.context.EmbeddedValueResolverAware
 * @see org.springframework.context.ResourceLoaderAware
 * @see org.springframework.context.ApplicationEventPublisherAware
 * @see org.springframework.context.MessageSourceAware
 * @see org.springframework.context.ApplicationContextAware
 * @see org.springframework.context.support.AbstractApplicationContext#refresh()
 */
class ApplicationContextAwareProcessor implements BeanPostProcessor {

    private final ConfigurableApplicationContext applicationContext;

    private final StringValueResolver embeddedValueResolver;


    /**
     * Create a new ApplicationContextAwareProcessor for the given context.
     */
    public ApplicationContextAwareProcessor(ConfigurableApplicationContext applicationContext) {
       this.applicationContext = applicationContext;
       this.embeddedValueResolver = new EmbeddedValueResolver(applicationContext.getBeanFactory());
    }


    @Override
    public Object postProcessBeforeInitialization(final Object bean, String beanName) throws BeansException {
       AccessControlContext acc = null;

       if (System.getSecurityManager() != null &&
             (bean instanceof EnvironmentAware || bean instanceof EmbeddedValueResolverAware ||
                   bean instanceof ResourceLoaderAware || bean instanceof ApplicationEventPublisherAware ||
                   bean instanceof MessageSourceAware || bean instanceof ApplicationContextAware)) {
          acc = this.applicationContext.getBeanFactory().getAccessControlContext();
       }

       if (acc != null) {
          AccessController.doPrivileged(new PrivilegedAction<Object>() {
             @Override
             public Object run() {
                invokeAwareInterfaces(bean);
                return null;
             }
          }, acc);
       }
       else {
          invokeAwareInterfaces(bean);
       }

       return bean;
    }

    private void invokeAwareInterfaces(Object bean) {
       if (bean instanceof Aware) {
          if (bean instanceof EnvironmentAware) {
             ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());
          }
          if (bean instanceof EmbeddedValueResolverAware) {
             ((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver);
          }
          if (bean instanceof ResourceLoaderAware) {
             ((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext);
          }
          if (bean instanceof ApplicationEventPublisherAware) {
             ((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext);
          }
          if (bean instanceof MessageSourceAware) {
             ((MessageSourceAware) bean).setMessageSource(this.applicationContext);
          }
          if (bean instanceof ApplicationContextAware) {
             ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
          }
       }
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
       return bean;
    }

}

11.7 @Profile注解

注解概述

在容器中如果存在同一类型的多个组件,那么可以使用 @Profile 注解标识要获取的是哪一个 bean。

@Profile 注解是 Spring 为我们提供的可以根据当前环境,动态地激活和切换一系列组件的功能。

这个功能在不同的环境使用不同的变量的情景下特别有用,例如,开发环境、测试环境、生产环境使用不同的数据源,在不改变代码的情况下,可以使用这个注解来动态地切换要连接的数据库。

@Profile注解的源码

java 复制代码
package org.springframework.context.annotation;

/**
 * @author Chris Beams
 * @author Phillip Webb
 * @author Sam Brannen
 * @since 3.1
 * @see ConfigurableEnvironment#setActiveProfiles
 * @see ConfigurableEnvironment#setDefaultProfiles
 * @see AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME
 * @see AbstractEnvironment#DEFAULT_PROFILES_PROPERTY_NAME
 * @see Conditional
 * @see org.springframework.test.context.ActiveProfiles
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {

    /**
     * The set of profiles for which the annotated component should be registered.
     */
    String[] value();

}

从源码中我们可以得出如下三点结论:

  • @Profile 注解不仅可以标注在方法上,也可以标注在配置类上。
  • 如果 @Profile 注解标注在配置类上,那么只有是在指定的环境的时候,整个配置类里面的所有配置才会生效。
  • 如果一个 bean 上没有使用 @Profile 注解进行标注,那么这个 bean 在任何环境下都会被注册到 IOC 容器中,当然了,前提是在整个配置类生效的情况下。

实战案例

环境搭建

在pom.xml文件中添加c3p0数据源和MySQL驱动的依赖

xml 复制代码
<dependency>
    <groupId>c3p0</groupId>
    <artifactId>c3p0</artifactId>
    <version>0.9.1.2</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.44</version>
</dependency>

新建一个配置类,例如MainConfigOfProfile,并在该配置类中模拟开发、测试、生产环境的数据源

在真实项目开发中,那些数据库连接的相关信息,例如用户名、密码以及MySQL数据库驱动类的全名,这些要抽取在一个配置文件中

db.user=root
db.password=123456
db.driverClass=com.mysql.jdbc.Driver
java 复制代码
@PropertySource("classpath:/dbconfig.properties")
@Configuration
public class MainConfigOfProfile implements EmbeddedValueResolverAware {
        
        @Value("${db.user}")
        private String user;
        
        private StringValueResolver valueResolver;
        
        private String dirverClass;
        
        @Bean("testDataSource")
        public DataSource dataSourceTest(@Value("${db.password}") String pwd) throws Exception {
                ComboPooledDataSource dataSource = new ComboPooledDataSource();
                dataSource.setUser(user);
                dataSource.setPassword(pwd);
                dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test1");
                dataSource.setDriverClass(dirverClass);
                return dataSource;
        }
        
        @Bean("devDataSource")
        public DataSource dataSourceDev(@Value("${db.password}") String pwd) throws Exception {
                ComboPooledDataSource dataSource = new ComboPooledDataSource();
                dataSource.setUser(user);
                dataSource.setPassword(pwd);
                dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test2");
                dataSource.setDriverClass(dirverClass);
                return dataSource;
        }
        
        @Bean("prodDataSource")
        public DataSource dataSourceProd(@Value("${db.password}") String pwd) throws Exception {
                ComboPooledDataSource dataSource = new ComboPooledDataSource();
                dataSource.setUser(user);
                dataSource.setPassword(pwd);
                dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test3");
                dataSource.setDriverClass(dirverClass);
                return dataSource;
        }

        @Override
        public void setEmbeddedValueResolver(StringValueResolver resolver) {
                this.valueResolver = resolver;
                dirverClass = valueResolver.resolveStringValue("${db.driverClass}");
        }

}
  • 使用 @Bean("devDataSource") 注解标注的是开发环境使用的数据源
  • 使用 @Bean("testDataSource") 注解标注的是测试环境使用的数据源
  • 使用 @Bean("prodDataSource") 注解标注的是生产环境使用的数据源

写测试

java 复制代码
@Test
public void test01() {
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfigOfProfile.class);

    String[] namesForType = applicationContext.getBeanNamesForType(DataSource.class);
    for (String name : namesForType) {
            System.out.println(name);
    }
    
    applicationContext.close();
}

可以看到三种不同的数据源成功注册到了IOC容器中,说明我们的环境搭建成功了。

环境标识

我们成功搭建环境之后,接下来,就是要实现根据不同的环境来向 IOC 容器中注册相应的 bean 了。

  • 在开发环境注册开发环境下使用的数据源;
  • 在测试环境注册测试环境下使用的数据源;
  • 在生产环境注册生产环境下使用的数据源。

此时,@Profile 注解就显示出其强大的特性了。

  • 使用 @Profile("dev") 注解来标识在开发环境
  • 使用 @Profile("test") 注解来标识在测试环境
  • 使用 @Profile("prod") 注解来标识在生产环境
java 复制代码
@Profile("test")
@Bean("testDataSource")
public DataSource dataSourceTest(@Value("${db.password}") String pwd) throws Exception {
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setUser(user);
        dataSource.setPassword(pwd);
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/test");
        dataSource.setDriverClass(dirverClass);
        return dataSource;
}
 
@Profile("dev") // 定义了一个环境标识,只有当dev环境被激活以后,我们这个bean才能被注册进来
@Bean("devDataSource")
public DataSource dataSourceDev(@Value("${db.password}") String pwd) throws Exception {
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setUser(user);
        dataSource.setPassword(pwd);
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/ssm_crud");
        dataSource.setDriverClass(dirverClass);
        return dataSource;
}

@Profile("prod")
@Bean("prodDataSource")
public DataSource dataSourceProd(@Value("${db.password}") String pwd) throws Exception {
        ComboPooledDataSource dataSource = new ComboPooledDataSource();
        dataSource.setUser(user);
        dataSource.setPassword(pwd);
        dataSource.setJdbcUrl("jdbc:mysql://localhost:3306/scw_0515");
        dataSource.setDriverClass(dirverClass);
        return dataSource;
}
环境激活

通过@Profile注解加了环境标识的bean,只有这个环境被激活的时候,相应的bean才会被注册到IOC容器中。

如果需要一个默认的环境,可以通过 @Profile("default") 注解来标识一个默认的环境

激活环境有两种方式

方式一:命令行参数

我们在运行程序的时候可以添加相应的命令行参数。例如,如果我们现在的环境是测试环境,那么可以在运行程序的时候添加如下命令行参数。

-Dspring.profiles.active=test

方式二:代码实现

通过 AnnotationConfigApplicationContext 类的无参构造方法来实现

java 复制代码
@Test
public void test02() {
    // 1. 使用无参构造器创建一个IOC容器
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
    // 2. 在我们容器还没启动创建其他bean之前,先设置需要激活的环境(可以设置激活多个环境哟)
    applicationContext.getEnvironment().setActiveProfiles("test");
    // 3. 注册主配置类
    applicationContext.register(MainConfigOfProfile.class);
    // 4. 启动刷新容器
    applicationContext.refresh();
    
    String[] namesForType = applicationContext.getBeanNamesForType(DataSource.class);
    for (String name : namesForType) {
            System.out.println(name);
    }
    
    // 关闭容器
    applicationContext.close();
}
相关推荐
Q_19284999069 分钟前
基于Spring Boot的个人健康管理系统
java·spring boot·后端
liutaiyi89 分钟前
Redis可视化工具 RDM mac安装使用
redis·后端·macos
Q_192849990616 分钟前
基于Springcloud的智能社区服务系统
后端·spring·spring cloud
xiaocaibao77719 分钟前
Java语言的网络编程
开发语言·后端·golang
m0_748245171 小时前
Web第一次作业
java
小码的头发丝、1 小时前
Java进阶学习笔记|面向对象
java·笔记·学习
m0_548514771 小时前
前端Pako.js 压缩解压库 与 Java 的 zlib 压缩与解压 的互通实现
java·前端·javascript
坊钰1 小时前
【Java 数据结构】移除链表元素
java·开发语言·数据结构·学习·链表
chenziang12 小时前
leetcode hot100 LRU缓存
java·开发语言