1. 介🐱绍
spring-boot 基于 约定大于配置 的思想,一般仅需引入 xxx-starter 后,再加少量配置即可完成对 xxx 框架的整合;
那么这个是个啥原理呢🤔?,我们能不能自己也搞一个呢?本文即由此展开;
先手撸一个 消息服务starter ,然后debug 看运行流程;
2. 手🐦撸starter
2.1 示例需求
提供一个可以快速集成的消息服务(包括但不限于 短信、邮件、微信公众号、电话 等);
要求:
1)配置简单;
2)可以单独配置(例如仅配置短信);
2.2 开🚀干
注:这里我仅挑一个 短信进行实现,其他类比即可
初始化
创建一个 spring-boot 项目
导🍰包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
cod💪ing
定义短信所需的认证配置类
@Data
@ConfigurationProperties(prefix = "msg")
public class MsgProperties {
private String accessId;
private String accessSecret;
}
定义短信发送服务
public class MsgService {
private Logger logger = LoggerFactory.getLogger(MsgService.class);
private MsgProperties properties;
public MsgService() {
}
public MsgService(MsgProperties properties) {
this.properties = properties;
}
/**
* 发送短信
* @param templateCode 短信模板编码
* @param phone 手机号
* @param params 替换字符数组
*/
public void sendMessage(String templateCode,String phone,String... params){
sendMessageOnXxPlatform(templateCode,phone,params);
}
/**
* 通过 xx 平台 发送短信
*/
public void sendMessageOnXxPlatform(String templateCode,String phone,String... params){
// 模拟第三方短信服务
if(StringUtils.hasText(properties.getAccessId()) && StringUtils.hasText(properties.getAccessSecret())){
logger.info("send msg by xx platform success templateCode = {} , phone = {} , params : {}",templateCode,phone,params);
}
}
}
自动配置类
@Configuration
@EnableConfigurationProperties(MsgProperties.class)
@ConditionalOnProperty(prefix = "msg",value = "enable")
public class MsgServiceAutoConfiguration {
@Autowired
private MsgProperties msgProperties;
@Bean
@ConditionalOnMissingBean(MsgService.class)
public MsgService msgService(){
return new MsgService(msgProperties);
}
}
在resources 目录下 创建 META-INF 目录, 在META-INF 下创建spring.factories文件
|-rescources
|-META-INF
|-spring.factories
spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.yi.auth.config.MsgServiceAutoConfiguration
使🏃🏻♀️用
1) 新建一个spring-boot 测试项目
2) 引包
<dependency>
<groupId>com.yiyi</groupId>
<artifactId>yiyi-msg-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
3 ) 配置必要属性
application.properties
app.id=yiyi-example-api
msg.enable=true
msg.access-id=12138
msg.access-secret=12138ss
4 ) controller 调用服务
@Autowired
private MsgService msgService;
@PostMapping("/send_msg")
public String sendMsg(){
msgService.sendMessageOnXxPlatform("code_12138","16602223926","1","2","3");
return "send msg success";
}
5 )开测
curl --location --request POST 'localhost:18083/send_msg'
日志打印:
com.yi.auth.service.MsgService : send msg by xx platform success templateCode = code_12138 , phone = 16602223926 , params : [1, 2, 3]
返回结果:send msg success
结论🌲: 完美
6 ) 关闭功能测试
application.properties文件中 msg.enable=false 即可;spring就不会加载MsgService;
3. 🔍内部实现
3.1 启动项目debug
可以使用本文示例yiyi-example 项目 ;
debug 后时序图如下:
3.2 核心加载逻辑如下
public final class SpringFactoriesLoader {
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
}
简述一下上述代码的目的:扫描程序加载的所有包,遍历每一个META-INF/spring.factories文件,找到需要加载的bean,放到集合中;