一文详解 springboot 项目启动时异步执行初始化逻辑

你知道的越多,你不知道的越多
点赞再看,养成习惯
如果您有疑问或者见解,欢迎指教:
企鹅:869192208

前言

前面的工作中,为了提高地区数据的响应时间,需要加载全国区划数据到 redis 中缓存起来,这个过程希望在项目时启动。
由于初始化全国区划到 redis 中这个过程是比较耗时的,所以我们可以考虑使用异步执行的方式去实现。

代码实现
定义异步处理工具类
import com.xymy.common.config.ThreadPoolExecutorUtil;
import lombok.extern.slf4j.Slf4j;

import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;

/**
 * <h2>异步处理工具类<h2>
 *
 * @author xymy
 * @create 2023-02-07 16:45
 */
@Slf4j
public enum RunMerger {
    SM;

    public void asyncHandle(String threadName, Runnable ...runs) {
        this.asyncHandle(null, threadName,runs);
    }

    public void asyncHandle(ExecutorService seedPool, String threadName, Runnable ... runs) {
        if (seedPool == null) {
            seedPool = ThreadPoolExecutorUtil.getSeedPool(threadName, runs.length);
        }
        try {
            for (Runnable run : runs) {
                CompletableFuture.runAsync(() -> {
                        run.run();
                    },seedPool).exceptionally(ex -> {
                        log.error("处理异常", ex);
                    return null;
                });
            }
        } finally {
            ThreadPoolExecutorUtil.releaseSeedPool(seedPool);
        }
    }

    public void syncHandle(Runnable ...runs) {
        Arrays.stream(runs).forEach(Runnable::run);
    }

    public void asyncHandle(Runnable ...runs) {
        this.asyncHandle(null, runs);
    }

}
实现 java 线程池
  • 新建 ThreadPoolExecutorUtil 类
import cn.hutool.core.thread.ThreadFactoryBuilder;
import com.xymy.common.utils.DistrKeyGenerator;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;

import java.util.Map;
import java.util.Set;
import java.util.concurrent.*;

/**
 * <h2>线程池<h2>
 *
 * @author xymy
 * @create 2021-11-22 14:24
 */
@Slf4j
public class ThreadPoolExecutorUtil {
    private static Map<String, ExecutorService> executors = new ConcurrentHashMap<>();

    public static ExecutorService getSeedPool(int poolSize) {
        DistrKeyGenerator keyGenerator = new DistrKeyGenerator();
        return getSeedPool(keyGenerator.generateKey() +"-", poolSize);
    }

    public static ExecutorService getSeedPool(String poolName, int poolSize) { // 需手动释放
        if (StringUtils.isBlank(poolName)) {
            DistrKeyGenerator keyGenerator = new DistrKeyGenerator();
            poolName = keyGenerator.generateKey() +"-";
        }
        ExecutorService executorService = executors.get(poolName);
        if (null == executorService) {
            synchronized (ThreadPoolExecutorUtil.class) {
                executorService = executors.get(poolName);
                if (null == executorService) {
                    executorService = init(poolName,poolSize);
                    executors.put(poolName, executorService);
                }
            }
        }
        return executorService;
    }

    public static void releaseSeedPool(ExecutorService executorService) {
        Set<Map.Entry<String, ExecutorService>> entries = executors.entrySet();
        for (Map.Entry<String, ExecutorService> entry : entries) {
            ExecutorService value = entry.getValue();
            if (value == executorService) {
                executors.remove(entry.getKey());
            }
        }
        if (executorService != null) {
            executorService.shutdown();
        }
    }

    public static void releaseSeedPool(String poolName) { // 释放
        ExecutorService executorService = executors.remove(poolName);
        if (executorService != null) {
            executorService.shutdown();
        }
    }

    private ThreadPoolExecutorUtil(){}


    private static ExecutorService init(String poolName, int poolSize) {
        ThreadFactoryBuilder threadFactoryBuilder = new ThreadFactoryBuilder();
        threadFactoryBuilder.setUncaughtExceptionHandler((t, e) -> log.error( "线程[{}]异常:", t.getName(), e));
        threadFactoryBuilder.setNamePrefix("apply-pool-" + poolName);
        threadFactoryBuilder.setDaemon(false);
        return new ThreadPoolExecutor(poolSize,
                poolSize,0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(poolSize*10),
                threadFactoryBuilder.build(),
                new ThreadPoolExecutor.CallerRunsPolicy());
    }

}
  • 新建 DistrKeyGenerator 类,用来生成主键
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>32.1.1-jre</version>
</dependency>
import com.google.common.base.Preconditions;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

/**
 * 主键生成
 * sharding-jdbc核心源码
 * @author xymy
 * @create 2019-07-24 09:37
 */
@Slf4j
@Component
public class DistrKeyGenerator {

    public static final long EPOCH;
    // 自增序列长度
    private static final long SEQUENCE_BITS = 12L;
    // workId的长度(单位是位时长度)
    private static final long WORKER_ID_BITS = 10L;

    private static final long SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1;

    private static final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS;

    private static final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS;
    // 位运算计算workerId的最大值(workerId占10位,那么1向左移10位就是workerId的最大值)
    private static final long WORKER_ID_MAX_VALUE = 1L << WORKER_ID_BITS;


    private long workerId = 0;
    // EPOCH就是起始时间,从2016-11-01 00:00:00开始的毫秒数
    static {
        Calendar calendar = Calendar.getInstance();
        calendar.set(2016, Calendar.NOVEMBER, 1);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        EPOCH = calendar.getTimeInMillis();
    }

    private long sequence;

    private long lastTime;

    public DistrKeyGenerator() {

    }

    public DistrKeyGenerator(long workerId) {
        Preconditions.checkArgument(workerId >= 0L && workerId < WORKER_ID_MAX_VALUE);
        this.workerId = workerId;
    }

    /**
     * Generate key.
     * 生成id
     * @return key type is @{@link Long}.
     */
    public synchronized long generateKey() {
        long currentMillis = getCurrentMillis();
        // 每次取分布式唯一ID的时间不能少于上一次取时的时间
        Preconditions.checkState(lastTime <= currentMillis, "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", lastTime, currentMillis);
        // 如果同一毫秒范围内,那么自增,否则从0开始
        if (lastTime == currentMillis) {
            // 如果自增后的sequence值超过4096,那么等待直到下一个毫秒
            if (0L == (sequence = ++sequence & SEQUENCE_MASK)) {
                currentMillis = waitUntilNextTime(currentMillis);
            }
        } else {
            sequence = 0;
        }
        // 更新lastTime的值,即最后一次获取分布式唯一ID的时间
        lastTime = currentMillis;
        if (log.isDebugEnabled()) {
            log.debug("{}-{}-{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(lastTime)), workerId, sequence);
        }
        // 从这里可知分布式唯一ID的组成部分;
        return ((currentMillis - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (workerId << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
    }
    // 获取下一毫秒的方法:死循环获取当前毫秒与lastTime比较,直到大于lastTime的值;
    private long waitUntilNextTime(final long lastTime) {
        long time = getCurrentMillis();
        while (time <= lastTime) {
            time = getCurrentMillis();
        }
        return time;
    }

    /**
     * Get current millis.
     *
     * @return current millis
     */
    private long getCurrentMillis() {
        return System.currentTimeMillis();
    }

}

新建 AppInit 实现 ApplicationRunner 接口完成启动项目时异步数据初始化
@Component
@Slf4j
public class AppInit implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) {

        RunMerger.SM.asyncHandle(() -> {
            log.info("项目启动完成,开始初始化全国区划数据...");
            //省略数据初始化的代码,此处为耗时的操作
            String countryCode = "xxxxx";
            xxService.initAreaInfoList(countryCode);
            log.info("项目启动完成,完成初始化全国区划数据...");
        });
    }
}

至此,就可以实现 springboot 项目启动时异步执行初始化逻辑。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于SpringBoot项目的测试类,通常会使用注解@SpringBootTest来标记测试类,该注解表示在测试用例中启动Spring应用程序上下文。可以使用@Autowired注解来注入需要测试的类或组件,并使用@Test注解来标记测试方法。例如: ```java import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.test.context.junit4.SpringRunner; import java.util.List; @RunWith(SpringRunner.class) @SpringBootTest public class SpringbootTest { @Autowired private DiscoveryClient discoveryClient; @Test public void NacosTest() { List<String> services = discoveryClient.getServices(); services.forEach(x-> System.out.println(x)); } } ``` 在这个测试类中,使用了@Autowired注解将DiscoveryClient注入到测试类中,并使用@Test注解标记了一个名为NacosTest的测试方法,该方法使用DiscoveryClient来获取服务列表并打印输出。 此外,有时需要在测试用例中模拟环境或添加临时属性配置。可以使用@SpringBootTest注解的属性来实现。例如,可以使用webEnvironment属性来设置测试用例的web环境: ```java @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) public class WebTest { // 测试逻辑 } ``` 还可以使用properties属性来为测试用例添加临时的属性配置: ```java @SpringBootTest(properties = {"test.prop=testValue1"}) public class PropertiesAndArgsTest { @Value("${test.prop}") private String msg; @Test void testProperties(){ System.out.println(msg); } } ``` 这样,就可以在测试用例中使用注入的属性值进行测试。123 #### 引用[.reference_title] - *1* [SpringBoot测试类](https://blog.csdn.net/lixinkuan328/article/details/121396675)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] - *2* *3* [一文了解SpringBoot的单元测试](https://blog.csdn.net/Learning_xzj/article/details/125432871)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值