我们知道 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,首先需要实现自动化配置,实现自动化配置需要满足以下两个条件:

  1. 能够自动配置项目所需要的配置信息,也就是自动加载依赖环境;

  2. 能够根据项目提供的信息自动生成 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的更多相关文章

  1. 手撸一个SpringBoot的Starter,简单易上手

    前言:今天介绍一SpringBoot的Starter,并手写一个自己的Starter,在SpringBoot项目中,有各种的Starter提供给开发者使用,Starter则提供各种API,这样使开发S ...

  2. 看了 Spring 官网脚手架真香,也撸一个 SpringBoot DDD 微服务的脚手架!

    作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 为什么我们要去造轮子? 造轮子的核心目的,是为了解决通用共性问题的凝练和复用. 虽然 ...

  3. 手写一个springboot starter

    springboot的starter的作用就是自动装配.将配置类自动装配好放入ioc容器里.作为一个组件,提供给springboot的程序使用. 今天手写一个starter.功能很简单,调用start ...

  4. 真香,撸一个SpringBoot在线代码修改器

    前言 项目上线之后,如果是后端报错,只能重新编译打包部署然后重启:如果仅仅是前端页面.样式.脚本修改,只需要替换到就可以了. 小公司的话可能比较自由,可以随意替换,但是有些公司权限设置的比较严格,需要 ...

  5. 我的第一个springboot starter

      在springboot中有很多starter,很多是官方开发的,也有是个人或开源组织开发的.这些starter是用来做什么的呐? 一.认识starter   所谓的starter,在springb ...

  6. 带你搭一个SpringBoot+SpringData JPA的环境

    前言 只有光头才能变强. 文本已收录至我的GitHub仓库,欢迎Star:https://github.com/ZhongFuCheng3y/3y 不知道大家对SpringBoot和Spring Da ...

  7. SpringBoot Starter缘起

    SpringBoot通过SpringBoot Starter零配置自动加载第三方模块,只需要引入模块的jar包不需要任何配置就可以启用模块,遵循约定大于配置的思想. 那么如何编写一个SpringBoo ...

  8. 徒手撸一个 Spring Boot 中的 Starter ,解密自动化配置黑魔法!

    我们使用 Spring Boot,基本上都是沉醉在它 Stater 的方便之中.Starter 为我们带来了众多的自动化配置,有了这些自动化配置,我们可以不费吹灰之力就能搭建一个生产级开发环境,有的小 ...

  9. 分享一个springboot脚手架

    项目介绍 在我们开发项目的时候各个项目之间总有一些可共用的代码或者配置,如果我们每新建一个项目就把代码复制粘贴再修改就显得很没有必要.于是我就做了一个 poseidon-boot-starter 该项 ...

随机推荐

  1. HCNP Routing&Switching之BGP邻居建立条件、优化和认证

    前文我们了解了BGP相关概念.AS相关概念以及BGP邻居类型.基础配置等,相关回顾请参考https://www.cnblogs.com/qiuhom-1874/p/15370838.html:今天我们 ...

  2. JDK里常见容器总结

    自己总结.   扩容 线程安全   是否支持null 的key 说明 hashmap 2*length 否   是 1.8以后增加红黑树.提高检索效率 hashtable   是   否 官方不建议使 ...

  3. BUAA_2020_软件工程_提问回顾与总结

    项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任建) 这个作业的要求在哪里 提问回顾与总结作业要求 我在这个课程的目标 了解软件工程的技术,掌握工程化开发的能力 这个作业在哪 ...

  4. FastAPI 学习之路(五十九)封装统一的json返回处理工具

    这之前的接口,我们返回的格式都是每个接口异常返回的数据格式都会不一样,我们处理起来没有那么方便,我们可以封装一个统一的json处理. 那么我们看下如何来实现呢 from fastapi import ...

  5. 攻防世界 杂项13.can_has_stdio?

    打开发现是由trainfuck编码组成的小星星阵容,果断交给解密网站进行解密, 解密网站:http://ctf.ssleye.com/brain.html flag:flag{esolangs_for ...

  6. Luogu P1084 疫情控制 | 二分答案 贪心

    题目链接 观察题目,答案明显具有单调性. 因为如果用$x$小时能够控制疫情,那么用$(x+1)$小时也一定能控制疫情. 由此想到二分答案,将问题转换为判断用$x$小时是否能控制疫情. 对于那些在$x$ ...

  7. hdu 5094 Maze (BFS+状压)

    题意: n*m的迷宫.多多要从(1,1)到达(n,m).每移动一步消耗1秒.有P种钥匙. 有K个门或墙.给出K个信息:x1,y1,x2,y2,gi    含义是(x1,y1)与(x2,y2)之间有gi ...

  8. 第01课 OpenGL窗口(3)

    接下来的代码段创建我们的OpenGL窗口.我花了很多时间来做决定是否创建固定的全屏模式这样不需要许多额外的代码,还是创建一个容易定制的友好的窗口但需要更多的代码.当然最后我选择了后者.我经常在EMai ...

  9. 关于ENSP错误代码的常见问题

    1.最适合ensp运行的环境是win7,在win7上运行基本不会出什么大问题(ensp370+virtualbox4.2.8) 2.如果需要重新安装,最好把旧版本清除干净,ensp+virtualbo ...

  10. prometheus(3)之grafan可视化展现

    可视化UI界面Grafana的安装和配置 Grafana介绍 Grafana是一个跨平台的开源的度量分析和可视化工具,可以将采集的数据可视化的展示,并及时通知给告警接收方.它主要有以下六大特点: 1. ...