java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot动态定时任务和路由

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

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

您可能感兴趣的文章:
阅读全文