目录
前言
通过前面总结我们知道,已经有四种定时任务的创建方法。但是他们都一定的局限性。而开源的xxl-job是一个分布式任务调度平台,他不仅集成简单,开箱即用。而且内置了许多强大的功能,所以是一个很优秀的job集成方案。
整体设计
本demo将承接jwt+shiro+模板模式实现推送作为数据推送端,通过xxl-job控制台管理各个任务,客户端实现我们的拉取业务
xxl-job说明
xxl-job的说明可参考其详细的文档说明xxl-job,而这里只需要下载源码导入,然后运行xxl-job-admin(后台管理),而其执行器里集成我们的拉取业务
这里选择2.3.1版本进行下载和开发
并且只保留如图所示的模块
1 初始化tables_xxl_job.sql
2 修改xxl-job-admin中application.properties中datasource和email
3 启动xxl-job-admin
浏览器输入localhost:8080/xxl-job-admin
用户名-密码 admin-123456
可看到如下界面,即成功
接下来就是xxl-job-executor-sample-springboot执行器里执行我们的拉取业务,首先从标的设计开始
表设计
核心表有两个,首先需要有一个保存最新的token信息表,每次拉取信息都过携带token,其次有一个记录业务接口的信息,用于标记每个业务接口
sys_task_token(token信息表)
CREATE TABLE `sys_task_token` (
`id` int(11) NOT NULL COMMENT '主键',
`token` varchar(50) DEFAULT NULL COMMENT '接口验证密钥',
`date_time` datetime DEFAULT NULL COMMENT '获取token时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
sys_task_datetime(接口配置表)
CREATE TABLE `sys_task_datetime` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`page_index` int(11) DEFAULT NULL COMMENT '页码',
`api_name` varchar(50) DEFAULT NULL COMMENT '接口名称',
`execute_time` datetime DEFAULT NULL COMMENT '创建时间',
`after_min` int(11) DEFAULT NULL COMMENT '接口提前量',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;
-- ----------------------------
-- Records of sys_task_datetime
-- ----------------------------
INSERT INTO `sys_task_datetime` VALUES ('1', '1', 'token', '2021-04-16 14:04:13', null);
INSERT INTO `sys_task_datetime` VALUES ('2', '1', 'leesin', '2021-04-14 11:17:03', null);
INSERT INTO `sys_task_datetime` VALUES ('3', '1', 'yurnero', '2020-08-18 16:07:01', null);
这里配置了三个接口,分别对应之前的token,盲僧接口,剑圣接口推送
工厂模式设计
以上是简化工厂模式uml图,事实上我们只需要用一个枚举值去控制每个具体xxApiRequest服务,在系统初始化向工厂里注入改组件即可,而实际调用时只需一行代码创建即可
AbstractApiRequest apiRequest = factory.create(TaskEnumUtils.taskDatetimeType.LEESIN.key);
apiRequest.excute();
事实上就可以获得具体子类,这也是oop充满魅力的地方
码上有戏
如图,为项目代码结构
核心代码
使用工厂模式就是为了防止模板发生变化,从而扩展多个产品线或者一个产品线下多个产品
首先是系统
注入服务
@Configuration
public class SystemConfig {
@Resource
private LeesinApiRequest leesinApiRequest;
@Resource
private YurneroApiRequest yurneroApiRequest;
@Bean
public ApiRequestFactory createFactory() {
ApiRequestFactory factory = new ApiRequestFactory();
Map<String, AbstractApiRequest> serviceMap = new HashMap<>(6);
serviceMap.put(TaskEnumUtils.taskDatetimeType.LEESIN.key, leesinApiRequest);
serviceMap.put(TaskEnumUtils.taskDatetimeType.YURNERO.key, yurneroApiRequest);
factory.setServiceMap(serviceMap);
return factory;
}
}
如果有需要,可以扩展注入服务到工厂里的serverMap中
枚举核心配置
public class TaskEnumUtils {
public enum taskDatetimeType {
TOKEN("token"), LEESIN("leesin"), YURNERO("yurnero");
public String key;
private taskDatetimeType(String key) {
this.key = key;
}
}
public static EnumMap taskDatetimeTypeEnum = new EnumMap(taskDatetimeType.class) {
{
put(taskDatetimeType.TOKEN, "token");
put(taskDatetimeType.LEESIN, "盲僧接口");
put(taskDatetimeType.YURNERO, "剑圣接口");
}
};
public enum taskApiUrlType {
LEESIN("leesin"), YURNERO("yurnero");
public String key;
private taskApiUrlType(String key) {
this.key = key;
}
public static String getValueByKey(String key) {
for (taskApiUrlType item : values()) {
if (item.key.equals(key)) {
return (String) taskApiUrlTypeEnum.get(item);
}
}
return null;
}
}
public static EnumMap taskApiUrlTypeEnum = new EnumMap(taskApiUrlType.class) {
{
put(taskApiUrlType.LEESIN, "getinformation/leesinlist");
put(taskApiUrlType.YURNERO, "getinformation/yurnerolist");
}
};
}
主要控制接口的相对url和服务的key
ResponseModel作为顶层返回类,通过向下转型可以获取任意子类类型,并且在模板中声明为抽象,交给子类
public abstract class AbstractApiRequest {
@Resource
private RestTemplate restTemplate;
@Resource
private ISysTaskTokenService iTskTokenService;
@Resource
private ApiTokenRequest tokenRequest;
@Resource
private ISysTaskDatetimeService taskDatetimeService;
protected volatile String param;
/**
* 一次性插入数据的数量
*/
protected final int limit = 50;
@Retryable(value = {RemoteAccessException.class}, maxAttempts = 3, backoff = @Backoff(delay = 5000L, multiplier = 1))
public void excute() throws IOException {
//1 获取token
String token = iTskTokenService.getToken().getToken();
if (token == null || "".equals(token)) {
XxlJobLogger.log("token为空!");
tokenRequest.excute();
}
//2 获取任务执行配置
String apiName = initApiName();
SysTaskDatetime tskDateTime = taskDatetimeService.getByApiName(apiName);
if (tskDateTime == null) {
XxlJobLogger.log("接口【" + apiName + "】未配置或不存在");
return;
}
//执行时间
String executetime = DateUtil.format(tskDateTime.getExecuteTime(), "yyyy-MM-dd'T'HH:mm:ss");
//当前时间
String currenttime = DateUtil.format(new Date(), "yyyy-MM-dd'T'HH:mm:ss");
//读取的首页数
int pageIndex = tskDateTime.getPageIndex();
//读取的下一页
int nextPage = pageIndex + 1;
// 是否有异常
boolean isE = false;
while (pageIndex <= nextPage) {
ResponseEntity<? extends ResponseModel> responseEntity = null;
Map<String, Object> map = getStringObjectMap(executetime, currenttime, pageIndex);
Map<String, Object> requestExtMap = getRequestExtMap();
if (null != requestExtMap) {
map.putAll(requestExtMap);
}
String relativeApiUrl = initRelativeApiUrl();
Class<? extends ResponseModel> resultMappingClass = initMappingClass();
try {
responseEntity = restTemplate.postForEntity(ApiParam.API_BASE_URL +
ApiRequestUrlUtil.getRequestUrl(relativeApiUrl, token)
, ApiRequestUrlUtil.getHttpEntity(map, apiName), resultMappingClass);
if (null != responseEntity.getBody()) {
int result = responseEntity.getBody().getResult();
if (result == 1) {
if (responseEntity.getBody().getPagesize() > 0) {
insertOrUpdate(responseEntity.getBody());
pageIndex++;
if (responseEntity.getBody().getNextpage() == 0 || responseEntity.getBody().getNextpage() == null) {
nextPage = 0;
break;
} else {
nextPage = responseEntity.getBody().getNextpage();
}
} else {
pageIndex = 1;
break;
}
} else {
XxlJobLogger.log("任务名称:{}获取数据异常,异常信息为{}", apiName, responseEntity.getBody().getMsg());
break;
}
} else {
pageIndex = 1;
break;
}
} catch (RestClientException e) {
isE = true;
XxlJobLogger.log("接口远程异常,{}", e.getMessage());
String exceptionMessage = "调用任务接口【" + apiName + "】异常: " + e.getMessage() + e.getStackTrace().toString();
throw new RemoteAccessException(exceptionMessage);
} catch (Exception e) {
nextPage = 0;
isE = true;
XxlJobLogger.log("接口异常,{}", e.getMessage());
tskDateTime.setPageIndex(pageIndex);
taskDatetimeService.updateById(tskDateTime);
}
}
//3 执行完成后,将页码初始化,并保存当前时间
if (!isE) {
tskDateTime.setPageIndex(1);
tskDateTime.setExecuteTime(new Date());
taskDatetimeService.updateById(tskDateTime);
}
}
@Recover
public void recover(RetryException e) {
XxlJobLogger.log("recovery,{}", e.getMessage());
}
public abstract String initApiName();
public abstract String initRelativeApiUrl();
public abstract Class<? extends ResponseModel> initMappingClass();
public abstract void insertOrUpdate(ResponseModel entity);
private Map<String, Object> getStringObjectMap(String executetime, String currenttime, int pageIndex) {
Map<String, Object> map = new HashMap<>(3);
map.put("pageindex", pageIndex);
map.put("starttime", executetime);
map.put("endtime", currenttime);
return map;
}
public abstract Map<String, Object> getRequestExtMap();
public void setParam(String param) {
this.param = param;
}
}
通过抽象拉取业务的模板方法,将可抽象的方法交由子类去完成,比如initApiName(),initRelativeApiUrl(),initMappingClass()。当然改模板方法可以按需再扩展
具体工厂创建
public class ApiRequestFactory extends Factory {
/**
* 保存注入的所有ApiXXXRequest
*/
private Map<String, AbstractApiRequest> serviceMap;
@Override
protected AbstractApiRequest createApiRequest(String apiName) {
if (StringUtils.isEmpty(apiName) || null == this.serviceMap) {
XxlJobLogger.log("初始化AbstractApiRequest失败");
return null;
}
return serviceMap.get(apiName);
}
public void setServiceMap(Map<String, AbstractApiRequest> serviceMap) {
this.serviceMap = serviceMap;
}
}
而具体的工厂最核心的就是将注入的service按照key去标识,我们只需要声明如下形式就可以调用相应的xxApiRequest,并执行模板方法
AbstractApiRequest apiRequest = factory.create(TaskEnumUtils.taskDatetimeType.LEESIN.key);
apiRequest.excute();
测试
我们需要先启动jwt+shiro+模板模式实现推送(二)中的代码作为推送端,启动xxljob的admin和客户端。然后通过bean模式注册客户端的bean,如下图所示
可分别执行上述盲僧和剑圣接口,即可执行客户端通过 @XxlJob声明的方法,如 @XxlJob(“leesinJobHandler”)
简单说明
当然这里只是一个demo,实际中我们可以扩展许多功能,比如多条产品线的不同业务拉取,同一个产品线的不同产品拉取等