简介
Java程序员和Spring息息相关,Spring在为广大Java程序员提供了极大的便捷性的同时也带来了极多的配置文件,SpringBoot
在这样的环境下应运而生,它以约定大于配置的方式让Java程序员在繁杂的配置文件中脱离出来,让Java程序员只用按需引入各种Starter
并加载默认配置,几乎做到开箱即用,SpringBoot
能提供这样的能力依赖于它的自动配置模式,接下来就简单下SpringBoot
自动配置以及自己动手实现一个SpringBoot Starter
并在其中介绍一些小技巧。
SpringBoot 自动配置
一个简单的SpringBoot
如下:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
其中最主要的就是@SpringBootApplication
注解,那么它有什么神奇之处呢,我们进入@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 {}
@SpringBootApplication
其实是一个复合注解包括@SpringBootConfiguration
、@EnableAutoConfiguration
、@ComponentScan
,我们今天主要讨论到是自动配置,所以望文生义肯定与@EnableAutoConfiguration
注解息息相关,我们继续进入@EnableAutoConfiguration
注解
@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
和@Import
,@Import
注解的主要功能就是将Class
导入到IOC
容器中,接下来我们就介绍一下@EnableAutoConfiguration
注解实现的两个功能
@AutoConfigurationPackage
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
@AutoConfigurationPackage
利用@Import
注解把AutoConfigurationPackages.Registrar.class
导入到IOC
容器,AutoConfigurationPackages.Registrar.class
主要的功能就是扫描主类所在包及其子包以及basePackages
和basePackageClasses
配置的包及其子包下的Bean
加入IOC
容器,里面的代码相对简单,感兴趣的小伙伴可以打开源代码看一看
@Import(AutoConfigurationImportSelector.class)
@Import(AutoConfigurationImportSelector.class)
将AutoConfigurationImportSelector.class
导入IOC
容器,AutoConfigurationImportSelector.class
里面有个方法
protected AutoConfigurationEntry getAutoConfigurationEntry(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 = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
看方法也能大致知道它的功能是获取自动配置的Entry
,其中主要的方法是getCandidateConfigurations
,我们继续进入
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;
}
其实在这通过方法中的Assert
提示消息
No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.
也能推测出SpringFactoriesLoader.loadFactoryNames
方法是通过META-INF/spring.factories
文件查找auto configuration classes
,我们继续进入SpringFactoriesLoader.loadFactoryNames
方法
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();//org.springframework.boot.autoconfigure.EnableAutoConfiguration
return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
在方法尾部loadSpringFactories(classLoaderToUse)
返回了一个Map
然后通过factoryTypeName
获取了value
,而factoryTypeName
的值在此时正是org.springframework.boot.autoconfigure.EnableAutoConfiguration
,我们继续进入loadSpringFactories
方法
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = new HashMap<>();
try {
//FACTORIES_RESOURCE_LOCATION="META-INF/spring.factories"
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
...
}
可以看到根据常量FACTORIES_RESOURCE_LOCATION
扫描jar
路径下的所有URL
,而FACTORIES_RESOURCE_LOCATION
的值是等于META-INF/spring.factories
的,所以该方法是将jar
路径下的所有META-INF/spring.factories
配置文件读取到Map
对象中,再通过key=org.springframework.boot.autoconfigure.EnableAutoConfiguration
获取到value
也就是需要自动配置的Class
关于自动配置的部分就讲完了,下面我们介绍如何自定义一个SpringBoot Starter
自定义SpringBoot Starter
首先新建一个SpringBoot Maven
项目,比较简单这里就不多做讲解了,需要注意的点是artifactId
通常为xxx-spring-boot-starter
,因为官方提供的starter
以spring-boot-starter-xxx
命名,所以官方建议自定义的starter
用xxx-spring-boot-starter
命名与官方做一个区分的同时也保持一定的命名规范,当然如果只是公司内部或者个人的一些组件或工具也可以用xxx-component
或其他名字来命名,这里想表达的意思是在团队协作中最好保持一定的命名规范,让团队其他成员减少不必要的理解成本或歧义。
代码结构如下:
根据上面讲的自动配置流程,我们需要在resources
目录下创建META-INF/spring.factories
文件,同时新建自动配置类DemoAutoConfiguration.java
,
spring.factories
内容如下
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.demo.autoconfiguration.DemoAutoConfiguration
上面讲过获取自动配置是以org.springframework.boot.autoconfigure.EnableAutoConfiguration
为key
所以我们需要这样配置,至此最简单的starter
已经实现了,但是这个starter
没提供任何能力,所以它是毫无意义的,我们接下来模拟一个发送邮件的功能来丰富我们的starter
。
我们新增一个properties
配置类
@Getter
@Setter
@ConfigurationProperties(prefix = "mail")
public class DemoProperties {
private String address;
private String msg;
}
修改DemoAutoConfiguration.java
@EnableConfigurationProperties({DemoProperties.class})
public class DemoAutoConfiguration {
}
关于@EnableConfigurationProperties
可以查看我另一片文章,@EnableConfigurationProperties使用技巧
我们创建一个service
,同时修改DemoAutoConfiguration
@AllArgsConstructor
@Slf4j
public class MailService {
private DemoProperties properties;
public void send(){
log.warn("mail address is {}, msg is {}", properties.getAddress(), properties.getMsg());
}
}
@EnableConfigurationProperties({DemoProperties.class})
public class DemoAutoConfiguration {
@ConditionalOnMissingBean
@Bean
MailService mailService(DemoProperties properties){
return new MailService(properties);
}
}
可以发现,我们没有用@Service
注解来标记MailService
,而是在DemoAutoConfiguration
里面手动的注册MailService
为Bean
,为什么要大费周章的自己手动注册呢?在这有个编码习惯,当你对外提供服务时,尽量让自己的服务处在可控制的状态,以防与用户预期产生差异性,在这只是一个很简单的例子,如果是一个非常复杂的模块或者Starter
再与其他服务进行交互时这是非常有必要的,比如这个例子,只有当DemoAutoConfiguration
被自动配置时MailService
才会被IOC
容器管理,如果采用@Service
注解,用户刚好扫描到你的包,那即使你的自动配置是没启用的MailService
也会被IOC
容器管理,这在大部分时候可能没啥影响,但是积少成多你的系统将越来越不可控。
接下来我们执行mvn clean install
这样一个新鲜的Starter
就生成了,在另一个项目中引入该Starter
<dependency>
<groupId>com.example</groupId>
<artifactId>demo-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
在application.yml中配置
mail:
address: test@foxmail.com
msg: hello
在启动类类做个简单的测试
@SpringBootApplication
public class Application {
@Autowired
MailService mailService;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@EventListener
public void ready(ApplicationReadyEvent event){
mailService.send();
}
}
启动项目可以得到消息
mail address is test@foxmail.com, msg is hello
至此SringBoot Starter
简单示例就完成了,下面介绍一些小技巧,在大部分框架中会做一些开关,比如enabled
配置,我们在properties中加入一个boolean
类型的字段
@Getter
@Setter
@ToString
@ConfigurationProperties(prefix = "mail")
public class DemoProperties {
private boolean enabled;
private String address;
private String msg;
}
@ConditionalOnProperty
在自动配置类上加入@ConditionalOnProperty
注解,prefix
表示前缀,value
表示值的字段名,havingValue
表示值为什么时生效,matchIfMissing
表示默认值
@EnableConfigurationProperties({DemoProperties.class})
@ConditionalOnProperty(prefix = "mail", value = "enabled", havingValue="true", matchIfMissing = true)
public class DemoAutoConfiguration {
@ConditionalOnMissingBean
@Bean
MailService mailService(DemoProperties properties){
return new MailService(properties);
}
}
这样我们就可以通过配置控制我们的自动配置是否生效
mail:
address: test@foxmail.com
msg: hello
enabled: false
如果现在改为false启动项目会报错,这是因为我们的自动配置设置为false后不会加载自动配置类,也就不会注入MailService
Field mailService in com.example.demo.Application required a bean of type 'com.example.demo.service.MailService' that could not be found.
The injection point has the following annotations:
- @org.springframework.beans.factory.annotation.Autowired(required=true)
@Import
前面我们讲过@Import
注解的作用,我们也可以利用@Import
注解也实现自动配置的控制,创建注解@EnableMail
,只做一件事就是引入自动配置类
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.TYPE)
@Import({DemoAutoConfiguration.class})
public @interface EnableMail {
}
删除spring.factories
的配置,然后重新install Starter
,在测试项目中加入注解@EnableMail
@SpringBootApplication
@EnableMail
public class Application {
@Autowired
MailService mailService;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@EventListener
public void ready(ApplicationReadyEvent event){
mailService.send();
}
}
启动项目,同样能得到下面的消息,这也是部分框架采用的自动配置的方式
mail address is test@foxmail.com, msg is hello
依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
转载来源:https://juejin.cn/post/6974721906348359693