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动态定时任务和路由内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • @RequestAttribute和@RequestParam注解的区别及说明

    @RequestAttribute和@RequestParam注解的区别及说明

    这篇文章主要介绍了@RequestAttribute和@RequestParam注解的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-05-05
  • SpringBoot整合微信登录功能的实现方案

    SpringBoot整合微信登录功能的实现方案

    今天通过本文给大家分享微信登录与SpringBoot整合过程,微信扫描登录实现代码知道扫描后点击登录的全部过程,本文给大家介绍的非常详细,需要的朋友可以参考下
    2021-10-10
  • Spring中的@Value和@PropertySource注解详解

    Spring中的@Value和@PropertySource注解详解

    这篇文章主要介绍了Spring中的@Value和@PropertySource注解详解,@PropertySource:读取外部配置文件中的key-value保存到运行的环境变量中,本文提供了部分实现代码,需要的朋友可以参考下
    2023-11-11
  • Mybatis基于注解实现多表查询功能

    Mybatis基于注解实现多表查询功能

    这篇文章主要介绍了Mybatis基于注解实现多表查询功能,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-09-09
  • java实现波雷费密码算法示例代码

    java实现波雷费密码算法示例代码

    这篇文章主要介绍了java实现波雷费密码算法示例代码,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-01-01
  • java开发_图片截取工具实现原理

    java开发_图片截取工具实现原理

    本文将详细介绍java开发_图片截取工具实现原理,需要了解的朋友可以参考下
    2012-11-11
  • Java实现企业微信消息推送功能的详细步骤

    Java实现企业微信消息推送功能的详细步骤

    这篇文章主要介绍了Java实现企业微信消息推送功能,本文图文实例代码相结合给大家介绍的非常详细,需要的朋友可以参考下
    2022-04-04
  • Java中的OpenTracing使用实例

    Java中的OpenTracing使用实例

    这篇文章主要介绍了Java中的OpenTracing使用实例,主要的OpenTracing API将所有主要组件声明为接口以及辅助类,例如Tracer,Span,SpanContext,Scope,ScopeManager,Format(用映射定义通用的SpanContext注入和提取格式),需要的朋友可以参考下
    2024-01-01
  • springboot实现对注解的切面案例

    springboot实现对注解的切面案例

    这篇文章主要介绍了springboot实现对注解的切面过程,首先定义一个注解、再编写对注解的切面只是记录的执行时间和打印方法,可以实现其他逻辑,需要的朋友可以参考一下
    2022-01-01
  • Java预览PDF时的文件名称问题及解决

    Java预览PDF时的文件名称问题及解决

    这篇文章主要介绍了Java预览PDF时的文件名称问题及解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01

最新评论