众所周知,SpringBoot能简化大部分配置过程,而且快速搭建起应用程序。主要的功劳在于自动装配和起步依赖。
起步依赖 (starter)
把一些具有某些功能的自研或三方依赖坐标打包到一块,使用的时候不需要再导入包含的依赖。一般情况下,官方starter命名格式一般是 spring-boot-starter-* ,比如 spring-boot-starter-web 、spring-boot-starter-test 。自定义starter一般的命名格式是 项目名称-starter。
比如spring-boot-starter-web依赖,其中包含了很多有关于web的依赖坐标,如下图所示:
自动配置
不需要手动配置,由starter帮我们完成,就跟装修房子差不多,SpringBoot不仅给我们送装修工具,而且还顺带把装修的活也干了,让我们享受了一条龙服务,我们不需要考虑整体过程,只需要装修完以后直接使(添加业务逻辑)就行。
要想自定义一个起步依赖,那么就得先知道SpringBoot是怎么进行自动配置的:
首先就是启动类,这个类不仅是应用程序的入口,还具有着其他隐藏功能。下面是一个普通的启动类:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class);
}
}
进入@SpringBootApplication注解,发现它是一个组合注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM,
classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
@AliasFor(annotation = EnableAutoConfiguration.class)
Class<?>[] exclude() default {};
@AliasFor(annotation = EnableAutoConfiguration.class)
String[] excludeName() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
Class<?>[] scanBasePackageClasses() default {};
}
先简单解释一下另外两个注解@SpringBootConfiguration和@ComponentScan。前面这个就相当于@Configuration注解,主要功能就是标记这个类,而后面这个主要功能就是开启自动扫描包。
其中重点就是@EnableAutoConfiguration注解,主要功能就是开启SpringBoot的自动配置功能,进入源码可以看到:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
发现这个注解也是个组合注解,简单说一下@AutoConfigurationPackage注解,这个注解的主要功能就是把和启动类同级以及子级包中带有@Configuration、@Component的注解的类注册到容器里。
重点是@Import(AutoConfigurationImportSelector.class)注解,其中@Import注解的作用就是把组件导入到容器里,进一步进入这个组件类的源码里:
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
protected AutoConfigurationEntry getAutoConfigurationEntry(
AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata,
attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
其中有一个selectImports方法,其中loadMetadata方法,就是让类加载器去找到元数据地址,然后调用了getAutoConfigurationEntry方法,把类路径下所有jar包的spring.factories文件里键为EnableAutoConfiguration的值取出来,这些值也就是全限定名,SpringBoot会通过反射机制来添加到容器里。获取spring.factories里值的过程是通过getCandidateConfigurations方法来实现的。
我们在类路径下找一下这个spring.factories文件:
自定义起步依赖
了解了自动配置的过程,自定义一个起步依赖,项目名称以starter结尾。
配置POM
需要在POM文件里导入两个依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
定义属性类
把未来需要调用的属性封装到属性类里:
@Data
@ConfigurationProperties(prefix = "myproperties") // 属性配置的前缀
public class MyProperties {
private boolean ready;
private String info;
}
定义业务类
自定义starter的主要目的就是把某些业务抽取出来,方便其他地方能直接调用:
public class MyService {
@Autowired
private MyProperties myProperties;
// 具体业务
public String doSome() {
return "you can" + myProperties.getInfo();
}
}
定义自动配置类
SpringBoot会先读取 META-INF\spring.factories 文件,之后再根据自动配置类里的注解来决定是否生效。以下就是自动配置类的实现:
/**
* @Configuration: 声明该类为配置类
* @EnableConfigurationProperties: 绑定属性类,并放入容器
* @ConditionalOnProperty: 检查指定属性是否具有特定值的条件
*/
@Configuration
@EnableConfigurationProperties(MyProperties.class)
@ConditionalOnProperty(prefix = "myproperties",name = "ready", havingValue = "true")
public class MyAutoConfigure {
@Bean(name = "myService")
public MyService myConfigBean() {
return new MyService();
}
}
因为会读取 META-INF\spring.factories 文件,所以需要在resource文件夹底下创建这个文件(文件名必须要一致):
org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
com.doyens.file.config.MyAutoConfigure
starter编写完之后,用maven命令打包到本地仓库,就可以开始测试了。
测试
在另一个项目里导入starter依赖的坐标:
<dependency>
<groupId>com.auto.test</groupId>
<artifactId>my-auto-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
在配置文件定义属性:
myconfigbean:
ready: true
info: 666
编写测试类,需要设置由SpringBoot来运行测试方法:
@SpringBootTest(classes = DemoApplication.class)
@RunWith(SpringRunner.class)
public class StarterTest {
@Resource(name = "myService")
private MyService myService;
@Test
public void test() {
String str= myService.doSome();
System.out.println(str);
}
}
最后,测试结果: