从头带你撸一个Springboot Starter

时间:2022-10-13 18:15:10

我们知道 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。

我们的整体项目框架如下:

从头带你撸一个Springboot Starter

如同别的 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在线代码修改器

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

  4. 带你搭一个SpringBoot&plus;SpringData JPA的环境

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

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

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

  6. 分享一个springboot脚手架

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

  7. (02) 第一个springboot程序

    1. 创建一个springboot程序 1. idea 自带的springboot插件 2. 直接从https://start.spring.io 创建好程序下载下来, 之后覆盖你的创建的项目 2. ...

  8. 整理代码,将一些曾经用过的功能整合进一个spring-boot

    一 由于本人的码云太多太乱了,于是决定一个一个的整合到一个springboot项目里面. 附上自己的项目地址https://github.com/247292980/spring-boot 功能 1. ...

  9. SpringBoot &vert; 第一章:第一个SpringBoot应用

    springboot简单介绍 概述 SpringBoot的核心功能 优缺点 优点 缺点 工程搭建 创建项目 项目结构 pom依赖 主入口 编写controller 启动应用 总结 老生常谈 sprin ...

随机推荐

  1. 黑云压城城欲摧 - 2016年iOS公开可利用漏洞总结

    黑云压城城欲摧 - 2016年iOS公开可利用漏洞总结 作者:蒸米,耀刺,黑雪 @ Team OverSky 0x00 序 iOS的安全性远比大家的想象中脆弱,除了没有公开的漏洞以外,还有很多已经公开 ...

  2. 几种display&colon;table-cell的应用

    一.display:table-cell属性简述 display:table- cell属性指让标签元素以表格单元格的形式呈现,类似于td标签.目前IE8+以及其他现代浏览器都是支持此属性的,但是IE ...

  3. java eclipse打jar包和执行jar中的main函数

    jar包使用eclipse打包步骤 右键需要打包的项目->选择Export 到这里有两种打包的方式 1.如果项目中没有使用其他第三方包等,则直接选择下图中的第一种即可(JAR file) 2.如 ...

  4. Grunt 新手指南

    导言 作为一个正在准备从java 后端转大前端,一直都有想着,在js 的世界里面有没有类似于maven或者gradle 的东西..然后,就找到了grunt 这玩意 Grunt是用来干什么的 诸如ant ...

  5. 使用spring cloud实现分布式配置管理

    <7天学会spring cloud系列>之创建配置管理服务器及实现分布式配置管理应用. 本文涉及到的项目: 开源项目:http://git.oschina.net/zhou666/spri ...

  6. 1&period;单件模式&lpar;Singleton Pattern&rpar;

    意图:为了保证一个类仅有一个实例,并提供一个访问它的全局访问点. 1.简单实现(多线程有可能产生多个实例) public class CommonSigleton { /// <summary& ...

  7. 【iHMI43 4&period;3寸液晶模块】demo例程(版本1&period;03)发布

    ============================== 技术论坛:http://www.eeschool.org 博客地址:http://xiaomagee.cnblogs.com 官方网店:h ...

  8. objective-c自学总结(二)---init&sol;set&sol;get方法

    一:类的声明和实现: 声明:(放在“类名+.h”文件中). 类的声明主要有两部分组成:实例变量和方法. 例 #import <Foundation/Foundation.h> @inter ...

  9. codeforces 830 B Cards Sorting

    B. Cards Sorting  http://codeforces.com/problemset/problem/830/B Vasily has a deck of cards consisti ...

  10. Python连接MySQL数据库之pymysql模块

    pymysql 在python3.x 中用于连接MySQL服务器的一个库:Python2中则使用mysqldb pymysql的模块的基本的使用 # 导入pymysql模块 import pymysq ...