详解SpringBoot自动装配原理

x33g5p2x  于2021-12-07 转载在 Spring  
字(8.4k)|赞(0)|评价(0)|浏览(270)

一、从RedisAutoConfiguration源码分析自动装配

SpringBoot主张一种零配置的开发方式,特别是第三方框架的整合往往只需要在项目中引入相应的starter,就能做到开箱即用的效果。以整合Redis为例:

  1. 使用构建工具导入spring-boot-starter-redis依赖。
  2. 在application.yml中编写redis配置信息。
  3. 在项目直接注入RedisTemplate类的实例对象,依靠此对象完成对redis的操作。

以上过程中,我们并没有实例化RedisTemplate类型的对象,也就是说在项目导入spring-boot-starter-redis依赖后,会在Spring容器中自动注册一个RedisTemplate类的实例对象。

查看Redis自动配置类的源代码:

①注解 @Configuration 标注了本类是一个配置类。

②注解 @ConditionalOnClass 表示条件装配,当Spring容器中存在RedisOperations类实例时,该配置类才会生效。

⑤注解 @Bean 标注方法的返回值注册到Spring容器中。

⑥注解也是一个条件注解,当前Spring容器中没有name为redisTemplate的实例时,@Bean 才会生效(这样可以允许用户自己定义RedisTemplate 并注册到Spring容器,但是name一定要为redisTemplate)。

⑦注解 @ConditionalOnSingleCandidate 表示当Spring容器中有唯一一个 RedisConnectionFactory 实例对象或者有多个RedisConnectionFactory 实例对象,其中一个使用@Primary注解时(保证注入的唯一性,程序不会报错),才会让@Bean生效。这是因为 RedisTemplate 类对象必须依赖于 RedisConnectionFactory 进行构造,如果Spring容器中不存在RedisConnectionFactory类实例,则无法生成 RedisTemplate 类实例。

依靠这些注解,当Spring容器中没有 RedisTemplate 类实例时,自动创建一个 RedisTemplate 类实例并注册到Spring容器。

二、@EnableConfigurationProperties注解

@EnableConfigurationProperties 注解可以令 @ConfigurationProperties 注解的类生效,@ConfigurationProperties 注解的作用是批量注入有着相同前缀的属性(SpringBoot属性注入 ),@ConfigurationProperties 注解可以将application.yml文件中的属性注入到对象实例中,使用 @Component 注解可以将对象实例化并注入到Spring容器。

@EnableConfigurationProperties 注解也会令@ConfigurationProperties 注解标注的类实例化并注册到Spring容器(此处的作用相当于在类上同时写了@ConfigurationProperties和@Component)。

在spring-boot-starter模块中新建Member类,并设置好setter、getter方法(此处用lombok生成)。

package com.it.vo;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix = "com.it")
public class Member {
    private Integer id;
    private String name;
    private double salary;
}

新建MemberAutoConfiguration 自动装配类。

package com.it.config;

import com.it.vo.Member;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(value = Member.class) // Bean注册
public class MemberAutoConfiguration { // 自动装配类

}

建立启动类,启动SpringBoot程序。

@Slf4j
@RestController
@SpringBootApplication
public class StartSpringApp {

	@Autowired
    private Member member;
    
    public static void main(String[] args) {
        SpringApplication.run(StartSpringApp.class, args);
    }

    @RequestMapping("/member")
    public Member getMember() {
        log.info("member: {}", member.toString());
        return member;
    }
}

访问:http://localhost:8080/member

以上使用了@Autowired 注解直接注入了Member实例,当Spring容器中有多个Member实例时,可以用@Qualifier 注解指定注入实例。

三、@Import注解

翻看@EnableConfigurationProperties 注解源代码,发现其引用了@Import注解。

@Import注解的作用是将Bean加入到Spring容器中,其支持3种形式的导入。

  1. 类导入
  2. ImportSelector导入
  3. ImportBeanDefinitionRegistrar导入

3.1 类导入

修改MemberAutoConfiguration 类,将@EnableConfigurationProperties 注解替换为 @Import注解后执行,程序依然正常。

package com.it.config;

import com.it.vo.Member;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import(Member.class)
public class MemberAutoConfiguration {

}

也就是说在配置类上使用@Import(Member.class) 相当于实例化一个Member对象并注册到Spring容器,这一点和@EnableConfigurationProperties 是一样的,都等价于在Member类上直接使用 @Component 注解。@EnableConfigurationProperties 注解源代码上的 @Import 采用的就是这种形式。

打开@Import 注解源代码可以发现,其value接收Class数组,也就是说写在@Import注解中的类型都会被实例化并注册到Spring容器。

@Import({TestA.class,TestB.class,TestC.class})  // 将TestA,TestB,TestC实例化并注册到Spring容器

3.2 ImportSelector导入

使用ImportSelector可以实现将Bean批量注册到Spring容器。

ImportSelector是一个接口,要完成Bean注册的功能需要自定义其实现子类并覆写 selectImports 方法。

新建两个测试VO类:

package com.it.vo;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class UselessTypeA {
    public UselessTypeA() {
        log.info("UselessTypeA实例化...");
    }
}
package com.it.vo;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class UselessTypeB {
    public UselessTypeB() {
        log.info("UselessTypeB实例化...");
    }
}

新建 DefaultImportSelector 实现 ImportSelector 接口,selectImports 方法返回一个String[]数组,这个数组的内容是要注册的Bean信息(类全名),DefaultImportSelector 需要使用@Import注解导入后才会生效,将放在selectImports方法的Bean注册到容器中。

package com.it.selector;

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

public class DefaultImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.it.vo.UselessTypeA","com.it.vo.UselessTypeB"}; // 要注册Bean的类全名
    }
}

修改 MemberAutoConfiguration 类,@Import注解导入 DefaultImportSelector。

@Configuration
@Import({Member.class, DefaultImportSelector.class})
public class MemberAutoConfiguration {

}

启动项目,观察UselessTypeA、UselessTypeB 确实已经实例化了。

如果想知道UselessTypeA、UselessTypeB实例对象是否被注册到Spring容器中,则可以编写接口,打印出容器的所有实例对象(也可以导入spring-boot-starter-actuator后访问beans端点查看)。

@RestController
@RequestMapping("/beans")
public class BeanInfoAction {

    @GetMapping("/get")
    public List<String> getBeans() {
        List<String> beans = new ArrayList<>();
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MemberAutoConfiguration.class);
        String[] names = context.getBeanDefinitionNames();
        for (String name : names) {
            beans.add("[" + name + "]" + context.getBean(name).getClass().getSimpleName());
        }
        return beans;
    }
}

启动项目,访问:http://localhost:8080/beans/get 发现UselessTypeA、UselessTypeB确实注册到了Spring容器中。

3.3 ImportBeanDefinitionRegistrar导入

相比于ImportSelector方式,使用ImportBeanDefinitionRegistrar导入可以指定注册Bean的name属性。

新建DefaultImportBeanDefinitionRegistrar 类并实现ImportBeanDefinitionRegistrar 接口。

public class DefaultImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        RootBeanDefinition uselessBeanA = new RootBeanDefinition(UselessTypeA.class);
        RootBeanDefinition uselessBeanB = new RootBeanDefinition(UselessTypeB.class);
        registry.registerBeanDefinition("uselessBeanA", uselessBeanA);
        registry.registerBeanDefinition("uselessBeanB", uselessBeanB);
    }
}

DefaultImportBeanDefinitionRegistrar 类也需要 @Import注解导入,修改MemberAutoConfiguration 类

@Configuration
@Import({Member.class, DefaultImportBeanDefinitionRegistrar.class})
public class MemberAutoConfiguration {

}

重新启动容器,访问http://localhost:8080/beans/get 发现Bean名称被修改了。

四、application.yml配置提示

如果想要在application.yml中出现配置项的提示信息,项目需要导入spring-boot-configuration-processor依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

如果使用的是gradle构建工具,则需要禁止一系列的打包任务。

jar {enabled true} //允许当前模块打包为*.jar文件

// 不启用javaDoc任务
javadoc {enabled false}
javadocTask{enabled false}
// 不生成javadoc的*.jar文件
javadocJar{enabled false}
// 不执行springboot的打包任务
bootJar{enabled false}

dependencies { // 配置子模块依赖
    compile 'org.springframework.boot:spring-boot-starter-web'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
}

将项目打包后,会在build -> classes -> java -> main -> META-INF 文件夹下发现一个spring-configuration-metadata.json文件,这个文件就是用来描述application.yml中的提示信息。

重新进入application.yml文件,发现配置项的名称和类型都会正常提示。

五、自定义Starter组件

starter组件可以在被其他项目引用时提供注册好的Bean实例,spring-boot-starter-redis就是一个starter。

由于以上代码都编写在spring-boot-starter模块中,为了方便,这里将spring-boot-starter模块制作成starter,再新建一个starter-test模块负责引入spring-boot-starter模块。

修改MemberAutoConfiguration 类,注册一个List集合到容器中。

@Configuration
@Import({Member.class, DefaultImportBeanDefinitionRegistrar.class})
public class MemberAutoConfiguration {

    @Bean(name = "messages")
    public List<String> messages() {
        return List.of("hello", "spring-boot-starter","test array");
    }
}

再resources根路径下新建一个spring.factories文件,这个文件否则配置装配信息。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.it.config.MemberAutoConfiguration

以上配置指明,其他模块导入本模块后,可以依靠MemberAutoConfiguration类进行自动装配。

新建starter-test项目,导入spring-boot-starter模块作为依赖。

<dependency>
    <groupId>com.it</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

gradle项目:

compile this.rootProject.project('spring-boot-starter')

在starter-test项目中编写application.yml文件,此时提示信息正常显示。

server:
  port: 8080
com:
  it:
    id: 10
    name: NicholasGUB
    salary: 0.13

编写主启动类启动程序,观察日志发现Bean注册正常。

创建测试Controller,查看Member属性和List集合是否正常注入。

@RestController
@RequestMapping("/test/*")
public class TestController {

    private final List<String> messages;
    private final Member member;

    @Autowired
    public TestController(List<String> messages, Member member) {
        this.messages = messages;
        this.member = member;
    }

    @GetMapping("/beans")
    public Object getBeans() {
        Map<String, Object> result = new HashMap<>();
        result.put("messages", messages);
        result.put("member", member);
        return result;
    }
}

访问:http://localhost:8080/test/beans 发现Bean全部注册正常,application.yml中的配置也生效。

相关文章