spring-boot 自定义starter

362 阅读2分钟

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,放到集合中;

参见

徒手撸一个Spring Boot中的starter

本文示例github-源码

springboot中META-INF/spring.factories解析加载类