Spring Boot 教程:如何开发一个 starter¶
导语¶
熟悉 Spring Boot 的同学都知道,Spring Boot 提供了很多开箱即用的 starter,比如 spring-boot-starter-mail、spring-boot-starter-data-redis 等等,使用这些 starter 非常简单,引入依赖,再在配置文件中配置相关属性即可。本课程教您自己开发一个 starter,具备了这个技能后,您可以在工作中封装自己业务相关的各种 starter。
如何开发一个自定义的 starter¶
开发一个 starter 简单来说以下几步即可:
- 一个/多个自定义配置的属性配置类(可选)
- 一个/多个自动配置类
- 自动配置类写入到 Spring Boot 的 SPI 机制配置文件:spring.factories
Java SPI 机制简介¶
Spring Boot 的 starter 的核心其实就是通过 SPI 机制自动注入配置类,不过是它自己实现的一套 SPI 机制,我们先了解一下 Java 的 SPI 机制。
SPI 全称 Service Provider Interface,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件。
SPI 的大概流程是:
调用方 –> 标准服务接口 –> 本地服务发现 (配置文件) –> 具体实现
所以 Java SPI 实际上 是“基于接口的编程+策略模式+配置文件” 组合实现的动态加载机制。
一个 SPI 的典型案例就是 JDBC 的驱动,Java JDBC 定义接口规范(java.sql.Driver),各个数据库厂商(MySQL/Oracle/MS SQLServer 等)去完成具体的实现,然后通过 SPI 配置文件引入具体的实现类,如下图:
Java SPI 机制示例¶
一个简单的 Java SPI 开发步骤:
- 定义一个业务接口
- 编写接口实现类
- 创建 SPI 的配置文件,实现类路径写入配置文件中
- 通过 Java SPI 机制调用
Spring Boot SPI 机制底层实现¶
了解了 Java 的 SPI 机制后,基本也能猜出 Spring Boot 的 SPI 实现了,基本流程是一样的:
读取配置文件 –> 将具体的实现类装配到 Spring Boot 的上下文环境中
接下来我们从源码中查找答案。
入口:Spring Boot 的启动类,@SpringBootApplication 注解,查看源码可以发现这是一个组合注解,包括 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM,
classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
...
}
@EnableAutoConfiguration,熟悉 Spring Boot 的同学应该知道 Spring Boot 有很多 @EnableXXX 注解,其实现就是通过 @Import(xxxSelector) 导入一个 xxxSelector 的实现类,来装载配置类:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
...
}
继续看 AutoConfigurationImportSelector 源码,我们关注下 selectImports 方法即可,该方法就是用来装配自动配置类的:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
//加载自动配置元数据配置文件,后面会解释
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
//加载自动配置类,会合并上面的元数据配置文件中的配置类
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
autoConfigurationMetadata, annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
继续跟踪源码:
getAutoConfigurationEntry –> getCandidateConfigurations –>SpringFactoriesLoader.loadFactoryNames –> loadSpringFactories –> classLoader.getResources (FACTORIES_RESOURCE_LOCATION)
终于找到了 SPI 的配置文件:FACTORIES_RESOURCE_LOCATION。
到这基本就可以看到 Spring Boot 的装载流程了,在 META-INF/spring.factories 下定义的配置类会自动装配到 Spring Boot 的上下文。
开发一个自定义 starter¶
了解了上面 Spring Boot 的 SPI 加载机制后,我们来开发一个自定义的 starter,我这里写个简单的邮件发送的 starter,为简化代码,这里我还是依赖 Spring Boot 提供的 mail-starter, 在这个基础上进行一层封装:
-
创建一个 module:email-spring-boot-starter,引入依赖。
<!-- 邮件发送支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> <!-- 模版邮件 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <scope>provided</scope> </dependency> -
编写邮件发送模版类,这里我添加了一个是否启用的开关:
@ConditionalOnProperty (name = "dragon.boot.email.enable", havingValue = "true") @Slf4j @Configuration @ConditionalOnProperty(name = "dragon.boot.email.enable", havingValue = "true") public class MailSenderTemplate { //注入Spring Boot提供的mail中的邮件发送类 @Autowired private JavaMailSender mailSender; @Value("${spring.mail.from}") private String from; @Autowired private FreeMarkerConfigurer freeMarkerConfigurer; /** * @MethodName: send * @Author: pengl * @Date: 2019-10-31 13:38 * @Description: 发送邮件 * @Version: 1.0 * @Param: [to, content, subject] * @Return: com.dragon.boot.common.model.Result **/ public Result send(String to, String content, String subject) { return send(MailDto.builder().to(to).content(content).subject(subject).build()); } /** * @MethodName: send * @Author: pengl * @Date: 2019-10-31 13:39 * @Description: 发送邮件(抄送) * @Version: 1.0 * @Param: [to, content, subject, cc] * @Return: com.dragon.boot.common.model.Result **/ public Result send(String to, String content, String subject, String cc) { return send(MailDto.builder().to(to).content(content).subject(subject).cc(cc).build()); } /** * @MethodName: sendTemplate * @Author: pengl * @Date: 2019-10-31 13:39 * @Description: 发送模版邮件 * @Version: 1.0 * @Param: [to, model, template, subject] * @Return: com.dragon.boot.common.model.Result **/ public Result sendTemplate(String to, Map<String, Object> model, String template, String subject) { return send(MailDto.builder().to(to).content(getTemplateStr(model, template)).subject(subject).build()); } /** * @MethodName: sendTemplate * @Author: pengl * @Date: 2019-10-31 13:39 * @Description: 发送模版邮件(带抄送) * @Version: 1.0 * @Param: [to, model, template, subject, cc] * @Return: com.dragon.boot.common.model.Result **/ public Result sendTemplate(String to, Map<String, Object> model, String template, String subject, String cc) { return send(MailDto.builder().to(to).content(getTemplateStr(model, template)).subject(subject).cc(cc).build()); } /** * @MethodName: getTemplateStr * @Author: pengl * @Date: 2019-10-31 13:38 * @Description: 解析freemark模版 * @Version: 1.0 * @Param: [model, template] * @Return: java.lang.String **/ private String getTemplateStr(Map<String, Object> model, String template) { try { return FreeMarkerTemplateUtils.processTemplateIntoString(freeMarkerConfigurer.getConfiguration().getTemplate(template), model); } catch (Exception e) { log.error("获取模版数据异常:{}", e.getMessage(), e); } return ""; } /** * @MethodName: send * @Author: pengl * @Date: 2019-10-31 13:34 * @Description: 发送邮件 * @Version: 1.0 * @Param: [mailDto] * @Return: com.dragon.boot.common.model.Result **/ public Result send(MailDto mailDto) { if (StringUtils.isAnyBlank(mailDto.getTo(), mailDto.getContent())) { return new Result(false, 1001, "接收人或邮件内容不能为空"); } String[] tos = filterEmail(mailDto.getTo().split(",")); if (tos == null) { log.error("邮件发送失败,接收人邮箱格式不正确:{}", mailDto.getTo()); return new Result(false, 1002, ""); } MimeMessage mimeMessage = mailSender.createMimeMessage(); try { MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); helper.setFrom(from); helper.setTo(tos); helper.setText(mailDto.getContent(), true); helper.setSubject(mailDto.getSubject()); //抄送 String[] ccs = filterEmail(mailDto.getCc().split(",")); if (ccs != null) { helper.setCc(ccs); } //秘抄 String[] bccs = filterEmail(mailDto.getBcc().split(",")); if (bccs != null) { helper.setBcc(bccs); } //定时发送 if (mailDto.getSendDate() != null) { helper.setSentDate(mailDto.getSendDate()); } //附件 File[] files = mailDto.getFiles(); if (files != null && files.length > 0) { for (File file : files) { helper.addAttachment(file.getName(), file); } } mailSender.send(mimeMessage); } catch (Exception e) { log.error("邮件发送异常:{}", e.getMessage(), e); return new Result(false, 1099, "邮件发送异常:" + e.getMessage()); } return new Result(); } /** * 邮箱格式校验过滤 * * @param emails * @return */ private String[] filterEmail(String[] emails) { List<String> list = Arrays.asList(emails); if (CollectionUtil.isEmpty(list)) { return null; } list = list.stream().filter(e -> checkEmail(e)).collect(Collectors.toList()); return list.toArray(new String[list.size()]); } private boolean checkEmail(String email) { return ReUtil.isMatch("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$", email); } } -
编写 SPI 配置文件,在 resources 下新建文件夹 META-INF,创建配置文件 spring.factories,内容如下:
-
一个简单的 starter 模块就编写好了,使用时引入这个依赖, application.properties 属性文件里添加配置即可。
# 邮件发送配置 spring.mail.host=mail.xxx.com spring.mail.username=xx spring.mail.password=xx spring.mail.protocol=smtp spring.mail.properties.mail.smtp.auth=true spring.mail.properties.mail.smtp.port=465 [email protected] spring.mail.properties.mail.smtp.starttls.enable=true spring.mail.properties.mail.smtp.starttls.required=true spring.mail.properties.mail.smtp.ssl.enable=true spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory spring.mail.properties.mail.smtp.socketFactory.fallback=false spring.mail.default-encoding=utf-8 [email protected] # 所有附件最大长度(单位字节,默认100M) spring.mail.maxUploadSize=104857600 spring.mail.maxInMemorySize=4096 #启用email模块 dragon.boot.email.enable=true
这只是一个最简单的例子,如果严格按规范,可以将所有的 autoconfig 类,包括 Property 属性配置类和逻辑配置类都放到一个独立的模块中,再另起一个 starter 模块,引入这个独立的 autoconfig 模块。
自定义 starter 优化¶
属性配置自动提示功能:使用 Spring Boot 官方提供的 starter 的时候,在 application.properties 中编写属性配置是有自动提示功能的,要实现这个也很简单,引入一下依赖即可,该插件引入后,打包时会检查 @ConfigurationProperties 下的类,自动生成 spring-configuration-metadata.json 文件用于编写属性提示:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
- 启动优化:前面有提到 Spring Boot 的 SPI 加载流程,会先加载自动配置元数据配置文件,引入以下依赖,该插件会自动生成 META-INF/spring-autoconfigure-metadata.properties,供 AutoConfigurationImportSelector 过滤加载,提升启动性能:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
总结¶
依托 Spring Boot 强大的 AutoConfig 能力,我们可以封装各种自定义 starter,做到开箱即用,降低业务耦合,提高开发效率!

