在上一篇对Spring Boot官方Starter的深度解析中,我们剖析了其分类与底层机制。本文将聚焦实战,手把手教你开发一个短信服务Starter,通过集成阿里云短信能力,达成“引入依赖,简单配置,即刻调用”的目标。整个过程严格遵循Spring Boot官方开发规范,旨在帮助你掌握自定义Starter的全流程,以解决企业级开发中短信功能重复集成的痛点。
一、需求与设计:短信 Starter 的核心目标
我们要开发的sms-spring-boot-starter需满足以下核心目标,兼顾开箱即用的便捷性与未来扩展的灵活性:
- 开箱即用:引入依赖后,自动完成短信客户端的配置,开发者可直接注入
SmsService 使用;
- 配置灵活:支持通过
application.yml 或 application.properties 轻松配置密钥、签名、模板ID等参数;
- 条件生效:当用户未配置必要参数时,Starter自动失效;当用户自定义了
SmsService实现时,自动覆盖Starter提供的默认Bean;
- 易于扩展:设计上预留多厂商(如阿里云、腾讯云)接口,便于后续平滑扩展。
二、项目结构:严格遵循 Starter 开发规范
一个规范的自定义Starter项目结构是保证其兼容性与可维护性的基础。以下是推荐的标准结构:
sms-spring-boot-starter/
├── src/main/java/
│ └── com/example/sms/
│ ├── config/ # 自动配置类
│ │ └── SmsAutoConfiguration.java
│ ├── properties/ # 配置属性绑定
│ │ └── SmsProperties.java
│ ├── core/ # 核心功能层
│ │ ├── SmsService.java # 短信发送接口
│ │ ├── impl/
│ │ │ └── AliyunSmsServiceImpl.java # 阿里云实现
│ │ └── SmsVendorEnum.java # 厂商枚举
│ └── exception/ # 自定义异常
│ └── SmsException.java
├── src/main/resources/
│ └── META-INF/spring/
│ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
└── pom.xml # Maven依赖配置
三、核心代码实现:从配置到功能
1. 配置属性类:绑定 yml 参数
使用 @ConfigurationProperties 注解,将配置文件中的参数绑定到Java Bean,并提供校验与IDE智能提示支持。这是所有Spring Boot应用配置管理的核心机制之一。
package com.example.sms.properties;
import com.example.sms.core.SmsVendorEnum;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotBlank;
@Data
@Validated
@ConfigurationProperties(prefix = "sms")
public class SmsProperties {
// 是否启用短信服务
private boolean enabled = true;
// 短信厂商
private SmsVendorEnum vendor = SmsVendorEnum.ALIYUN;
// 阿里云AccessKey(必填)
@NotBlank(message = "sms.access-key不能为空")
private String accessKey;
// 阿里云AccessSecret(必填)
@NotBlank(message = "sms.access-secret不能为空")
private String accessSecret;
// 短信签名
@NotBlank(message = "sms.sign-name不能为空")
private String signName;
// 默认模板ID
private String defaultTemplateId;
// 阿里云区域
private String regionId = "cn-hangzhou";
}
2. 核心接口与实现:封装阿里云短信逻辑
定义统一的业务接口以屏蔽底层厂商差异,并实现阿里云短信发送的具体逻辑。
// 短信发送接口
package com.example.sms.core;
import java.util.Map;
public interface SmsService {
// 使用默认模板发送
boolean send(String phone, Map<String, String> params);
// 指定模板发送
boolean send(String phone, String templateId, Map<String, String> params);
}
// 阿里云实现类
package com.example.sms.core.impl;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.example.sms.core.SmsService;
import com.example.sms.exception.SmsException;
import com.example.sms.properties.SmsProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RequiredArgsConstructor
public class AliyunSmsServiceImpl implements SmsService {
private final SmsProperties properties;
private IAcsClient acsClient;
private IAcsClient getClient() {
if (acsClient == null) {
DefaultProfile profile = DefaultProfile.getProfile(
properties.getRegionId(),
properties.getAccessKey(),
properties.getAccessSecret()
);
acsClient = new DefaultAcsClient(profile);
}
return acsClient;
}
@Override
public boolean send(String phone, Map<String, String> params) {
if (properties.getDefaultTemplateId() == null) {
throw new SmsException("默认模板ID未配置");
}
return send(phone, properties.getDefaultTemplateId(), params);
}
@Override
public boolean send(String phone, String templateId, Map<String, String> params) {
SendSmsRequest request = new SendSmsRequest();
request.setPhoneNumbers(phone);
request.setSignName(properties.getSignName());
request.setTemplateCode(templateId);
request.setTemplateParam(mapToJson(params));
try {
SendSmsResponse response = getClient().getAcsResponse(request);
if (!"OK".equals(response.getCode())) {
throw new SmsException("短信发送失败:" + response.getMessage());
}
return true;
} catch (Exception e) {
log.error("短信发送异常", e);
throw new SmsException("短信发送异常:" + e.getMessage());
}
}
private String mapToJson(Map<String, String> params) {
// 简化实现,实际项目建议使用Jackson或FastJSON
return params == null ? "{}" :
"{" + params.entrySet().stream()
.map(e -> "\"" + e.getKey() + "\":\"" + e.getValue() + "\"")
.reduce((a, b) -> a + "," + b).orElse("") + "}";
}
}
3. 自动配置类:Starter 的灵魂
自动配置类是Starter的核心,通过一系列@Conditional注解精确控制Bean的创建条件,实现“约定优于配置”。
package com.example.sms.config;
import com.example.sms.core.SmsService;
import com.example.sms.core.impl.AliyunSmsServiceImpl;
import com.example.sms.properties.SmsProperties;
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
// 启用配置属性绑定
@EnableConfigurationProperties(SmsProperties.class)
// 当配置了sms.enabled=true且vendor为ALIYUN时生效(matchIfMissing提供了默认值)
@ConditionalOnProperty(prefix = "sms", name = {"enabled", "vendor"}, havingValue = "true", matchIfMissing = true)
// 类路径下存在SmsService接口时才生效
@ConditionalOnClass(SmsService.class)
public class SmsAutoConfiguration {
// 只有当用户没有自定义SmsService的Bean时,才注册这个默认的阿里云实现
@Bean
@ConditionalOnMissingBean(SmsService.class)
public SmsService smsService(SmsProperties properties) {
return new AliyunSmsServiceImpl(properties);
}
}
4. 注册自动配置类:关键步骤
在 src/main/resources/META-INF/spring/ 目录下创建 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,内容为自动配置类的全限定名:
com.example.sms.config.SmsAutoConfiguration
请注意:对于 Spring Boot 2.7 以下版本,需使用 spring.factories 文件进行注册,格式如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.sms.config.SmsAutoConfiguration
5. Maven 依赖配置:精简且无冲突
Starter的依赖管理需遵循“精简、明确、避免传递冲突”的原则,非必要依赖应标记为optional。
<?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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.15</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>sms-spring-boot-starter</artifactId>
<version>1.0.0</version>
<name>短信服务Starter</name>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<!-- 提供配置元数据,用于IDE提示 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 阿里云短信SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>2.8.3</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.1.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>
四、安装与使用:一键集成到业务项目
1. 安装 Starter 到本地仓库
在Starter项目根目录下执行Maven命令,将其安装到本地Maven仓库以供其他项目引用:
mvn clean install -DskipTests
2. 业务项目集成使用
(1)引入依赖
在业务项目的pom.xml中添加对自定义Starter的依赖:
<dependency>
<groupId>com.example</groupId>
<artifactId>sms-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
(2)配置参数
在 application.yml 中添加必要的短信服务配置,这些配置会由我们之前定义的SmsProperties类自动绑定:
sms:
enabled: true
vendor: ALIYUN
access-key: 你的阿里云AccessKey
access-secret: 你的阿里云AccessSecret
sign-name: 你的短信签名
default-template-id: SMS_123456789 # 默认模板ID
(3)注入使用
完成配置后,即可在业务代码中直接注入 SmsService 接口使用,如同使用Spring内置的Bean一样简单。
import com.example.sms.core.SmsService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@RestController
public class SmsController {
@Resource
private SmsService smsService;
@GetMapping("/send-sms")
public String sendSms() {
Map<String, String> params = new HashMap<>();
params.put("code", "123456");
smsService.send("13800138000", params);
return "短信发送成功";
}
}
五、核心规范与扩展建议
1. 自定义 Starter 开发规范总结
- 命名规范:第三方Starter推荐使用
xxx-spring-boot-starter 格式命名,以便与官方的 spring-boot-starter-xxx 区分。
- 条件装配:善用
@ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty 等注解,实现Bean的智能、按需注册。
- 依赖管理:仅引入核心功能依赖,将非必需或可能引起冲突的依赖标记为
<optional>true</optional>。
- 配置友好:通过
spring-boot-configuration-processor 依赖为自定义配置属性提供IDE自动补全和提示。
2. 扩展建议
- 多厂商支持:可新增
TencentSmsServiceImpl 等实现类,并通过配置vendor属性或更复杂的工厂模式动态选择。这体现了云原生应用中对多云服务兼容的设计思想。
- 配置加密:集成如Jasypt等工具,对
access-key和access-secret等敏感信息进行加密存储,提升应用安全性。
- 增强可观测性:集成Micrometer等指标库,暴露短信发送成功率、耗时等监控指标。
- 结果统一封装:可定义全局的返回值类,对短信发送的成功/失败结果进行统一封装,方便前端或调用方处理。
六、核心总结
开发一个自定义短信服务Starter,本质上是将通用业务逻辑与Spring Boot自动配置机制进行深度封装,形成可独立分发、复用的组件。这完美践行了“一次开发,多处使用”的理念,能极大提升团队开发效率与项目整洁度。
通过本次实践,我们从配置属性绑定、核心接口设计、自动配置类编写,到最终的SPI注册文件配置,完整走通了自定义Starter的标准开发流程。每一步对规范的遵循,正是深入理解和驾驭Spring Boot“约定优于配置”哲学的关键所在。