SpringBoot主张一种零配置的开发方式,特别是第三方框架的整合往往只需要在项目中引入相应的starter,就能做到开箱即用的效果。以整合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 注解可以令 @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 注解指定注入实例。
翻看@EnableConfigurationProperties 注解源代码,发现其引用了@Import注解。
@Import注解的作用是将Bean加入到Spring容器中,其支持3种形式的导入。
修改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容器
使用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容器中。
相比于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中出现配置项的提示信息,项目需要导入spring-boot-configuration-processor依赖。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-configuration-processorartifactId>
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组件可以在被其他项目引用时提供注册好的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.itgroupId>
<artifactId>spring-boot-starterartifactId>
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中的配置也生效。