深入了解下springboot中的bean加载和覆盖问题

springboot难免要用到bean,但这些bean如何导入,对于初学者时间头疼的事,本文尽量简单的例子说明springboot是如何处理bean的。

相关问题:@TestConfiguration 无法覆盖bean
@Configuration 配置不生效
bean的继承和后续手动注入

java注解bean配置类

原则1:@TestConfiguration 复写 @Configuration

即 @TestConfiguration 优先于@Configuration

原则2: xml配置优先于class配置,

即约定优于配置原则,配置改写约定

原则3:后置替换前置

同名文件后置文件替换前置文件,非同名文件,后置的bean替换前置的bean定义;
这里是替换,注意不是补充或者修正是完全替换定义。

@ImportResource(locations = {
            "classpath:A.xml",
            "classpath:B.xml"})  //这里test里的配置优先级是最高的

这里AB中的如果同时定义了bean1 则最终生效的是B中的bean1;如果引用(父级)包中也存在A,那么恭喜你,引用(父级)包中的A将不会再起作用,即使其中有本项目中A不同的bean定义也不会被实现,完全被本地的A所替代。

@TestConfiguration ,@Configuration,这两个是一对, Configuration用在正式的包里,TestConfiguration是用在测试的包里,是对Configuration的补充和“覆盖”。
这里需要特别注意的是如果同时存在xml和config.class包的引入,则xml会在java配置的bean后面引入,也即xml的配置会覆盖掉Configuration和TestConfiguration中的注入,也就是说TestConfiguration里的java定义的覆盖仅仅是针对java方式定义的bean的有效,xml的则需要单独引入测试用的xml才可以)。如果需要在以java方式覆盖掉xml的配置,这在springboot里是不被允许的,也即xml配置优先于java config方式的注入,以方便通过修改配置的方式升级jar包程序。但是如果想在测试的时候在不引入新的xml的情况下测试怎么覆盖呢?这就需要下面的AnnotationConfigApplicationContext和GenericApplicationContext这两个类来重新注入修改,注意这时候ioc里的是修改了的,但之前通过@Autowired创建的属性是不会自己自动更新的,需要手动更新或者自己重新获取进行测试。

注解引入bean的两种方式

@ImportResource :引入xml
@Import :引入config.class

手动创建ApplicationContext

ClassPathXmlApplicationContext :通过xml方式创建
AnnotationConfigApplicationContext:通过config.class创建
GenericApplicationContext:通过类注册bean

不多说直接上测试代码进行说明
正式包里的xmlbean配置,放在resources目录下,
foo.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:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-2.5.xsd ">
    <bean id="foo" class="java.lang.String" autowire="byName">
        <constructor-arg index="0"  type="java.lang.String" value="foo0xml" />
    </bean>
</beans>

test测试包里的xml配置,放在测试的resources目录下,
foo-test.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:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context-2.5.xsd ">
    <bean id="foo" class="java.lang.String" autowire="byName">
        <constructor-arg index="0"  type="java.lang.String" value="foo1xml" />
    </bean>
  </beans>

正式包里的config
Config.java

@Configuration
public class Config {
    @Bean
    public String foo() {
        return "foo0";
    }
    @Bean
    public String foo2() {
        return "foo20";
    }
}

测试的脚本 FooTest

import 你的package.config.Config;
import org.junit.jupiter.api.Test;//注意这里用的springboot2.4.2,是最新的测试类junit5
import org.junit.jupiter.api.extension.ExtendWith;//springboot2.4.2的测试类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportResource;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.junit.Assert.assertEquals;
@ExtendWith(SpringExtension.class)
//springboot2.4.2,它的作用就是扫描spring注解,默认启用自动扫描Configuration,和TestConfiguration
public class FooTest {
    @Autowired
    protected ApplicationContext applicationContext;
    @Autowired
    public String foo;
    @Autowired
    public String foo2;
    @TestConfiguration()
    //这里如果配置的是xml则永远都是xml优先于Configuration,TestConfiguration,在没有xml的时候它会覆盖Configuration里注入的bean
    @ImportResource(locations = {
            "classpath:foo.xml",
            "classpath:foo-test.xml"})  //这里test里的配置优先级是最高的
    @Import(Config.class)//这个可以不写,ExtendWith会自动扫描到,放在这仅仅是做对比测试
    static class TestConfig {
        @Bean
        public String foo() {
            return "foo11";
        }
        @Bean
        public String foo2() {
            return "foo21";
        }
    }
//这个类在ExtendWith不会被用到,是为了后面的AnnotationConfigApplicationContext 手动注入时bean用的
    @Import(Config.class)
    static class TestConfig2 {
        @Bean
        public String foo() {
            return "foo12";
        }
        @Bean
        public String foo2() {
            return "foo22";
        }
         }
    @Test //初始带xml测试
    public void configTest() {
        assertEquals("foo1xml",foo);//因为初始时,TestConfiguration加载了xml所以xml的值生效
        assertEquals("foo21",foo2);//foo2仅存在于javaconfig里所以TestConfiguration的值生效
    }
   @Test //初始bean中的值测试
    public void config1Test() {
        String[] beans =  applicationContext.getBeanDefinitionNames();//可以通过遍历这个查看所以注入的bean
        assertEquals("foo1xml",applicationContext.getBean("foo"));
        assertEquals("foo21",applicationContext.getBean("foo2"));
    }
    @Test  //用ClassPathXmlApplicationContext后续加载xml,相对来说,使用注解方式方便,注解方式的还可以使@Autowired方式的属性生效,
    public void config2Test() {
        ApplicationContext applicationContext2=new ClassPathXmlApplicationContext(new String[]{
                "classpath:foo.xml",
                "classpath:foo-test.xml"},applicationContext);
        //applicationContext2以applicationContext为父级创建可以集成它的所有bean
       // @Autowired方式已经生成过的属性是不能自动使用新的bean的
        assertEquals("foo1xml", applicationContext2.getBean("foo"));
        assertEquals("foo21",applicationContext2.getBean("foo2"));
    }
    @Test //用config方式后续加载bean配置
    public void config3Test(){
        AnnotationConfigApplicationContext applicationContext3=new AnnotationConfigApplicationContext(TestConfig2.class);
        (applicationContext3).setParent(applicationContext);
        //applicationContext3以applicationContext为父级创建可以集成它的所有bean
       // @Autowired方式已经生成过的属性是不能自动使用新的bean的
        assertEquals("foo12", applicationContext3.getBean("foo"));
        assertEquals("foo22",applicationContext3.getBean("foo2"));
        }
    @Test //单点覆盖,可以覆盖任何
    public void config4Test(){
        GenericApplicationContext applicationContext4=new GenericApplicationContext(applicationContext);
        applicationContext4.registerBean("foo",String.class,"foo3");//这里是单点方式只能单个修改bean不能使用config.class类进行注入
        applicationContext4.refresh();//注入修改完需要refresh以下才可以生效
        assertEquals("foo3", applicationContext4.getBean("foo"));
        assertEquals("foo21",applicationContext.getBean("foo2"));
    }
}

最后,总的来看,springboot bean的加载方式,原则就是通过注解方式注入,xml的配置会在config的配置之后被加载,xml的注入会覆盖config里的而不管你是不是TestConfiguration,根xml的位置无关。如非必要还是建议按照约定的来,java方式注入的bean用TestConfiguration来修改,xml方式注入的测试的时候就用test.xml来覆盖,这样所有的@Autowired都是生效的,方便测试和系统稳定。


版权声明:本文为wuqingsssss原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。