【1】什么是SpringBoot starter机制?
想要自定义starter,首先要了解springboot是如何加载starter的,也就是springboot的自动装配机制。
SpringBoot中的starter是一种非常重要的机制(自动化配置),能够抛弃以前繁杂的配置,将其统一集成进starter,应用者只需要在maven中引入starter依赖,SpringBoot就能自动扫描到要加载的信息并启动相应的默认配置。
starter让我们摆脱了各种依赖库的处理,需要配置各种信息的困扰。SpringBoot会自动通过classpath路径下的类发现需要的Bean,并注册进IOC容器。SpringBoot提供了针对日常企业应用研发各种场景的spring-boot-starter依赖模块。
所有这些依赖模块都遵循着约定成俗的默认配置,并允许我们调整这些配置,即遵循“约定大于配置”的理念。
【2】为什么要自定义starter?
在我们的日常开发工作中,经常会有一些独立于业务之外的配置模块,我们经常将其放到一个特定的包下,然后如果另一个工程需要复用这块功能的时候,需要将代码硬拷贝到另一个工程,重新集成一遍,麻烦至极。
如果我们将这些可独立于业务代码之外的功配置模块封装成一个个starter,复用的时候只需要将其在pom中引用依赖即可,SpringBoot为我们完成自动装配
【3】什么时候需要创建自定义starter
在我们的日常开发工作中,可能会需要开发一个通用模块,以供其它工程复用。SpringBoot就为我们提供这样的功能机制,我们可以把我们的通用模块封装成一个个starter,这样其它工程复用的时候只需要在pom中引用依赖即可,由SpringBoot为我们完成自动装配。
下边,我来记录一下Springboot自定义starter的过程。
一:引入POM依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.6.13</version>
</dependency>
二:准备工作
我这里使用redisutil工具类来做示例:
1:redisutil.java内容如下所示:
package com.modules.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.*;
import java.util.concurrent.TimeUnit;
//@Component
public class RedisUtil
{
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public RedisUtil(RedisTemplate<String, Object> redisTemplate)
{
this.redisTemplate = redisTemplate;
}
/**
* 指定缓存失效时间
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key,long time)
{
try
{
if(time>0)
{
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
}
catch (Exception e)
{
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key){
return redisTemplate.getExpire(key,TimeUnit.SECONDS);
}
/**
* 判断key是否存在
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key)
{
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String ... key)
{
if(key!=null&&key.length>0)
{
if(key.length==1)
{
redisTemplate.delete(key[0]);
}
else
{
redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
}
}
}
//============================String=============================
/**
* 普通缓存获取
* @param key 键
* @return 值
*/
public Object get(String key){
return key==null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key,Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key,Object value,long time){
try {
if(time>0){
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
}else{
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
2:RedisLuaUtils.java代码内容如下:
package com.modules.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.types.Expiration;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicIntegerArray;
/**
* @author camellia
* redis 加锁工具类
*/
@Slf4j
public class RedisLuaUtils
{
/**
* 超时时间(毫秒)
*/
private static final long TIMEOUT_MILLIS = 100000;
/**
* 重试次数
*/
private static final int RETRY_TIMES = 100;
/***
* 睡眠时间(重试间隔)
*/
private static final long SLEEP_MILLIS = 1000;
/**
* 用来加锁的lua脚本
* 因为新版的redis加锁操作已经为原子性操作
* 所以放弃使用lua脚本
*/
private static final String LOCK_LUA =
"if redis.call("setnx",KEYS[1],ARGV[1]) == 1 " +
"then " +
" return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
" return 0 " +
"end";
/**
* 用来释放分布式锁的lua脚本
* 如果redis.get(KEYS[1]) == ARGV[1],则redis delete KEYS[1]
* 否则返回0
* KEYS[1] , ARGV[1] 是参数,我们只调用的时候 传递这两个参数就可以了
* KEYS[1] 主要用來传递在redis 中用作key值的参数
* ARGV[1] 主要用来传递在redis中用做 value值的参数
*/
private static final String UNLOCK_LUA =
"if redis.call("get",KEYS[1]) == ARGV[1] "
+ "then "
+ " local number = redis.call("del", KEYS[1]) "
+ " return tostring(number) "
+ "else "
+ " return tostring(0) "
+ "end ";
/**
* 检查 redisKey 是否上锁(没加锁返回加锁)
*
* @param redisKey redisKey
* @param template template
* @return Boolean
*/
public static Boolean isLock(String redisKey, String value, RedisTemplate<Object, Object> template)
{
return lock(redisKey, value, template, RETRY_TIMES);
}
private static Boolean lock(String redisKey, String value, RedisTemplate<Object, Object> template, int retryTimes)
{
boolean result = lockKey(redisKey, value, template);
// 循环等待上一个用户锁释放,或者锁超时释放
while (!(result) && retryTimes-- > 0)
{
try
{
log.debug("lock failed, retrying...{}", retryTimes);
Thread.sleep(RedisLuaUtils.SLEEP_MILLIS);
}
catch (InterruptedException e)
{
return false;
}
result = lockKey(redisKey, value, template);
}
return result;
}
/**
* 加锁
* @param key
* @param value
* @param template
* @return
*/
private static Boolean lockKey(final String key, final String value, RedisTemplate<Object, Object> template)
{
try
{
Boolean nativeLock = template.opsForValue().setIfAbsent(key,value, Duration.ofSeconds(RedisLuaUtils.TIMEOUT_MILLIS));
System.out.println("加锁成功:"+nativeLock);
return nativeLock;//*/
}
catch (Exception e)
{
log.info("lock key fail because of ", e);
//throw new Exception("redis 连接失败!");
return false;
}
}
/**
* 释放分布式锁资源(解锁)
*
* @param redisKey key
* @param value value
* @param template redis
* @return Boolean
*/
public static Integer releaseLock(String redisKey, String value, RedisTemplate<Object, Object> template)
{
try
{
List<Object> list = new CopyOnWriteArrayList<>();
list.add(redisKey);
Integer result = template.execute(new DefaultRedisScript<>(UNLOCK_LUA,Integer.class), list, value);
return result;//*/
}
catch (Exception e)
{
log.info("release lock fail because of ", e);
return 0;
}
}
}
Lua脚本的工具类我这里是将他定义成静态的。后边也会将他用另一种方式定义成stater。直接使用@Recourse注解注入。
3:yml配置redis.
spring:
#redis
redis:
# 超时时间
timeout: 10000
# 使用的数据库索引,默认是0
database: 0
# 密码
password: xxxx
###################以下为red1s哨兵增加的配置#######
sentinel:
master: mymaster # 哨兵主节点名称,自定义
nodes:127.0.0.1:26379, 127.0.0.1:26380, 127.0.0.1:26381
##################以下为]ettuce连接池增加的配置######
lettuce:
pool:
max-active: 100 #连接池最大连接数(使用负值表示没有限制)
max-idle: 100 #连接池中的最大空闲连接
min-idle: 50 #连接池中的最小空闲连接
max-wait: -1 #连接池最大阻塞等待时间(使用负值表示没有限制)
三:创建自定义starer的第一种方式
1:使用上边准备好的redisutil.java类。
2:创建RedisConfig.java配置类,内容如下:
package com.modules.config;
import com.modules.utils.RedisUtil;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import javax.annotation.Resource;
@Configuration
public class RedisUtilsConfig
{
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Bean
public RedisUtil RedisUtil()
{
return new RedisUtil(redisTemplate);
}
}
3:在Recourse目录下创建META-INF目录,在META-INF目录下创建spring.factories文件,内容如下:
# 多个文件使用逗号拼接
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.modules.config.RedisUtilsConfig
到这里,自定义stater就完成了。
四:在其他项目中使用自定义stater
1:引入pom依赖
<dependency>
<groupId>com.modules</groupId>
<artifactId>utils</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
2:在代码中使用
/**
* 注入自定义stater 的 redisutil工具类
*/
@Resource
private RedisUtil redisUtil;
@GetMapping("java/testRedis")
public void testRedis()
{
// 使用多线程来模拟多用户并发操作
for (int i = 0; i < 20; i++)
{
final int temp = i;
new Thread(() -> {
System.out.println("开始创建订单:"+temp);
// 生成唯一uuid
String uuid = UUID.randomUUID().toString();
// 上锁
Boolean isLock = RedisLuaUtils.isLock("Locks", uuid, redisTemplate);
if (!isLock)
{
// System.out.println("锁已经被占用:"+temp);
}
else
{ // 获取到锁,做对应的处理。
System.out.println("获取到锁!"+temp);
Boolean res = redisUtil.set("tests", "test:"+temp);
String username = "redis";
// 将记录写入数据库,这一步可以换成其他操作
String ip = "0.0.0.0";
Browse browse = new Browse();
browse.setUsername(username.toString());
browse.setArticleTitle("test:"+temp);
browse.setIp(ip);
browse.setIsWeixin((byte) '0');
articleDao.addBrowse(browse);
System.out.println("redis写入状态:"+res+" - "+temp);
}
/*try
{
Thread.sleep((long) 1);
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}//*/
//一定要记得释放锁,否则会出现问题,解锁会校验uuid,校验失败,解锁也就失败
Integer res = RedisLuaUtils.releaseLock("Locks", uuid, redisTemplate);
System.out.println("redis-lock解锁状态:"+res+" - "+temp);
}).start();
}
}
我这里是用我测试redis分布式锁的示例来做演示的。
五:创建自定义starer的第二种方式
1:如果你不想像上边一样配置一个starter,也还是有其他方法的。
上边的RedisLuaUtils.java我们使用的是静态的,现在我改成非静态,代码如下所示:
package com.modules.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* @author camellia
* redis 加锁工具类
*/
@Slf4j
@Component
public class RedisLuaUtilsStater
{
/**
* 超时时间(毫秒)
*/
private final long TIMEOUT_MILLIS = 100000;
/**
* 重试次数
*/
private final int RETRY_TIMES = 100;
/***
* 睡眠时间(重试间隔)
*/
private final long SLEEP_MILLIS = 1000;
/**
* 用来加锁的lua脚本
* 因为新版的redis加锁操作已经为原子性操作
* 所以放弃使用lua脚本
*/
private final String LOCK_LUA =
"if redis.call("setnx",KEYS[1],ARGV[1]) == 1 " +
"then " +
" return redis.call('expire',KEYS[1],ARGV[2]) " +
"else " +
" return 0 " +
"end";
/**
* 用来释放分布式锁的lua脚本
* 如果redis.get(KEYS[1]) == ARGV[1],则redis delete KEYS[1]
* 否则返回0
* KEYS[1] , ARGV[1] 是参数,我们只调用的时候 传递这两个参数就可以了
* KEYS[1] 主要用來传递在redis 中用作key值的参数
* ARGV[1] 主要用来传递在redis中用做 value值的参数
*/
private final String UNLOCK_LUA =
"if redis.call("get",KEYS[1]) == ARGV[1] "
+ "then "
+ " local number = redis.call("del", KEYS[1]) "
+ " return tostring(number) "
+ "else "
+ " return tostring(0) "
+ "end ";
/**
* 检查 redisKey 是否上锁(没加锁返回加锁)
*
* @param redisKey redisKey
* @param template template
* @return Boolean
*/
public Boolean isLock(String redisKey, String value, RedisTemplate<Object, Object> template)
{
return lock(redisKey, value, template, RETRY_TIMES);
}
private Boolean lock(String redisKey, String value, RedisTemplate<Object, Object> template, int retryTimes)
{
boolean result = lockKey(redisKey, value, template);
// 循环等待上一个用户锁释放,或者锁超时释放
while (!(result) && retryTimes-- > 0)
{
try
{
log.debug("lock failed, retrying...{}", retryTimes);
Thread.sleep(SLEEP_MILLIS);
}
catch (InterruptedException e)
{
return false;
}
result = lockKey(redisKey, value, template);
}
return result;
}
/**
* 加锁
* @param key
* @param value
* @param template
* @return
*/
private Boolean lockKey(final String key, final String value, RedisTemplate<Object, Object> template)
{
try
{
Boolean nativeLock = template.opsForValue().setIfAbsent(key,value, Duration.ofSeconds(TIMEOUT_MILLIS));
System.out.println("加锁成功:"+nativeLock);
return nativeLock;//*/
}
catch (Exception e)
{
log.info("lock key fail because of ", e);
return false;
}
}
/**
* 释放分布式锁资源(解锁)
*
* @param redisKey key
* @param value value
* @param template redis
* @return Boolean
*/
public Integer releaseLock(String redisKey, String value, RedisTemplate<Object, Object> template)
{
try
{
List<Object> list = new CopyOnWriteArrayList<>();
list.add(redisKey);
Integer result = template.execute(new DefaultRedisScript<>(UNLOCK_LUA,Integer.class), list, value);
return result;//*/
}
catch (Exception e)
{
log.info("release lock fail because of ", e);
return 0;
}
}
}
注意,我这里使用@Component注解来修饰这个类的。
2:测试一下
(1):引入POM依赖
<dependency>
<groupId>com.modules</groupId>
<artifactId>utils</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
(2):在项目启动类上添加包扫描(这一步很重要)
(1):@SpringBootApplication(scanBasePackages = "com.xxx")
(2):@ComponentScan(basePackages = "com.xxx")
(3):这个注解是加在普通方法上的。
@Import({
// 这里写上注解的切面类
})
(3):在代码中调用:
@Resource
private RedisTemplate<Object, Object> redisTemplate;
@Resource
private ArticleDao articleDao;
/**
* 注入自定义stater 的 redisutil工具类
*/
@Resource
private RedisUtil redisUtil;
/**
* 注入自定义stater 的 RedisLuaUtils工具类
*/
@Resource
private RedisLuaUtilsStater redisLuaUtilsStater;
@GetMapping("java/testRedis")
public void testRedis()
{
// 使用多线程来模拟多用户并发操作
for (int i = 0; i < 20; i++)
{
final int temp = i;
new Thread(() -> {
System.out.println("开始创建订单:"+temp);
// 生成唯一uuid
String uuid = UUID.randomUUID().toString();
// 上锁
Boolean isLock = RedisLuaUtilsStater.isLock("Locks", uuid, redisTemplate);
if (!isLock)
{
// System.out.println("锁已经被占用:"+temp);
}
else
{ // 获取到锁,做对应的处理。
System.out.println("获取到锁!"+temp);
Boolean res = redisUtil.set("tests", "test:"+temp);
String username = "redis";
// 将记录写入数据库,这一步可以换成其他操作
String ip = "0.0.0.0";
Browse browse = new Browse();
browse.setUsername(username.toString());
browse.setArticleTitle("test:"+temp);
browse.setIp(ip);
browse.setIsWeixin((byte) '0');
articleDao.addBrowse(browse);
System.out.println("redis写入状态:"+res+" - "+temp);
}
/*try
{
Thread.sleep((long) 1);
}
catch (InterruptedException e)
{
throw new RuntimeException(e);
}//*/
//一定要记得释放锁,否则会出现问题,解锁会校验uuid,校验失败,解锁也就失败
Integer res = RedisLuaUtilsStater.releaseLock("Locks", uuid, redisTemplate);
System.out.println("redis-lock解锁状态:"+res+" - "+temp);
}).start();
}
}
六:Springboot3自定义stater
上边所有的例子都是在springboot2框架中实现的。
Springboot3框架中的自定义stater与springboot2框架是不一样的。具体如下所示:
**1) ** 在recourse目录下创建META-INF目录
**2) ** 在META-INF目录下创建spring目录
**3) ** 在spring目录下创建org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,内容如下:
com.modules.es.config.ElasticSearchConfig
多个配置文件,配置多行就可以了。
七:自定义stater项目POM依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
有了这两个依赖,让引用starter的人知道,你这个starter有哪些属性,在设置属性时,会有提示。
以上大概就是我在项目中自定义stater的全过程。
有好的建议,请在下方输入你的评论。