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

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

这篇文章对最近项目里用的几个知识点简单做个总结,其中包括了动态定时任务,动态注册Bean、动态注册理由三个部分的知识。

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
     */
    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"),"");
    }
}

你可能感兴趣的:(Spring,spring,boot,spring,java)