从头带你撸一个Springboot Starter
我们知道 SpringBoot 提供了很多的 Starter 用于引用各种封装好的功能:
| 名称 | 功能 |
|---|---|
| spring-boot-starter-web | 支持 Web 开发,包括 Tomcat 和 spring-webmvc |
| spring-boot-starter-redis | 支持 Redis 键值存储数据库,包括 spring-redis |
| spring-boot-starter-test | 支持常规的测试依赖,包括 JUnit、Hamcrest、Mockito 以及 spring-test 模块 |
| spring-boot-starter-aop | 支持面向切面的编程即 AOP,包括 spring-aop 和 AspectJ |
| spring-boot-starter-data-elasticsearch | 支持 ElasticSearch 搜索和分析引擎,包括 spring-data-elasticsearch |
| spring-boot-starter-jdbc | 支持JDBC数据库 |
| spring-boot-starter-data-jpa | 支持 JPA ,包括 spring-data-jpa、spring-orm、Hibernate |
SpringBoot 通过 Starter 机制将各个独立的功能从 jar 包的形式抽象为统一框架中的一个子集,从而使得 SpringBoot 的完整度从框架层面达到了统一。其实现的机制也不复杂,SpringBoot 在启动时会从依赖的 starter 包中寻找 /META-INF/spring.factories 文件,然后根据文件中配置的启动类完成 Starter 的初始化,同 Java 的 SPI 机制类似。
考虑到 SpringBoot Starter 机制的意义本身就是对独立功能的封装,这些功能要求改动少,可以作为多个项目的公共部分对外提供服务。那么对于我们日常项目中底层不变经常变的公共服务是否可以起到借鉴意义。或者对于公司内部项目的架构师来说也是首选。
如果想自定义 Starter,首先需要实现自动化配置,实现自动化配置需要满足以下两个条件:
能够自动配置项目所需要的配置信息,也就是自动加载依赖环境;
能够根据项目提供的信息自动生成 Bean,并且注册到 Bean 管理容器中;
条件 1 的实现需要引入如下两个 jar 包:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.0.0.RELEASE</version>
<optional>true</optional>
</dependency>
</dependencies>
通过 autoconfigure 根据项目 jar 包的依赖关系自动配置应用程序。spring.factories 文件指定了AutoConfiguration 类列表,只有在列表中的自动配置才会被检索到。Spring 会检测 classpath 下所有的META-INF/spring.factories 文件;若要引入自定义的自动配置,需要将自定义的 AutoConfiguration 类添加到 spring.factories 文件中。
条件 2 则是在条件 1 的基础上加载你自定义的 bean。
命名规范
对于 SpringBoot 官方的 jar 包都是有一套命名规则:
规则:spring-boot-starter-模块名。比如:spring-boot-starter-web、spring-boot-starter-jdbc。
对于我们自己自定义的 Starter,为了区别于普通的 jar 包我们也应该有明显的 starter 标识,比如:
模块-spring-boot-starter
通过这种方式让调用方更直观的知道这是一个 Starter,从而很快就知道使用方式。
一个可以运行的示例
以下代码可以从 Github 仓库找到:redis-sentinel-spring-boot-starter。
我们通过自己实现一个可以运行的示例来演示实际开发中如何通过 Starter 快速搭建基础服务。下面的示例主要功能实现是重写 Springboot 的 Redis Sentinel,底层将 Lettuce 替换为 Jedis。
我们的整体项目框架如下:

如同别的 Starter 一样,我们要实现引用方通过自定义配置来使用 Redis,那我们要提供配置解析类:
package com.rickiyang.redis.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* @date: 2021/11/16 11:39 上午
* @author: rickiyang
* @Description:
*/
@Data
@ConfigurationProperties(prefix = RedisSentinelClientProperties.SENTINEL_PREFIX)
public class RedisSentinelClientProperties {
public final static String SENTINEL_PREFIX = "rickiyang.redis.sentinel";
private String masterName;
private String sentinels;
private long maxWait;
private int maxIdle;
private int maxActive;
private boolean blockWhenExhausted;
private long maxWaitMillis;
private int maxTotal;
private int minIdle;
private long minEvictableIdleTimeMillis;
private boolean testOnBorrow;
private boolean testOnReturn;
private boolean testWhileIdle;
private int numTestsPerEvictionRun;
private long softMinEvictableIdleTimeMillis;
private long timeBetweenEvictionRunsMillis;
private byte whenExhaustedAction;
}
如何将 yml 中的配置解析出来呢?这就需要我们去定义一个 yml 解析文件。resources下新增 META-INF 文件夹,新增配置解析类:spring-configuration-metadata.json
{
"hints": [],
"groups": [
{
"sourceType": "com.starter.demo.config.RedisSentinelClientProperties",
"name": "rickiyang.redis.sentinel",
"type": "com.starter.demo.config.RedisSentinelClientProperties"
},
{
"sourceType": "com.starter.demo.config.RedisSentinelClientProperties",
"defaultValue": false,
"name": "rickiyang.redis.sentinel.block-when-exhausted",
"type": "java.lang.Boolean"
},
{
"sourceType": "com.starter.demo.config.RedisSentinelClientProperties",
"name": "rickiyang.redis.sentinel.masterName",
"type": "java.lang.String"
},
{
"sourceType": "com.starter.demo.config.RedisSentinelClientProperties",
"defaultValue": 0,
"name": "rickiyang.redis.sentinel.max-active",
"type": "java.lang.Integer"
},
{
"sourceType": "com.starter.demo.config.RedisSentinelClientProperties",
"defaultValue": 0,
"name": "rickiyang.redis.sentinel.max-idle",
"type": "java.lang.Integer"
},
{
"sourceType": "com.starter.demo.config.RedisSentinelClientProperties",
"defaultValue": 0,
"name": "rickiyang.redis.sentinel.max-total",
"type": "java.lang.Integer"
},
{
"sourceType": "com.starter.demo.config.RedisSentinelClientProperties",
"defaultValue": 0,
"name": "rickiyang.redis.sentinel.max-wait",
"type": "java.time.Duration"
},
{
"sourceType": "com.starter.demo.config.RedisSentinelClientProperties",
"defaultValue": 0,
"name": "rickiyang.redis.sentinel.min-evictable-idle-time-millis",
"type": "java.lang.Long"
},
{
"sourceType": "com.starter.demo.config.RedisSentinelClientProperties",
"defaultValue": 0,
"name": "rickiyang.redis.sentinel.min-idle",
"type": "java.lang.Integer"
},
{
"sourceType": "com.starter.demo.config.RedisSentinelClientProperties",
"defaultValue": 0,
"name": "rickiyang.redis.sentinel.num-tests-per-eviction-run",
"type": "java.lang.Integer"
},
{
"sourceType": "com.starter.demo.config.RedisSentinelClientProperties",
"name": "rickiyang.redis.sentinel.sentinels",
"type": "java.lang.String"
},
{
"sourceType": "com.starter.demo.config.RedisSentinelClientProperties",
"defaultValue": 0,
"name": "rickiyang.redis.sentinel.soft-min-evictable-idle-time-millis",
"type": "java.lang.Long"
},
{
"sourceType": "com.starter.demo.config.RedisSentinelClientProperties",
"defaultValue": false,
"name": "rickiyang.redis.sentinel.test-on-borrow",
"type": "java.lang.Boolean"
},
{
"sourceType": "com.starter.demo.config.RedisSentinelClientProperties",
"defaultValue": false,
"name": "rickiyang.redis.sentinel.test-on-return",
"type": "java.lang.Boolean"
},
{
"sourceType": "com.starter.demo.config.RedisSentinelClientProperties",
"defaultValue": false,
"name": "rickiyang.redis.sentinel.test-while-idle",
"type": "java.lang.Boolean"
},
{
"sourceType": "com.starter.demo.config.RedisSentinelClientProperties",
"defaultValue": 0,
"name": "rickiyang.redis.sentinel.time-between-eviction-runs-millis",
"type": "java.lang.Long"
},
{
"sourceType": "com.starter.demo.config.RedisSentinelClientProperties",
"defaultValue": 0,
"name": "rickiyang.redis.sentinel.when-exhausted-action",
"type": "java.lang.Byte"
}
]
}
这一套配置解析规则就是通过我们上面引入的两个 Spring 配置解析相关的 jar 包来实现的。
SpringBoot 遵循约定大于配置的思想,通过约定好的配置来实现代码简化。@ConfigurationProperties 可以把指定路径下的属性注入到对象中。
SpringAutoConfigration 自动配置
SpringBoot 没出现之前所有的配置都是通过 xml 的方式进行解析。一个项目里面的依赖一旦多了起来开发者光是理清里面的依赖关系都很头疼。SpringBoot 的 AutoConfig 基本思想就是通过项目的 jar 包依赖关系来自动配置程序。
@EnableAutoConfiguration 和 @SpringBootApplication 都有开启 AutoConfig 能力。
@SpringBootApplication的作用等同于一起使用这三个注解:@Configuration、@EnableAutoConfiguration、和@ComponentScan
spring.factories 文件指定了AutoConfiguration类列表,只有在列表中的自动配置才会被检索到。Spring 会检测 classpath 下所有的 META-INF/spring.factories 文件;若要引入自定义的自动配置,需要将自定义的AutoConfiguration 类添加到 spring.factories 文件中。
spring.factories 的解析由 SpringFactoriesLoader 负责。SpringFactoriesLoader.loadFactoryNames() 扫描所有 jar 包类路径下 META-INF/spring.factories文件, 把扫描到的这些文件的内容包装成 properties 对象从 properties 中获取到 EnableAutoConfiguration.class 类(类名)对应的值,然后把他们添加在容器中 。
同样我们的项目中也配置了自动加载配置的启动类,spring.factories:
# Initializers
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.starter.demo.config.RedisSentinelClientAutoConfiguration
AutoConfigration 启动的时候会去检测配置类是否从 application.yml 获取到对应的配置值,如果没有则使用默认配置或者抛异常。
上例中的 Redis autoConfigration 对应的配置类:
package com.rickiyang.redis.config;
import com.google.common.collect.Sets;
import com.rickiyang.redis.annotation.EnableRedisSentinel;
import com.rickiyang.redis.redis.RedisClient;
import com.rickiyang.redis.redis.sentinel.RedisSentinelFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
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;
import javax.annotation.Resource;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import static com.rickiyang.redis.config.RedisSentinelClientProperties.SENTINEL_PREFIX;
/**
* @date: 2021/11/16 9:52 上午
* @author: rickiyang
* @Description:
*/
@Slf4j
@Configuration
@ConditionalOnClass(EnableRedisSentinel.class)
@ConditionalOnProperty(prefix = SENTINEL_PREFIX, name = "masterName")
@EnableConfigurationProperties(RedisSentinelClientProperties.class)
public class RedisSentinelClientAutoConfiguration {
@Resource
RedisSentinelClientProperties redisSentinelClientProperties;
@Bean(initMethod = "init", destroyMethod = "destroy")
public RedisSentinelFactory redisSentinelClientFactory() throws Exception {
RedisSentinelFactory redisSentinelClientFactory = new RedisSentinelFactory();
String[] sentinels = redisSentinelClientProperties.getSentinels().split(",");
redisSentinelClientFactory.setMasterName(redisSentinelClientProperties.getMasterName());
redisSentinelClientFactory.setServers(Sets.newHashSet(sentinels));
reflectProperties(redisSentinelClientFactory);
log.info("[init redis sentinel factory, redisSentinelClientProperties={}]", redisSentinelClientProperties);
return redisSentinelClientFactory;
}
@Bean
public RedisClient redisClient(RedisSentinelFactory redisSentinelFactory) throws Exception {
return new RedisClient(redisSentinelFactory);
}
private String createGetMethodName(Field propertiesField, String fieldName) {
String convertFieldName = fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
return propertiesField.getType() == boolean.class ? "is" + convertFieldName : "get" + convertFieldName;
}
private String createSetMethodName(String fieldName) {
String convertFieldName = fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);
return "set" + convertFieldName;
}
private boolean isPropertyBlank(Object value) {
return value == null || "0".equals(value.toString()) || "false".equals(value.toString());
}
private void reflectProperties(RedisSentinelFactory redisSentinelClientFactory) throws Exception {
Field[] propertiesFields = RedisSentinelClientProperties.class.getDeclaredFields();
for (Field propertiesField : propertiesFields) {
String fieldName = propertiesField.getName();
if ("masterName".equals(fieldName) || "sentinels".equals(fieldName) || "SENTINEL_PREFIX".equals(fieldName)) {
continue;
}
Method getMethod = RedisSentinelClientProperties.class.getMethod(createGetMethodName(propertiesField, fieldName));
Object value = getMethod.invoke(redisSentinelClientProperties);
if (!isPropertyBlank(value)) {
Method setMethod = RedisSentinelFactory.class.getMethod(createSetMethodName(fieldName), propertiesField.getType());
setMethod.invoke(redisSentinelClientFactory, value);
}
}
}
}
可以看到类头加了一些注解,这些注解的作用是限制这个类被加载的条件和时机。
常用的类加载限定条件有:
- @ConditionalOnBean:当容器里有指定的 bean 时生效。
- @ConditionalOnMissingBean:当容器里不存在指定 bean 时生效。
- @ConditionalOnClass:当类路径下有指定类时生效。
- @ConditionalOnMissingClass:当类路径下不存在指定类时生效。
- @ConditionalOnProperty:指定的属性是否有指定的值,比如
@ConditionalOnProperty(prefix=”aaa.bb”, value=”enable”, matchIfMissing=true),表示当 aaa.bb 为 enable 时条件的布尔值为 true,如果没有设置的情况下也为 true 的时候这个类才会被加载。
除了 Condition 开头的限定类注解之外,还有 Import 开头的注解,主要作用是引入类并将其声明为一个 bean。主要目的是将多个分散的 bean 配置融合为一个更大的配置类。
- @Import:在注解使用类加载之前先加载被引入的类。
- @ImportResource:在注解使用类加载之前引入配置文件。
上面的 Config 类头有一个注解:
@ConditionalOnClass(EnableRedisSentinel.class)
即加载的限定条件是 EnableRedisSentinel 类要先加载。EnableRedisSentinel 是一个注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableRedisSentinel {
}
这个注解的使用同别的 Starter 一样都是放在项目的启动类上即可。
基础的代码部分大概如上,关于 Redis 连接相关的代码大家可以看源码部分自己参考。将代码现在下来之后本地通过 maven 打成 jar 包,然后新开一个 SpringBoot 项目引入 maven jar 包。在启动类加上注解 @EnableRedisSentinel ,application.yml 文件中配置:
rickiyang:
redis:
sentinel:
masterName: redis-sentinel-test
sentinels: 127.0.0.1:20012::,127.0.0.2:20012::,127.0.0.3:20012
maxTotal: 1000
maxIdle: 50
minIdle: 16
maxWaitMillis: 15000
启动项目就能看到我们的 Starter 被加载起来。
从头带你撸一个Springboot Starter的更多相关文章
- 手撸一个SpringBoot的Starter,简单易上手
前言:今天介绍一SpringBoot的Starter,并手写一个自己的Starter,在SpringBoot项目中,有各种的Starter提供给开发者使用,Starter则提供各种API,这样使开发S ...
- 看了 Spring 官网脚手架真香,也撸一个 SpringBoot DDD 微服务的脚手架!
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 为什么我们要去造轮子? 造轮子的核心目的,是为了解决通用共性问题的凝练和复用. 虽然 ...
- 手写一个springboot starter
springboot的starter的作用就是自动装配.将配置类自动装配好放入ioc容器里.作为一个组件,提供给springboot的程序使用. 今天手写一个starter.功能很简单,调用start ...
- 真香,撸一个SpringBoot在线代码修改器
前言 项目上线之后,如果是后端报错,只能重新编译打包部署然后重启:如果仅仅是前端页面.样式.脚本修改,只需要替换到就可以了. 小公司的话可能比较自由,可以随意替换,但是有些公司权限设置的比较严格,需要 ...
- 我的第一个springboot starter
在springboot中有很多starter,很多是官方开发的,也有是个人或开源组织开发的.这些starter是用来做什么的呐? 一.认识starter 所谓的starter,在springb ...
- 带你搭一个SpringBoot+SpringData JPA的环境
前言 只有光头才能变强. 文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y 不知道大家对SpringBoot和Spring Da ...
- SpringBoot Starter缘起
SpringBoot通过SpringBoot Starter零配置自动加载第三方模块,只需要引入模块的jar包不需要任何配置就可以启用模块,遵循约定大于配置的思想. 那么如何编写一个SpringBoo ...
- 徒手撸一个 Spring Boot 中的 Starter ,解密自动化配置黑魔法!
我们使用 Spring Boot,基本上都是沉醉在它 Stater 的方便之中.Starter 为我们带来了众多的自动化配置,有了这些自动化配置,我们可以不费吹灰之力就能搭建一个生产级开发环境,有的小 ...
- 分享一个springboot脚手架
项目介绍 在我们开发项目的时候各个项目之间总有一些可共用的代码或者配置,如果我们每新建一个项目就把代码复制粘贴再修改就显得很没有必要.于是我就做了一个 poseidon-boot-starter 该项 ...
随机推荐
- MS office设置夜间模式
点击文件 帐户 -> office主题
- IDA硬编码修改SO,重新打包APK绕过FRIDA反调试
该案例来自看雪 通过IDA修改SO 找到检测点 修改字符串 双击该字符串进入TEXT VIEW 选择 HEX VIEW 修改十六进制 右击EDIT 修改字符 右击 菜单栏-> edit -> ...
- 禅道开源版 Ldap认证插件开发
禅道开源版-Ldap插件开发 背景 由于开源版无法使用ldap认证,所以在此分享一下自己开发禅道的ldap开发过程,希望对你有所帮助. 简单说一下这个插件的功能: 1.跳过原有禅道认证,使用ldap认 ...
- 【JAVA】【作业向】第一题:本学期一班级有n名学生,m门课程。现要求对每门课程的成绩进行统计:平均成绩、最高成绩、最低成绩,并统计考试成绩的分布律。
1.预备知识:动态数组Array实现: 2.解题过程需要理解的知识:吧唧吧唧吧唧吧唧 不想做了 就用了最简单的方法 和c语言类似 java版本 `import java.util.Scanner; / ...
- Coursera Deep Learning笔记 深度卷积网络
参考 1. Why look at case studies 介绍几个典型的CNN案例: LeNet-5 AlexNet VGG Residual Network(ResNet): 特点是可以构建很深 ...
- Git: 搭建一个本地私人仓库
Git: 搭建一个本地私人仓库 寝室放个电脑.实验室也有个电脑 为进行数据同步,充分利用实验室的服务器搭建了个本地私人仓库 1. 安装流程 当然首先保证服务器上与PC机上都已经安装了可用的Git 在P ...
- Charles的简单用法
Charles的简单用法 一.抓电脑上 http 包 二.显示请求的 Request 和 Response 三.抓取电脑上 https 包 1.安装根证书 2.在钥匙串中启用根证书 3.配置哪些需要抓 ...
- 镜头Lens Image circle像圈的解释是什么意思
Image circle镜头中指的是:像圈 像圈(image circle)是指入射光线通过镜头后,在焦平面上呈现出的圆形的明亮清晰的影像幅面,也称像面大小.镜头像圈由镜头光学结构决定,一旦设计完成, ...
- 零基础学习C语言入门必备知识
今天跟大家一起从零学C语言: 1. C语言简介 1.1 C语言发展史 C语言是一种广泛使用的面向过程的计算机程序设计语言,既适合于系统程序设计,又适合于应用程序设计.C语言的发展历程大致如图1-1所示 ...
- PCIE学习链接集合
<PCIE基础知识+vivado IP core设置> https://blog.csdn.net/eagle217/article/details/81736822 <一步一步开始 ...