从头带你撸一个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 该项 ...
随机推荐
- 036—环境变量path
day04 课堂笔记 1.开发第一个java程序:HelloWorld 1.1.程序写完以后,一定要ctrl+s进行保存 源代码若修改,需重新进行编译 1.2.编译阶段 怎么编译?使用什么命令?这个命 ...
- java---String 和 StringBuffer
Java-String和StringBuffer类 Java String 类 字符串在Java中属于对象,Java提供String类来创建和操作字符串. 创建字符串 创建字符串常用的方法如下: ...
- JavaScript中的函数、参数、变量
高中大学数学很差,学JavaScript,发现根本不理解其中的函数.参数.变量等概念. 李永乐老师教学视频:<高三数学复习100讲>函数 bilibili.com/video/av5087 ...
- SpringBoot 整合 Thymeleaf & 如何使用后台模板快速搭建项目
如果你和我一样,是一名 Java 道路上的编程男孩,其实我不太建议你花时间学 Thymeleaf,当然他的思想还是值得借鉴的.但是他的本质在我看来就是 Jsp 技术的翻版(Jsp 现在用的真的很少很少 ...
- Noip模拟84 2021.10.27
以后估计都是用\(markdown\)来写了,可能风格会有变化 T1 宝藏 这两天老是会的题打不对,还是要细心... 考场上打的是维护\(set\)的做法,但是是最后才想出来的,没有维护对于是没有交. ...
- GPIO原理与配置(跑马灯,蜂鸣器,按键)
一.STM32 GPIO固件库函数配置方法 1. 根据需要在项目中删掉一些不用的固件库文件,保留有用的固件库文件 2. 在stm32f10x_conf.h中注释掉这些不用的头文件 3. STM32的I ...
- Machine learning(3-Linear Algebra Review )
1.Matrices and vectors Matrix :Rectangular array of numbers a notation R3×3 Vector : An n×1 matrix t ...
- 到底能不能用 join
互联网上一直流传着各大公司的 MySQL 军规,其中关于 join 的描述,有些公司不推荐使用 join,而有些公司则规定有条件的使用 join, 它们都是教条式的规定,也没有详细说其中的原因,这就很 ...
- yum history使用详解(某次为解决误卸载软件的回退实验)
[root@localhost ~]# yum history list #查看历史 Loaded plugins: fastestmirror ID | Command line | Date an ...
- Ubuntu virtualenv 创建 python3 虚拟环境 激活 退出
首先默认安装了virtualenv 创建python3虚拟环境 your-name@node-name:~/virtual_env$ virtualenv -p /usr/bin/python3 py ...