这篇文章对最近项目里用的几个知识点简单做个总结,其中包括了动态定时任务,动态注册Bean、动态注册理由三个部分的知识。
之前用过Spring中的定时任务,通过@Scheduled注解就能快速的注册一个定时任务,但有的时候,我们业务上需要动态创建,或者根据配置文件、数据库里的配置去创建定时任务。这里有两种思路,一种是自己实现定时任务调度器或者第三方任务调度器如Quartz,另一种是使用Spring内置的定时任务调度器ThreadPoolTaskScheduler,其实很简单,从IOC容器中拿到对应的Bean,然后去注册定时任务即可。下面以动态管理cron任务为例介绍具体的实现方案。
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;
}
}
}
该部分主要是获取调度器实例,然后实现注册、取消、获取列表的方法。
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());
}
}
定时任务的单元测试不好测试,这里首先实现一个需要被执行的任务,任务中会有一个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.");
}
}
动态Bean的场景一开始是为了动态注册路由(Controller),后来发现直接创建实例也可以注册路由,不过这里也还要记录一下,后面很多场景可能会用到。
这里封装了一个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);
}
}
创建一个静态内部类,用来注册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"),"");
}
}
动态路由这个场景是因为项目中有个调用外部接口的单元测试,我又不想用mock方法,就想真实的测试一下HTTP请求的过程。一种是通过@RestController暴露一个接口,另一种就是动态注册路由。
动态注册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);
}
}
创建一个内部类用来定义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"),"");
}
}