Springboot(五十二)Springboot自定义starter

63 阅读10分钟

【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<StringObject> redisTemplate;
 
    public RedisUtil(RedisTemplate<StringObject> 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<ObjectObject> template)
    {
 
        return lock(redisKey, value, template, RETRY_TIMES);
    }
 
    private static Boolean lock(String redisKey, String value, RedisTemplate<ObjectObject> 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<ObjectObject> 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<ObjectObject> 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:26379127.0.0.1:26380127.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<ObjectObject> template)
    {
 
        return lock(redisKey, value, template, RETRY_TIMES);
    }
 
    private Boolean lock(String redisKey, String value, RedisTemplate<ObjectObject> 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<ObjectObject> 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<ObjectObject> 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的全过程。

 

有好的建议,请在下方输入你的评论。