数据拉取之xxl-job+工厂模式+token实现拉取(三)

目录

前言

通过前面总结我们知道,已经有四种定时任务的创建方法。但是他们都一定的局限性。而开源的xxl-job是一个分布式任务调度平台,他不仅集成简单,开箱即用。而且内置了许多强大的功能,所以是一个很优秀的job集成方案。

整体设计

本demo将承接jwt+shiro+模板模式实现推送作为数据推送端,通过xxl-job控制台管理各个任务,客户端实现我们的拉取业务

xxl-job说明

xxl-job的说明可参考其详细的文档说明xxl-job,而这里只需要下载源码导入,然后运行xxl-job-admin(后台管理),而其执行器里集成我们的拉取业务
这里选择2.3.1版本进行下载和开发
并且只保留如图所示的模块
xxl-job

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,实际中我们可以扩展许多功能,比如多条产品线的不同业务拉取,同一个产品线的不同产品拉取等

源码地址

友情提示,需先下载推送端,在下载本demo代码github地址

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值