[版权申明] 非商业目的注明出处可自由转载
出自:shusheng007
现在Java后端Spring是绝对的霸主,而基于Spring的SpringBoot已经成为使用Spring的首选方式。我第一次使用SpringBoot的时候我觉得它很神奇,我只是在maven项目的pom.xml
文件里面加个spring-boot-starter-web
的依赖,我就可以直接写Rest API了。但是有时我也很困惑:当我使用三方库时,有的只需要加上starter
就可以直接使用了,但有的又需要加上某个注解,有的还需要在application.properties
文件中配置属性,属性的名字还不能错,这一切都是怎么发生的呢?
最近有时间就总结一下吧,希望在提高自己技术水平的同时可以给那些迷茫的刚入行的同学们一些帮助,记住我的代号:ShuSheng007
经常听到SpringBoot采用的约定大于配置的思想,极大的简化了项目搭建的难度,那么什么是约定大于配置呢?
定义:
约定优于配置(Convention Over Configuration)也称作按约定编程,是一种软件设计范式。目的在于减少软件开发人员所需要做出的决定的数量,从而获得简单而又不失其中的灵活性的能力
说人话就是给默认值,所有可以配置的地方都给默认值,如果你不特别配置,那么我们就使用约定的默认值。这样就造就了SpringBoot项目零配置启动的奇观,其实是SpringBoot的开发者帮我们做了决定。
那SpringBoot是怎么为我们做决定的呢?这就要涉及到它的自动装配原理,让我们一起揭开它的神秘的面纱吧
SpringBoot在启动时会扫描外部引用 jar 包中的META-INF/spring.factories
文件,将文件中配置的类型信息加载到 Spring 容器中(就是按照配置的类型信息,new 对象丢到Spring容器中),并执行类中定义的各种操作。
所以说,如果你想让SpringBoot自动将你的库的某个类的实例丢到Spring容器中,就需要按照其要求提供META-INF/spring.factories
文件。
文件内容长下面这样:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ ...
第一行是固定的,表示下面列出的自动装配类都是要被扫描的,然后根据条件进行自动装配。
原理非常简单,但如果不看一下具体的实现,相信你还是处于懵逼状态,面试的时候一下就让人家问住了,大家都这样不只是你,所以不要不好意思,哈哈。
SpringBoot的自动配置代码都在你项目External Libraries
里的org.springframework.boot:spring-boot-autoconfigure:xxxx
依赖里
其中
xxxx
为你使用的springboot的版本号,我使用的是2.5.4
。
我们可以在其jar包的META-INF
文件夹中找到一个spring.factories
文件,可以看到里面有非常多要自动配置的类
# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ ... org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\ ...
这些自动配置的类很多都是SpringBoot官方提供的那些starter
里的配置类,例如我们熟悉的MongoDB的配置类如下
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
我们可以看到,这里有非常多的配置类,难道SpringBoot在启动的时候都要将这些类里的@Bean
实例化到Spring容器中吗?人家有那么傻吗?你都不使用MongoDb他给你配置一个你不得骂娘吗?
那SpringBoot是如何确定哪些配置类该使用,哪些该忽略呢?答案就是使用各种 ConditionalOnXXX
注解,也就是说当符合某些条件时才自动装配。
让我们以MongoDB的自动配置类来具体查看一下,下面是其部分代码,关键看此类上面的那些注解。
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(MongoClient.class) @EnableConfigurationProperties(MongoProperties.class) @ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDatabaseFactory") public class MongoAutoConfiguration { @Bean @ConditionalOnMissingBean(MongoClient.class) public MongoClient mongo(ObjectProvider<MongoClientSettingsBuilderCustomizer> builderCustomizers, MongoClientSettings settings) { return new MongoClientFactory(builderCustomizers.orderedStream().collect(Collectors.toList())) .createMongoClient(settings); } ... }
其中两个注解格外引人注目
@ConditionalOnClass(MongoClient.class) @ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDatabaseFactory")
@ConditionalOnClass(MongoClient.class)
表示当SpringBoot项目的类路径中存在MongoClient
时才会自动装配此配置类。这什么意思呢?意思就是你得引入MongoDB的的starter才能引入MongoClient
,SpringBoot才会自动装配此配置类。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency>
如果不引入MongoDB的starter,MongoClient 会爆红,但是却没问题,有没有想过为什么?因为@ConditionalOnClass通过字节码来加载此Class的,允许其不存在类路径里。
@ConditionalOnMissingBean(type = "org.springframework.data.mongodb.MongoDatabaseFactory")
这个注解又是什么意思呢?意思就是当你不仅引入了MongoDB的starter,但同时你使用了自己的MongoDatabaseFactory
那么SpringBoot也会放弃帮你自动装配,而使用你自己的配置。这就是比较高级的用法了,你比较牛逼,你知道如何配置使MongoDB可以更好的工作,所以你不需要SpringBoot帮你做决定,正所谓:我命由我不由天!
/** * Created by ShuSheng007 * <p> * Author shusheng007 * Date 2021/9/17 19:08 * Description */ @Configuration public class MyMongoDbConfig { @Bean public MongoDatabaseFactory mongoDatabaseFactory(){ ... } }
@EnableConfigurationProperties(MongoProperties.class)
这个注解其实对于我们大部分人意义更为巨大,因为我们大部分时间是在使用SpringBoot,而不是写SpringBoot。还记得你在使用SpringBoot时,经常要在application.properties
文件中对使用的库进行配置吗?不知道你们怎么样,反正我在刚接触SB的时候会经常抱怨:我C,我怎么知道他们的key是啥呢?为什么我只是在这配置了一下就影响到我的MongoDB了呢?这一切都是这个注解做的妖。
看到MongoProperties
那个类了吗?这个就是我们经常写的解析application.properties
文件配置的那个类
@ConfigurationProperties(prefix = "spring.data.mongodb") public class MongoProperties { /** * Default port used when the configured port is {@code null}. */ public static final int DEFAULT_PORT = 27017; /** * Default URI used when the configured URI is {@code null}. */ public static final String DEFAULT_URI = "mongodb://localhost/test"; /** * Mongo server host. Cannot be set with URI. */ private String host; /** * Mongo server port. Cannot be set with URI. */ private Integer port = null; ... }
是不是找到了熟悉的味道?看@ConfigurationProperties(prefix = "spring.data.mongodb")
说明MongoDB的配置都必须以spring.data.mongodb
开头,
至于具体的key叫什么,自己看下这个类的属性名称呗!在这里你还可以检索到某个功能使用哪个属性来配置!
自动装配的关键代码在org.springframework.boot.autoconfigure.AutoConfigurationImportSelector
类的如下方法中
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); }
这个方法通过读取过滤等操作后获得了一个要自动装配类的列表。
有了上面的理论知识,写一个starter已经变得很容易了,但是写好一个starter仍然有一定的挑战性。总体需要两步
META-INF/spring.factories
文件中加入需要自动装配的类一般情况下,我们的starter包括很多个互相依赖的项目,他们共同工作使得某个库可以正常工作。例如MongoDB可能要依赖其他类库,那么其他类库也会进入这个starter。例如上文提到的MongoDB的starter的依赖关系如下图所示,他们共同支撑了MongoDB的正常使用。
假设我们有一个库,这个库的功能是可以对外输出一句问候语,那我们就为这个类库写个starter,使得它可以集成到SpringBoot项目中。
创建一个名为 hello-spring-boot-autoconfigure的maven项目 ,最终的目录结构如下:
├── pom.xml ├── src │ ├── main │ │ ├── java │ │ │ └── top │ │ │ └── ss007 │ │ │ └── hellospringbootautoconfigure │ │ │ ├── config │ │ │ │ ├── HelloAutoConfiguration.java │ │ │ │ └── HelloProperties.java │ │ │ └── library │ │ │ └── ShuSheng007.java │ │ └── resources │ │ └── META-INF │ │ ├── additional-spring-configuration-metadata.json │ │ └── spring.factories │ └── test │ └── java │ └── top │ └── ss007 │ └── hellospringbootautoconfigure
其中,library/ShuSheng007.java
实际情况下应是在另一个库中的,就是我们要使用的那个库,假设叫library,然后这个库是要在稍后的starter项目中与此项目一起引入的。由于我们这里只是演示所以就直接放在autoconfigure项目中了。
第一步:在pom.xml
文件中引入如下依赖,后两个是可选的,其作用一会再说
<groupId>top.ss007</groupId> <artifactId>hello-spring-boot-autoconfigure</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> <version>2.5.4</version> </dependency> <!-- 在META-INF中生成属性配置的元数据给IDE,用于编辑属性文件时的代码提示,可选--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> <version>2.5.4</version> </dependency> <!-- 在META-INF中生成忽略配置元数据,加快自动配置的初始化速度,可选--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure-processor</artifactId> <optional>true</optional> <version>2.5.4</version> </dependency> </dependencies>
第二步:构建属性配置类(可选)
这一步主要是为了使我们的starter具有自定义的能力,即我们可以通过application.properties
文件对其进行配置
@ConfigurationProperties(prefix = "ss007.hello") public class HelloProperties { /** * 讲话者姓名 */ private String name = "ShuSheng007"; private String content = "Hello Spring Starter"; //省略getter和setter }
这里我们给出对讲话者与讲话内容进行自定义的能力,这两个值都给了默认值。
第三步:构建自动配置类
/** * Created by Ben.Wang * <p> * Author Ben.Wang * Date 2021/9/17 23:13 * Description */ @Configuration @ConditionalOnProperty(value = "ss007.hello.enabled",havingValue = "true") @ConditionalOnClass(ShuSheng007.class) @EnableConfigurationProperties(HelloProperties.class) public class HelloAutoConfiguration { private final HelloProperties helloProperties; public HelloAutoConfiguration(HelloProperties helloProperties) { this.helloProperties = helloProperties; } @Bean @ConditionalOnMissingBean public ShuSheng007 shuSheng007(){ return new ShuSheng007(helloProperties.getName(),helloProperties.getContent()); } }
这里值得注意的是,我使用了@ConditionalOnProperty(value = "ss007.hello.enabled",havingValue = "true")
作为条件,所以要想启用自动配置,还需要在application.properties
文件中配置ss007.hello.enabled=true
第三步:将自动配置类加入META-INF/spring.factories
中
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ top.ss007.hellospringbootautoconfigure.config.HelloAutoConfiguration
经过以上3步,自动配置项目就写完了,下一步创建starter项目,将依赖的各种库已经这个自动配置项加入即可。
starter项目就比较简单了,只包含一个pom.xml
文件,它只是将使用到的library与我们新键建立的hello-spring-boot-autoconfigure作为依赖引入到pom
文件中。
项目结构如下所示:
hello-spring-boot-starter └── pom.xml
pom.xml
内容如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns=...> <modelVersion>4.0.0</modelVersion> <groupId>top.ss007</groupId> <artifactId>hello-spring-boot-starter</artifactId> <version>1.0.0</version> <packaging>pom</packaging> <name>hello-spring-boot-starter</name> <description>hello-spring-boot-starter</description> <dependencies> <dependency> <groupId>top.ss007</groupId> <artifactId>hello-spring-boot-autoconfigure</artifactId> <version>1.0.0</version> </dependency> </dependencies> </project>
因为我们将library合并到了hello-spring-boot-autoconfigure,所以这里只引入它一个依赖就够了
这个不用我说了吧,毕竟天天都在用starter。
pom.xml
中引入依赖<dependency> <groupId>top.ss007</groupId> <artifactId>hello-spring-boot-starter</artifactId> <version>1.0.0</version> </dependency>
为了演示我加了一个开关属性,所以需要在application.properties
文件中加入ss007.hello.enabled=true
的配置,很多starter不需要。
现在可以直接注入ShuSheng007
类的实例,然后使用其方法了,因为其已经被SpringBoot启动时自动装配好了。我们实现CommandLineRunner
,让应用run起来时就调用我们的starter。
@SpringBootApplication public class HelloSbStarterUserApplication implements CommandLineRunner { public static void main(String[] args) { SpringApplication.run(HelloSbStarterUserApplication.class, args); } @Autowired private ShuSheng007 shuSheng007; @Override public void run(String... args) throws Exception { shuSheng007.say(); } }
输出:
ShuSheng007说:Hello Spring Starter
至此,一个简单的starter就已经写好了,但是还不专业,我们还可以使其更专业一点。
你是否注意到,当你在编辑application.properties
文件时,你的IDE会特别贴心的奉上提示。话说现在没有提示你还写的了代码吗?
如何让我们自己写的starter也具有如下的提示呢?
第一: 生成由@ConfigurationProperties
注解修饰的属性类的元数据。
这个比较简单,只要在我们的hello-spring-boot-autoconfigure项目中引入如下库即可
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> <version>2.5.4</version> </dependency>
这个库会自动根据@ConfigurationProperties
修饰的类生成metadata给IDE使用。这个生成的元数据文件是target/classes/META-INF/spring-configuration-metadata.json
。
{ "groups": [ { "name": "ss007.hello", "type": "top.ss007.hellospringbootautoconfigure.config.HelloProperties", "sourceType": "top.ss007.hellospringbootautoconfigure.config.HelloProperties" } ], "properties": [ { "name": "ss007.hello.content", "type": "java.lang.String", "sourceType": "top.ss007.hellospringbootautoconfigure.config.HelloProperties" }, { "name": "ss007.hello.name", "type": "java.lang.String", "description": "讲话者姓名", "sourceType": "top.ss007.hellospringbootautoconfigure.config.HelloProperties" }, // 下面这个是从additional-spring-configuration-metadata.json合并过来的 { "name": "ss007.hello.enabled", "type": "java.lang.Boolean", "description": "是否启动Hello的自动装配功能." } ], "hints": [] }
可见,我们在HelloProperties
类的java doc (讲话者姓名)也会被包含在元数据中给IDE读取。
第二: 生成其他属性的元数据
我们的项目中,除了HelloProperties
类以外,还有一个属性ss007.hello.enabled
,这个属性通过上面的方式是无法产生元数据的,进而导致IDE不提示,那么有没有办法解决这个问题呢?有,我们需要提供一个自定义的配置元数据,让spring-boot-configuration-processor
帮我们加到spring-configuration-metadata.json
文件中去。
在hello-spring-boot-autoconfigure项目的resources/META-INF
中添加一个additional-spring-configuration-metadata.json
文件
{"properties": [ { "name": "ss007.hello.enabled", "type": "java.lang.Boolean", "description": "是否启动Hello的自动装配功能." } ] }
即可。
我们知道,SpringBoot在自动装配时会通过各种过滤条件来确定某个类是否需要装配,这在装配类太多的时候会影响启动数据,我们可以将过滤条件提供给IDE,进而加快这个流程。
只需要在hello-spring-boot-autoconfigure项目中引入如下工具即可:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-autoconfigure-processor</artifactId> <optional>true</optional> <version>2.5.4</version> </dependency>
它会在target/classes/META-INF
文件中生成一个spring-autoconfigure-metadata.json
top.ss007.hellospringbootautoconfigure.config.HelloAutoConfiguration= top.ss007.hellospringbootautoconfigure.config.HelloAutoConfiguration.ConditionalOnClass=top.ss007.hellospringbootautoconfigure.library.ShuSheng007
不知不觉文章变得很长了,通过此文相信你已经彻底脱了SpringBoot Starter的底裤了…值此中秋佳节,竟然阴雨绵绵,只能窝在家中写文章。
首发及源码地址