Why?(为什么有这篇文章)
记得当我刚开始接触springboot的时候,官网的Demo让我影响深刻。我只需要在pom文件里面添加web相关的依赖,我就自然而然地获得了以前用Springmvc需要手动配置地一堆bean的声明和依赖关系。之后随着参加的面试多了,面试官经常问到的两个问题就是:“你了解SpringAutoconfigure的原理吗?你有自己动手写过自己的starter吗?”于是乎,我就觉得这东西是的好好了解一下,就有了这篇文章。
What?(spring-boot-starter是个什么东西,解决了什么问题)
starter在英语中一般用在点菜的时候,叫做开胃小菜,而后面的主菜叫main course. 所以这也是很形象的一种比喻了,把某个部件比喻成小菜,而整个工程就是我们的main course. 那么这个spring-boot-starter的作用就是让你在构建main course前,把一些小组件(starter)很方便地整合进来。
我们可以在github上找到很多地starter,比如spring-boot-web-starter, spring-boot-rocketmq-starter… 这些starter引入之后,我们就能根据starter里面决定加载bean地条件来很方便地配置我们相关地组件。
How? (如何写一个自己地spring-boot-starter)
整体步骤
- 新建maven项目
- 引入自动配置依赖
- 编写实现类
- 编写配置类
- 写入spring.factories
- mvn install 生成jar包
- 在项目中使用刚刚的starter
步骤详解
新建maven项目 略
引入自动配置依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.kobelee</groupId>
<artifactId>test-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>1.5.13.RELEASE</version>
</dependency>
</dependencies>
</project>
编写实现类
这里我写一个简单地实现类,这个实现类就一个方法。传入一个字符串,把这个字符串加上yml中配置的某个项,然后返回。注意这里的构造方法是需要传入一个字符串str, 并且赋值给configStr.
package cn.kobelee.util.component;
public class SimpleStringConcater {
String configStr;
public String concat(String str){
return str + configStr;
}
public SimpleStringConcater(String str){
configStr = str;
}
}
编写配置类
上面的代码就传入一个字符串str然后和configStr拼接后返回。那么configStr怎么来呢?是的,从配置文件读取。那么既然要从配置文件读取,我就得写一个ConfigurationProperites的类。
package cn.kobelee.util.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("kobelee.util")
public class SimpleStringConcaterProperties {
private String configStr;
public String getConfigStr() {
return configStr;
}
public void setConfigStr(String configStr) {
this.configStr = configStr;
}
}
上面这段代码其实就只是声明了一个configStr的属性,这个属性会从配置文件的kobelee.util.configStr读取(我们也可以用@Value注解进行配置)。这一段配置其实不是必须的,但是我们写starter的目的就是为了方便我们在使用的时候,只需要在配置文件里面写入配置信息,就可以根据配置信息自动实例化bean.比如我们可以写一个数据库连接相关的starter,那么可以把数据库连接的信息放入配置文件,用一个这样的配置类进行载入。
到目前为止,我们的实现已经写好了实现类和配置文件类。但是,还有两个问题:
1. 这个SimpleStringConcaterProperties.configStr还是没有注入到SimpleStringConcater.configStr中啊。
2. 这个bean到底要什么情况下进行实例化还没有相关代码指定。
于是,我们还需要一个autoconfigure的类。
package cn.kobelee.util.config;
import cn.kobelee.util.component.SimpleStringConcater;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//这是一个Configuration类
@Configuration
//只有当classpath下存在SimpleStringConcater.class这个类的时候,才实例化这个Configuration bean
@ConditionalOnClass(SimpleStringConcater.class)
//为SimpleStringConcaterProperties.class启动配置
@EnableConfigurationProperties(SimpleStringConcaterProperties.class)
public class SimpleStringConcaterAutoconfigure {
@Autowired
private SimpleStringConcaterProperties properties;
//声明这是一个bean
@Bean
//当容器里缺失这个bean的时候才实例化这个bean
@ConditionalOnMissingBean
//当配置文件节点下,有一个kobelee.util.enabled有一个true的值的后,才实例化这个bean
@ConditionalOnProperty(prefix = "kobelee.util", value = "enabled", havingValue = "true")
public SimpleStringConcater simpleStringConcater(){
return new SimpleStringConcater(properties.getConfigStr());
}
}
代码逻辑很简单,就是返回一个SimpleStringConcater的bean,但是需要有条件。我们可以看到simpleStringConcater()方法上面两个注解:
@ConditionalOnMissingBean 和 @ConditionalOnProperty
和Conditional有关的注解,在官网上我们可以看到有如下这些:
Class condition:
@ConditionalOnClass classpath中有某个类才实例化此bean
@ConditionalOnMissingClass classpath中
Bean condition:
@ConditionalOnBean 有了某个bean才实例化此bean
@ConditionalOnMissingBean 缺失某个类型的bean才实例化此bean
Property condition:
@ConditionalOnProperty 配置文件满足某些条件才实例化
Resource condition:
@ConditionalOnResource 有某个资源才实例化(/home/test.txt表明需要home路径下有个test.txt)
Web Application Conditions:
@ConditionalOnWebApplication 是web应用才实例化
@ConditionalOnNotWebApplication 不是web应用才实例化
SpEL Expression Conditions:
@ConditionalOnExpression:根据SpEL表达式的返回结果来决定是否实例化
参考官网文档:https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/html/spring-boot-features.html#boot-features-condition-annotations
编写spring.factories
在resources/META-INF下新建
org.springframework.boot.autoconfigure.EnableAutoConfiguration=cn.kobelee.util.config.SimpleStringConcaterAutoconfigure
这个文件的目的是让springboot知道你需要自动配置的类都有哪些。如果你这个starter里面有多个Autoconfigure,可以采用这种方式隔开:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.kobelee.FirstAutoConfiguration,\
cn.kobelee.SecondAutoConfiguration
mvn install 略
到这一步,我们的自定义的starter已经创建好了。接下来我们去另一个springboot项目里面,去使用刚刚生成的test-spring-boot-starter-1.0-SNAPSHOT.jar
在项目中使用自定义starter
先在新项目中引入刚刚的jar包pom依赖:
<dependency>
<groupId>cn.kobelee</groupId>
<artifactId>test-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
application.yml里面定义我们starter需要的属性
kobelee:
util:
enabled: true
configStr: I'm configStr
在新项目中引用SimpleStringConcater并且@Autowired注入。
package cn.kobelee.demo.controller;
import cn.kobelee.util.component.SimpleStringConcater;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
@RestController
@RequestMapping("/")
public class TestController {
@Autowired
private SimpleStringConcater simpleStringConcater;
@RequestMapping("test")
public String test(){
String concatStr = simpleStringConcater.concat("I'm param Str. ");
System.out.println(concatStr);
return concatStr;
}
}
此时,我们回顾一下starter里面的simpleStringConcater的实例化条件:
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "kobelee.util", value = "enabled", havingValue = "true")
容器中确实没有这个bean,并且我在application.yml里面也在kobelee.util.enabled里面配置了true.
启动项目,访问Controller对应路径。发现控制台和浏览器返回都有了打印输出:
I'm param Str. I'm configStr
Conclusion 总结
本文以一个简单的StringConcat为例子,介绍了自定义spring-boot-starter涉及到的几个关键步骤。包括:
* pom依赖
* 实现类(业务逻辑)
* 配置类Properties和AutoConfiguration
* spring.factories
* mvn install 然后使用
适用场景
当需要有某些通用功能(比如MQ的使用、数据库的使用、和短信系统的接入等),而并不是特别个性化的业务,我们可以考虑做成一个starter来方便代码的复用。尤其当在编写架构层面的代码的时候,spring-boot-starter更是将这一点体现得淋漓尽致。
不时之需
spring.io官方提供了很多AutoConfigure,可以加入浏览器收藏夹以备不时之需。
https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/html/appendix-auto-configuration-classes.html#auto-configuration-classes
当考虑新建starter的时候,更好的方式是去github上搜索一下,看是否已经有“好事之徒”提前铺好了路。