SpringBoot动态定时任务、动态Bean、动态路由详解

 更新时间:2023年10月19日 09:15:02   作者:shirukai  
这篇文章主要介绍了SpringBoot动态定时任务、动态Bean、动态路由详解,之前用过Spring中的定时任务,通过@Scheduled注解就能快速的注册一个定时任务,但有的时候,我们业务上需要动态创建,或者根据配置文件、数据库里的配置去创建定时任务,需要的朋友可以参考下

1 动态定时任务

之前用过Spring中的定时任务,通过@Scheduled注解就能快速的注册一个定时任务,但有的时候,我们业务上需要动态创建,或者根据配置文件、数据库里的配置去创建定时任务。这里有两种思路,一种是自己实现定时任务调度器或者第三方任务调度器如Quartz,另一种是使用Spring内置的定时任务调度器ThreadPoolTaskScheduler,其实很简单,从IOC容器中拿到对应的Bean,然后去注册定时任务即可。下面以动态管理cron任务为例介绍具体的实现方案。

1.1 定义CronTask实体

package org.example.dynamic.timed;

import java.util.concurrent.ScheduledFuture;

/**
 * 定时任务
 *
 * @author shirukai
 */
public class CronTask {
    private String id;
    private String cronExpression;
    private ScheduledFuture<?> future;

    private Runnable runnable;

    public String getId() {
        return id;
    }

    public String getCronExpression() {
        return cronExpression;
    }

    public ScheduledFuture<?> getFuture() {
        return future;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    public void setFuture(ScheduledFuture<?> future) {
        this.future = future;
    }

    public static final class Builder {
        private String id;
        private String cronExpression;
        private ScheduledFuture<?> future;

        private Runnable runnable;

        private Builder() {
        }

        public static Builder aCronTask() {
            return new Builder();
        }

        public Builder setId(String id) {
            this.id = id;
            return this;
        }

        public Builder setCronExpression(String cronExpression) {
            this.cronExpression = cronExpression;
            return this;
        }

        public Builder setFuture(ScheduledFuture<?> future) {
            this.future = future;
            return this;
        }

        public Builder setRunnable(Runnable runnable) {
            this.runnable = runnable;
            return this;
        }

        public CronTask build() {
            CronTask cronTask = new CronTask();
            cronTask.id = this.id;
            cronTask.cronExpression = this.cronExpression;
            cronTask.future = this.future;
            cronTask.runnable = this.runnable;
            return cronTask;
        }
    }
}

1.2 实现动态任务调度器

该部分主要是获取调度器实例,然后实现注册、取消、获取列表的方法。

package org.example.dynamic.timed;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;


/**
 * 动态定时任务调度器
 *
 * @author shirukai
 */
@Component
@EnableScheduling
public class CronTaskScheduler {
    @Autowired
    private ThreadPoolTaskScheduler scheduler;

    private final Map<String, CronTask> tasks = new ConcurrentHashMap<>(16);

    /**
     * 注册定时任务
     *
     * @param task       任务的具体实现
     * @param expression cron表达式
     * @return cronTask
     */
    public CronTask register(Runnable task, String expression) {
        final CronTrigger trigger = new CronTrigger(expression);
        ScheduledFuture<?> future = scheduler.schedule(task, trigger);
        final String taskId = UUID.randomUUID().toString();
        CronTask cronTask = CronTask.Builder
                .aCronTask()
                .setId(taskId)
                .setCronExpression(expression)
                .setFuture(future)
                .setRunnable(task)
                .build();
        tasks.put(taskId, cronTask);
        return cronTask;
    }

    /**
     * 取消定时任务
     *
     * @param taskId 任务ID
     */
    public void cancel(String taskId) {
        if (tasks.containsKey(taskId)) {
            CronTask task = tasks.get(taskId);
            task.getFuture().cancel(true);
            tasks.remove(taskId);
        }
    }

    /**
     * 更新定时任务
     *
     * @param taskId     任务ID
     * @param expression cron表达式
     * @return cronTask
     */
    public CronTask update(String taskId, String expression) {
        if (tasks.containsKey(taskId)) {
            CronTask task = tasks.get(taskId);
            task.getFuture().cancel(true);
            final CronTrigger trigger = new CronTrigger(expression);
            ScheduledFuture<?> future = scheduler.schedule(task.getRunnable(), trigger);
            task.setFuture(future);
            tasks.put(taskId, task);
            return task;
        } else {
            return null;
        }
    }

    /**
     * 获取任务列表
     *
     * @return List<CronTrigger>
     */
    public List<CronTask> getAllTasks() {
        return new ArrayList<>(tasks.values());
    }


}

1.3 单元测试

定时任务的单元测试不好测试,这里首先实现一个需要被执行的任务,任务中会有一个CountDownLatch实例,主线程会等待countDown()方法执行,说明定时任务被调度了,如果超时未执行,说明定时任务未生效,此外还会定义一个AtomicInteger的计数器用来统计调用次数。具体的单元测试代码如下:

package org.example.dynamic.timed;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.Assert;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author shirukai
 */
@SpringBootTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class CronTaskSchedulerTest {
    @Autowired
    private CronTaskScheduler scheduler;
    final private static AtomicInteger counter = new AtomicInteger();
    final private static CountDownLatch latch = new CountDownLatch(1);

    private static CronTask task;

    public static class CronTaskRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("The scheduled task is executed.");
            final int count = counter.incrementAndGet();
            if (count <= 1) {
                latch.countDown();
            }
        }
    }

    @Test
    @Order(1)
    void register() throws InterruptedException {
        CronTaskSchedulerTest.task = scheduler.register(new CronTaskRunnable(), "* * * * * ?");
        boolean down = latch.await(2, TimeUnit.SECONDS);
        Assert.isTrue(down, "The scheduled task is not executed within 2 seconds.");

    }

    @Test
    @Order(4)
    void cancel() throws InterruptedException {
        if(CronTaskSchedulerTest.task!=null){
            int minCount = counter.get();
            scheduler.cancel(CronTaskSchedulerTest.task.getId());
            TimeUnit.SECONDS.sleep(5);
            int maxCount = counter.get();
            int deltaCount = maxCount - minCount;
            Assert.isTrue(deltaCount <= 1, "The scheduled task has not been cancelled.");
        }
    }

    @Test
    @Order(2)
    void update() throws InterruptedException {
        if (CronTaskSchedulerTest.task != null) {
            int minCount = counter.get();
            CronTaskSchedulerTest.task = scheduler.update(CronTaskSchedulerTest.task.getId(), "*/2 * * * * ?");
            TimeUnit.SECONDS.sleep(2);
            int maxCount = counter.get();
            int deltaCount = maxCount - minCount;
            Assert.isTrue(deltaCount <= 1, "The scheduled task has not been update.");
        }
    }

    @Test
    @Order(3)
    void getAllTasks() {
        int count = scheduler.getAllTasks().size();
        Assert.isTrue(count==1,"Failed to get all tasks.");
    }
}

2 动态Bean

动态Bean的场景一开始是为了动态注册路由(Controller),后来发现直接创建实例也可以注册路由,不过这里也还要记录一下,后面很多场景可能会用到。

2.1 SpringBeanUtils

这里封装了一个utils用来获取IOC容器中的Bean或者动态注册Bean到IOC中,实现很简单从ApplicationContext中获取BeanFactory,就可以注册Bean了,ApplicationContext通过getBean就可以获取Bean

package org.example.dynamic.bean;

import org.springframework.beans.BeansException;

import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Component;

/**
 * Spirng Bean动态注入
 *
 * @author shirukai
 */
@Component
public class SpringBeanUtils implements ApplicationContextAware {
    private static ConfigurableApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringBeanUtils.context = (ConfigurableApplicationContext) applicationContext;
    }

    public static void register(String name, Object bean) {
        context.getBeanFactory().registerSingleton(name, bean);
    }

    public static <T> T getBean(Class<T> clazz) {
        return context.getBean(clazz);
    }

}

2.2 单元测试

创建一个静态内部类,用来注册Bean,然后通过工具类中的register和getBean方法来验证。

package org.example.dynamic.bean;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.Assert;

import java.util.Objects;

/**
 * @author shirukai
 */
@SpringBootTest
class SpringBeanUtilsTest {
    public static class BeanTest {
        public String hello() {
            return "hello";
        }
    }

    @Test
    void register() {
        SpringBeanUtils.register("beanTest",new BeanTest());
        BeanTest beanTest = SpringBeanUtils.getBean(BeanTest.class);
        Assert.isTrue(Objects.equals(beanTest.hello(),"hello"),"");
    }

}

3 动态路由Controller

动态路由这个场景是因为项目中有个调用外部接口的单元测试,我又不想用mock方法,就想真实的测试一下HTTP请求的过程。一种是通过@RestController暴露一个接口,另一种就是动态注册路由。

3.1 SpringRouterUtils

动态注册controller实现很假单,通过RequestMappingHandlerMapping实例的registerMapping方法注册即可。

package org.example.dynamic.router;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.lang.reflect.Method;

/**
 * 路由注册
 * @author shirukai
 */
@Component
public class SpringRouterUtils implements ApplicationContextAware {
    private static RequestMappingHandlerMapping mapping;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringRouterUtils.mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
    }

    public static void register(RequestMappingInfo mapping, Object handler, Method method){
        SpringRouterUtils.mapping.registerMapping(mapping,handler,method);
    }


}

3.2 单元测试

创建一个内部类用来定义Controller层,然你后通过构造RequestMappingInfo来定义请求路径及方法。

package org.example.dynamic.router;

import org.apache.http.client.fluent.Form;
import org.apache.http.client.fluent.Request;
import org.apache.http.client.fluent.Response;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.TestPropertySource;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.util.pattern.PathPatternParser;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Objects;

import static org.junit.jupiter.api.Assertions.*;

/**
 * @author shirukai
 */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@TestPropertySource(properties = {"server.port=21199"})
class SpringRouterUtilsTest {
    public static class ExampleController {
        @ResponseBody
        public String hello(String name) {
            return "hi," + name;
        }
    }

    @Test
    void register() throws Exception {
        RequestMappingInfo.BuilderConfiguration options = new RequestMappingInfo.BuilderConfiguration();
        options.setPatternParser(new PathPatternParser());
        RequestMappingInfo mappingInfo = RequestMappingInfo
                .paths("/api/v1/hi")
                .methods(RequestMethod.POST)
                .options(options)
                .build();

        Method method = ExampleController.class.getDeclaredMethod("hello", String.class);

        SpringRouterUtils.register(mappingInfo, new ExampleController(), method);
        Response response = Request.Post("http://127.0.0.1:21199/api/v1/hi")
                .bodyForm(Form.form().add("name", "xiaoming").build())
                .execute();

        Assert.isTrue(Objects.equals(response.returnContent().asString(), "hi,xiaoming"),"");
    }
}

到此这篇关于SpringBoot动态定时任务、动态Bean、动态路由详解的文章就介绍到这了,更多相关SpringBoot动态定时任务和路由内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java Socket聊天室编程(一)之利用socket实现聊天之消息推送

    Java Socket聊天室编程(一)之利用socket实现聊天之消息推送

    这篇文章主要介绍了Java Socket聊天室编程(一)之利用socket实现聊天之消息推送的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2016-09-09
  • SpringBoot使用JTA实现对多数据源的事务管理

    SpringBoot使用JTA实现对多数据源的事务管理

    了解事务的都知道,在我们日常开发中单单靠事务管理就可以解决绝大多数问题了,但是为啥还要提出JTA这个玩意呢,到底JTA是什么呢?他又是具体来解决啥问题的呢?本文小编就给大家介绍一下如何在Spring Boot中使用JTA实现对多数据源的事务管理
    2023-11-11
  • Spring Security自定义异常 AccessDeniedHandler不生效解决方法

    Spring Security自定义异常 AccessDeniedHandler不生效解决方法

    本文主要介绍了Spring Security自定义异常 AccessDeniedHandler不生效解决方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-07-07
  • Spring Data JPA踩坑记录(@id @GeneratedValue)

    Spring Data JPA踩坑记录(@id @GeneratedValue)

    这篇文章主要介绍了Spring Data JPA踩坑记录(@id @GeneratedValue),具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-07-07
  • Java sort集合排序的两种方式解析

    Java sort集合排序的两种方式解析

    这篇文章主要介绍了Java sort集合排序的两种方式解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-11-11
  • 浅析Java的Spring框架中IOC容器容器的应用

    浅析Java的Spring框架中IOC容器容器的应用

    这篇文章主要介绍了Java的Spring框架中IOC容器容器的应用,包括BeanFactory容器和ApplicationContext容器的介绍,需要的朋友可以参考下
    2015-12-12
  • Java中获取List中最后一个元素的三种方法

    Java中获取List中最后一个元素的三种方法

    在Java编程中我们经常需要获取一个List集合中的最后一个元素,这篇文章主要给大家介绍了关于Java中获取List中最后一个元素的三种方法,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2023-12-12
  • 详解Java中Duration类的使用方法

    详解Java中Duration类的使用方法

    Duration类通过秒和纳秒相结合来描述一个时间量,最高精度是纳秒。本文将通过示例详细为大家讲讲Duration类的使用,需要的可以参考一下
    2022-05-05
  • 利用Spring MVC+Mybatis实现Mysql分页数据查询的过程详解

    利用Spring MVC+Mybatis实现Mysql分页数据查询的过程详解

    这篇文章主要给大家介绍了关于利用Spring MVC+Mybatis实现Mysql分页数据查询的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面跟着小编来一起学习学习吧。
    2017-08-08
  • java框架基础之SPI机制实现及源码解析

    java框架基础之SPI机制实现及源码解析

    这篇文章主要为大家介绍了java框架基础之SPI机制实现及源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09

最新评论