SpringBoot bean自动装配原理,这一篇就够了! 2

简介: SpringBoot bean自动装配原理,这一篇就够了!

4.2 autowireMode

我们继续看第二个属性:autowireMode,自动装配模式

我们在AbstractBeanDefinition源码中可以看到:


private int autowireMode = AUTOWIRE_NO;

自动装配模式默认是AUTOWIRE_NO,就是不开启自动装配

可选的常量值有以下四种:不自动装配,通过名称装配,通过类型装配,通过构造器装配

  • AUTOWIRE_NO
  • AUTOWIRE_BY_NAME
  • AUTOWIRE_BY_TYPE
  • AUTOWIRE_CONSTRUCTOR

接下来我们来模拟一个自动装配场景,仍然是A和B两个类,现在在A类中添加B类对象

@Component
public class A {
    @Value("我是AAA")
    private String name;
    @Autowired
    private B b;
}

我们希望b对象能够自动装配,于是我们给他加上了@Autowired注解,其他的完全不变,我们自定义的MyBeanPostProcessor中也不做任何操作,让我们运行测试类:

这里是MyBeanPostProcessor,我拿到了:pojo.A
最终拿到了==> A(name=我是AAA, b=B(name=我是BBB))

自动装配成功了!我们拿到的A类对象里面成功注入了B类对象b

现在问题来了,如果我把@Autowired注解去掉,自动装配会成功吗?

这里是MyBeanPostProcessor,我拿到了:pojo.A
最终拿到了==> A(name=我是AAA, b=null)

必然是不成功的

但是我就是想要不加@Autowired注解,仍然可以实现自动装配,需要怎么做?

这时就要在我们的MyBeanPostProcessor中做文章了,加入如下内容:


defA.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_NAME);

再输出结果:

这里是MyBeanPostProcessor,我拿到了:pojo.A
最终拿到了==> A(name=我是AAA, b=B(name=我是BBB))

自动装配成功了!这次我们可没加@Autowired,在我们的自定义的bean后置处理器中设置了autowireMode属性,也实现了自动装配

综上,autowireMode属性是用来控制自动装配模式的,默认值是AUTOWIRE_NO,即不自动装配

4.3 constructorArgumentValues

constructorArgumentValues的字面含义是构造器参数值

改变这个参数值,我们可以做到在实例化对象时指定特定的构造器

话不多说,show me your code:

因为要研究构造器,只能先”忍痛“关掉lombok插件,手写一个pojo.Student类

/**
 * Student类
 * @author dzzhyk
 */
@Component
public class Student {
    private String name;
    private Integer age;
    public Student() {
        System.out.println("==>使用空参构造器 Student()");
    }
    public Student(String name, Integer age) {
        this.name = name;
        this.age = age;
        System.out.println("==>使用双参数构造器 Student(String name, Integer age)");
    }
}

我们都知道,spring在实例化对象时使用的是对象的默认空参构造器:

我们新建一个测试方法test

@Test
public void test(){
    ApplicationContext ca = new ClassPathXmlApplicationContext("applicationContext.xml");
    Student student = ca.getBean("stu", Student.class);
    System.out.println("==>" + student);
}

运行可以得到下面结果:

这里是MyBeanPostProcessor,我拿到了:pojo.Student
==>使用空参构造器 Student()
==>pojo.Student@402e37bc

可以看到,确实使用了空参构造器

但是如何指定(自定义)使用哪个构造器呢?我根本看不见摸不着,Spring全帮我做了,实在是太贴心了。

接下来就聊聊constructorArgumentValues的使用:

我们在MyBeanPostProcessor中加入如下内容,对获取到的pojo.Student的bean定义进行操作:

ConstructorArgumentValues args = new ConstructorArgumentValues();
args.addIndexedArgumentValue(0, "我指定的姓名");
args.addIndexedArgumentValue(1, 20);
defStu.setConstructorArgumentValues(args);

再次运行test:


这里是MyBeanPostProcessor,我拿到了:pojo.Student
==>使用双参数构造器 Student(String name, Integer age)
==>pojo.Student@2f177a4b

可以看到这次使用了双参数构造器

有人会好奇ConstructorArgumentValues到底是个什么东西,我点进源码研究一番,结果发现这个类就是一个普通的包装类,包装的对象是ValueHolder,里面一个List一个Map<Integer, ValueHolder>

而ValueHolder这个对象继承于BeanMetadataElement,就是构造器参数的一个包装类型

通过这个例子我们可以看到ConstructorArgumentValues就是用来管控构造器参数的,指定这个值会在进行bean注入的时候选择合适的构造器。

5. 装配对象

现在我们把目光放回到SpringBoot的自动装配上来,原来在真正进行bean实例化对象前,我们前面还有这些过程,尤其是存在使用后置处理器BeanFactoryPostProcessor来对bean定义进行各种自定义修改的操作。

经过上面我们漫长的研究过程,我们终于可以回答第一个问题了:

自动装配的对象:Bean定义 (BeanDefinition)

6. My自动装配

看到这里又自然会产生疑问:不会吧,上面可都是自动装配啊,我在配置文件或者使用注解都配置了变量的值,然后加个@Autowired注解就OK了,spring也是帮我自动去装配。

再高端一点话,我就把XML文件写成JavaConfig配置类,然后使用@Configuration注解,这样也能自动装配,这不是很nice了吗?

6.1 自动装配之再思考

我的理解,上面的自动装配,我们至少要写一个配置文件,无论是什么形式,我们都至少需要一个文件把它全部写下来,就算这个文件的内容是固定的,但是为了装配这个对象,我们不得不写。

我们甚至都可以做成模板了,比如我在学习spring框架整合时,把经常写的都搞成了模板:

有了这些模板,我们只需要点点点,再进行修改,就能用了。

这样做确实很好,可是对于越来越成型的项目体系,我们每次都搞一些重复动作,是会厌烦的。而且面对这么多xml配置文件,我太难了。

于是我有了一个想说但不敢说的问题:

我一个配置文件都不想写,程序还能照样跑,我只关心有我需要的组件就可以了,我只需要关注我的目标就可以了,我想打开一个工程之后可以1秒进入开发状态,而不是花3小时写完配置文件(2.5小时找bug)希望有个东西帮我把开始之前的准备工作全做了,即那些套路化的配置,这样在我接完水之后回来就可以直接进行开发。

说到这里,想必大家都懂了:SpringBoot

6.2 一个例子

让我们在偷懒的道路上继续前进。

来看下面这个例子:

仍然是A类和B类,其中A类仍然引用了B类,我们给A类组件起id=“a”,B类组件起id=“b”

@Component("a")
public class A {
    @Value("我是AAA")
    private String name;
    @Autowired
    private B b;
}
@Component("b")
public class B {
    @Value("我是BBB")
    private String name;
}

可以看到我们使用了@Autowired注解来自动注入b,测试类如下:

@Test
public void test(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyAutoConfig.class);
A aaa = ac.getBean("a", A.class);
System.out.println(aaa);
}

细心的同学已经发现了:我们这里使用了AnnotationConfigApplicationContext这个JavaConfig配置类会使用到的加载类,于是我们顺利成章地点开它所加载的MyAutoConfig类文件

文件内容如下:

@Test
public void test(){
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(MyAutoConfig.class);
A aaa = ac.getBean("a", A.class);
System.out.println(aaa);
}

what? 我要声明的Bean对象都去哪了(注意:这里的applicationContext.xml是空的)?

让我们运行test:


A(name=我是AAA, b=B(name=我是BBB))

竟然运行成功了,这究竟是为什么?(元芳,你怎么看?)

细心的同学已经发现了:@MyEnableAutoConfig是什么注解?我怎么没有这个注解

让我们点进@MyEnableAutoConfig一探究竟:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(MyImportSelector.class)   // 导入bean定义
public @interface MyEnableAutoConfig {
}

原来如此!你是用了@Import注解导入了Bean定义对吧,注释都写着呢!

可是客官,@Import导入bean定义是没错,但是它导入的是MyImportSelector这个bean,不是A也不是B啊…

6.3 @Import注解

@Import的功能就是获取某个类的bean对象,他的使用形式大致如下:

@Import(A.class)
@Import(MyImportBeanDefinitionRegister.class)
@Import(MyImportSelector.class)

6.3.1 @Import(A.class)

第一种形式@Import(A.class),是最简单易懂的形式

我们需要哪个Bean定义,直接Import他的class即可

6.3.2 @Import(MyImportBeanDefinitionRegister.class)

第二种形式@Import(MyImportBeanDefinitionRegister.class)

传递了一个bean定义注册器,这个注册器的具体内容如下:

public class MyImportBeanDefinitionRegister implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        RootBeanDefinition aDef = new RootBeanDefinition(A.class);
        registry.registerBeanDefinition("a", aDef);
    }
}

这个注册器实现了ImportBeanDefinitionRegistrar接口,并且重写了里面的registerBeanDefinitions方法

看他做了什么事:创建了一个新的bean定义,他的类型就是A,然后把这个bean定义注册到BeanDefinitionMap(还记得吧!)里面,key值我们可以人为设置,这里就设置成"a"

这样在传递一个注册器的时候,我们就可以把注册器中新增的bean定义注册进来使用

6.3.3 @Import(MyImportSelector.class)

可以看到,这种使用方式就是我们刚才的注解中使用的方式

他传递了一个叫MyImportSelector的类,这个类依然是我们自己定义的,具体内容如下:

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        // 导入配置类
        return new String[]{"config.MyConfig"};
    }
}

这个类实现了ImportSelector接口,并且重写了selectImports方法,返回一个字符串数组

我们可以看到,返回的字符串数组中是我们要导入类的全类名

这个Importer返回的类如果是组件bean对象,就会被加载进来使用;如果是一个配置类,就会加载这个配置类

第三种和第二种的区别是第三种可以一次性写很多类,而且比较简洁,只需要清楚类的全包名即可。而第二种方式需要自己清楚包类名,手动创建bean定义,然后手动加入BeanDefinitionMap。

6.4 例子的研究

我们打开MyImportSelector,发现里面赫然写着几个大字:


return new String[]{"config.MyConfig"};

然后我们找到config.MyConfig类,发现这个类竟然就是我们刚才写的JavaConfig版本的配置文件:

@Configuration
public class MyConfig {
    @Bean
    public A a(){
        return new A();
    }
    @Bean
    public B b(){
        return new B();
    }
}

加载这个MyConfig配置类,就相当于加载了A和B两个Bean定义

喂!你是不是搞我!绕了一大圈,怎么还是加载这个配置文件啊!这个配置文件明明就是我自己写的。

7da47f2237a4fc9897e0a06b0edf45ba.png

总结一下,我们这个例子大概绕了这些过程:

6.5 将偷懒进行到底

"没有会偷懒的人解决不掉的问题“ —— 鲁迅

上面的例子也没有多大优化啊,我怎么觉得更加麻烦了?不但绕了一大圈,定义了许多新东西,到最后还是加载了我写好的JavaConfig类,说到底我不是还在写javaConfig类吗…

但是你注意到没有:有了上面的机制,我只需要把JavaConfig类写一次,然后放在某个地方,在MyImportSelector中加入这个地方的全包名路径,下次用的时候直接导入最顶层的MyAutoConfig类,所有有关这个部件我需要的东西,就全部自动整理好了,甚至比鼠标点点点添加代码模板还要快!

我突然有了个很棒的想法,不知道你有了没有 。

如果你开始有点感觉了,就会自然提出另一个问题:我这样做确实可以提高效率,但是一段代码里写入我自己定制的内容,每次更改起来不是太费劲了吗?

想到这里,我就不禁回想起使用JDBC的时候,在代码里改SQL语句的痛苦了,那真是生不如死…这种情况就构成了硬编码的行为,是不好的。

我们自然会想到:要是我创建一个配置文件properties来专门保存我这个需求所使用的bean对象,然后使用的时候在MyImportSelector中读取配置文件并且返回全包名,不就更加nice了吗?

于是MyImportSelector中的代码又改成了下面这样:

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        Properties properties = MyPropertyReader.readPropertyForMe("/MyProperty.properties");
        String strings = (String) properties.get(MyEnableAutoConfig.class.getName());
        return new String[]{strings};
    }
}

其中MyPropertyReader是我们自己新创建的用于读取properties文件的工具类

之所以要自己再定义这样一个工具类,是为了以后在其中可以做一些其他操作(比如:去重、预检查)

public class MyPropertyReader {
    public static Properties readPropertyForMe(String path){
        Properties properties = new Properties();
        try(InputStream sin = MyPropertyReader.class.getResourceAsStream(path)){
            properties.load(sin);
        }catch (IOException e){
            e.printStackTrace();
            System.out.println("读取异常...");
        }
        return properties;
    }
}

我们的配置文件里面这么写:


anno.MyEnableAutoConfig=config.MyConfig

可以看到,key是注解@MyEnableAutoConfig的类名,也就是根据这个注解,就会导入后面的MyConfig这个Bean,这个Bean就是我们的配置文件

如此一来我们读取这个配置文件,然后加载跟这个注解名称相符的value(即JavaConfig配置文件),就相当于我们在代码里手写的"config.MyConfig",只不过现在的形式已经发生了巨大的变化:我们添加或者删除一个配件,完全只需要修改MyProperty.properties这个配置文件就行了!

至此,无论是添加或者删除组件,无非是在配置文件中加上或者删除一行的问题了。

让我们在更新之后运行程序,可以看到成功拿到了配置文件的全类名

程序的运行当然也是没问题的:


A(name=我是AAA, b=B(name=我是BBB))

到此,我仿佛又领悟了一些东西。。。

我的配置文件好像活了,在我需要的时候他会出现,在我不需要的时候只需要在配置文件里面给他”打个叉“,他自己就跑开了

相关文章
|
5天前
|
消息中间件 Java Maven
深入理解Spring Boot Starter:概念、特点、场景、原理及自定义starter
深入理解Spring Boot Starter:概念、特点、场景、原理及自定义starter
|
4天前
|
开发框架 Java 开发者
Spring Boot中的自动装配原理
Spring Boot中的自动装配原理
|
3天前
|
Java
SpringBoot起步依赖原理分析
SpringBoot起步依赖原理分析
|
4天前
|
Java Linux 程序员
技术笔记:Spring生态研习【五】:Springboot中bean的条件注入
技术笔记:Spring生态研习【五】:Springboot中bean的条件注入
|
5天前
|
Java 应用服务中间件 Spring
SpringBoot条件注解原理
可以看到isPresent的逻辑是通过FilteringSpringBootCondition.resolve(className, classLoader); 来尝试加载该类,如果能正常加载,则代表该类存在,如果不能则代表该类不存在。
16 0
|
6天前
|
Java
手写SpringBoot(四)之bean动态加载
可以看到只有userApplication tomcatWebServer init打印,没有mySpringboot tomcatWebServer init打印,证明spring-boot只加载了用户定义的那个tomcatWebServer
7 0
|
10天前
|
Java
springboot Test 测试类中如何排除一个bean类
springboot Test 测试类中如何排除一个bean类
8 0
|
11天前
|
Java Spring
我是如何做到springboot自动配置原理解析
我是如何做到springboot自动配置原理解析
|
3天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的大学生心理健康诊断专家系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的大学生心理健康诊断专家系统的详细设计和实现(源码+lw+部署文档+讲解等)
14 0
|
3天前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的大学生成绩管理系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的大学生成绩管理系统的详细设计和实现(源码+lw+部署文档+讲解等)
13 0