SpringBoot结合XXLJOB实现定时任务
前言
上篇文章我们介绍了 Quartz 的使用,当时实现了两个简单的需求,不过最后我们总结的时候也提到 Quartz 有不少缺点,代码侵入太严重,所以本篇将介绍 xxl-job 这个定时任务框架。Quartz的不足
Quartz 的不足:Quartz 作为开源任务调度中的佼佼者,是任务调度的首选。但是在集群环境中,Quartz采用API的方式对任务进行管理,这样存在以下问题:通过调用API的方式操作任务,不人性化。需要持久化业务的 QuartzJobBean 到底层数据表中,系统侵入性相当严重。调度逻辑和QuartzJobBean耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务。Xxl-job介绍
官方说明:XXL-JOB 是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
通俗来讲:XXL-JOB 是一个任务调度框架,通过引入 XXL-JOB 相关的依赖,按照相关格式撰写代码后,可在其可视化界面进行任务的启动,执行,中止以及包含了日志记录与查询和任务状态监控。
更多详细介绍推荐阅读官方文档。项目实践Spring Boot集成XXL-JOB
Spring Boot 集成 XXL-JOB 主要分为以下两步:配置运行调度中心(xxl-job-admin)配置运行执行器项目
xxl-job-admin 可以从源码仓库中下载代码,代码地址有两个:GitHub:github.com/xuxueli/xxl…Gitee:gitee.com/xuxueli0323…
下载完之后,在 doc/db 目录下有数据库脚本 tables_xxl_job.sql,执行下脚本初始化调度数据库 xxl_job,如下图所示:
配置调度中心
将下载的源码解压,用 IDEA 打开,我们需要修改一下 xxl-job-admin 中的一些配置。(我这里下载的是最新版 2.3.1)
1、修改 application.properties,主要是配置一下 datasource 以及 email,其他不需要改变。### xxl-job, datasource spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver ### xxl-job, email spring.mail.host=smtp.qq.com spring.mail.port=25 spring.mail.username=1739468244@qq.com spring.mail.from=1739468244@qq.com # 此处不是邮箱登录密码,而是开启SMTP服务后的授权码 spring.mail.password=xxxxx 复制代码
2、修改 logback.xml,配置日志输出路径,我是在解压的 xxl-job-2.3.1 项目包中新建了一个 logs 文件夹。 复制代码
然后启动项目,正常启动后,访问地址为:http://localhost:8080/xxl-job-admin,默认的账户为 admin,密码为 123456,访问后台管理系统后台。
这样就表示调度中心已经搞定了,下一步就是创建执行器项目。创建执行器项目
本项目与 Quartz 项目用的业务表和业务逻辑都一样,所以引入的依赖会比较多。环境配置
1、引入依赖: org.springframework.boot spring-boot-starter-parent 2.6.3 1.8 1.2.73 5.5.1 8.0.19 1.4.2.Final 1.18.20 1.1.18 1.6.9 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-aop com.xuxueli xxl-job-core 2.3.1 com.baomidou mybatis-plus-boot-starter 3.5.1 com.baomidou mybatis-plus 3.5.1 mysql mysql-connector-java ${mysql.version} runtime com.alibaba druid-spring-boot-starter ${druid.version} org.projectlombok lombok 1.18.20 com.alibaba.fastjson2 fastjson2 2.0.12 org.mapstruct mapstruct ${org.mapstruct.version} org.mapstruct mapstruct-processor ${org.mapstruct.version} cn.hutool hutool-all ${hutool.version} org.springdoc springdoc-openapi-ui ${springdoc.version} org.springframework.boot spring-boot-maven-plugin 复制代码
2、application.yml 配置文件server: port: 9090 # xxl-job xxl: job: admin: addresses: http://127.0.0.1:8080/xxl-job-admin # 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册; executor: appname: hresh-job-executor # 执行器 AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册 ip: # 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务"; port: 6666 # ### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口; logpath: /Users/xxx/xxl-job-2.3.1/logs/xxl-job # 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径; logretentiondays: 30 # 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能; accessToken: default_token # 执行器通讯TOKEN [选填]:非空时启用; spring: application: name: xxl-job-practice datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/xxl_job?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false username: root password: root mybatis: mapper-locations: classpath:mapper/*Mapper.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl lazy-loading-enabled: true 复制代码
上述 xxl-job 的 logpath 配置与调度中心的输出日志用的是同一个目录,accessToken 也与调度中心的 xxl.job.accessToken 一致。核心类
1、xxl-job 配置类@Configuration public class XxlJobConfig { @Value("${xxl.job.admin.addresses}") private String adminAddresses; @Value("${xxl.job.executor.appname}") private String appName; @Value("${xxl.job.executor.ip}") private String ip; @Value("${xxl.job.executor.port}") private int port; @Value("${xxl.job.accessToken}") private String accessToken; @Value("${xxl.job.executor.logpath}") private String logPath; @Value("${xxl.job.executor.logretentiondays}") private int logRetentionDays; @Bean public XxlJobSpringExecutor xxlJobExecutor() { // 创建 XxlJobSpringExecutor 执行器 XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor(); xxlJobSpringExecutor.setAdminAddresses(adminAddresses); xxlJobSpringExecutor.setAppname(appName); xxlJobSpringExecutor.setIp(ip); xxlJobSpringExecutor.setPort(port); xxlJobSpringExecutor.setAccessToken(accessToken); xxlJobSpringExecutor.setLogPath(logPath); xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays); // 返回 return xxlJobSpringExecutor; } } 复制代码
2、xxl-job 工具类@Component @RequiredArgsConstructor public class XxlUtil { @Value("${xxl.job.admin.addresses}") private String xxlJobAdminAddress; private final RestTemplate restTemplate; // 请求Url private static final String ADD_INFO_URL = "/jobinfo/addJob"; private static final String REMOVE_INFO_URL = "/jobinfo/removeJob"; private static final String GET_GROUP_ID = "/jobgroup/loadByAppName"; /** * 添加任务 * * @param xxlJobInfo * @param appName * @return */ public String addJob(XxlJobInfo xxlJobInfo, String appName) { Map params = new HashMap<>(); params.put("appName", appName); String json = JSONUtil.toJsonStr(params); String result = doPost(xxlJobAdminAddress + GET_GROUP_ID, json); JSONObject jsonObject = JSON.parseObject(result); Map map = (Map) jsonObject.get("content"); Integer groupId = (Integer) map.get("id"); xxlJobInfo.setJobGroup(groupId); String xxlJobInfoJson = JSONUtil.toJsonStr(xxlJobInfo); return doPost(xxlJobAdminAddress + ADD_INFO_URL, xxlJobInfoJson); } // 删除job public String removeJob(long jobId) { MultiValueMap map = new LinkedMultiValueMap(); map.add("id", String.valueOf(jobId)); return doPostWithFormData(xxlJobAdminAddress + REMOVE_INFO_URL, map); } /** * 远程调用 * * @param url * @param json */ private String doPost(String url, String json) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity entity = new HttpEntity<>(json, headers); ResponseEntity responseEntity = restTemplate.postForEntity(url, entity, String.class); return responseEntity.getBody(); } private String doPostWithFormData(String url, MultiValueMap map) { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); HttpEntity> entity = new HttpEntity<>(map, headers); ResponseEntity responseEntity = restTemplate.postForEntity(url, entity, String.class); return responseEntity.getBody(); } } 复制代码
此处我们利用 RestTemplate 来远程调用 xxl-job-admin 中的服务,从而实现动态创建定时任务,而不是局限于通过 UI 界面来创建任务。
这里我们用到三个接口,都需要我们在 xxl-job-admin 中手动添加,这样在调用接口时,就不需要登录验证了,这就要求在定义接口时加上一个 PermissionLimit并设置 limit 为 false,那么这样就不用去登录就可以调用接口。
3、修改 JobGroupController,新增 loadByAppName 方法@RequestMapping("/loadByAppName") @ResponseBody @PermissionLimit(limit = false) public ReturnT loadByAppName(@RequestBody Map map) { XxlJobGroup jobGroup = xxlJobGroupDao.loadByAppName(map); return jobGroup != null ? new ReturnT(jobGroup) : new ReturnT(ReturnT.FAIL_CODE, null); } 复制代码
XxlJobGroupDao 文件以及对应的 xml 文件XxlJobGroup loadByAppName(Map map); 复制代码 复制代码
4、修改 JobInfoController,增加 addJob 方法和 removeJob 方法 @RequestMapping("/addJob") @ResponseBody @PermissionLimit(limit = false) public ReturnT addJob(@RequestBody XxlJobInfo jobInfo) { return xxlJobService.add(jobInfo); } @RequestMapping("/removeJob") @ResponseBody @PermissionLimit(limit = false) public ReturnT removeJob(String id) { return xxlJobService.remove(Integer.parseInt(id)); } 复制代码
addJob 方法与 JobInfoController 文件中的 add 方法具体逻辑是一样的,只是换个接口名。 @RequestMapping("/add") @ResponseBody public ReturnT add(XxlJobInfo jobInfo) { return xxlJobService.add(jobInfo); } 复制代码
至此,关于调度中心的修改就结束了。
5、XxlService 创建任务@Service @Slf4j @RequiredArgsConstructor public class XxlService { private final XxlUtil xxlUtil; @Value("${xxl.job.executor.appname}") private String appName; public void addJob(XxlJobInfo xxlJobInfo) { xxlUtil.addJob(xxlJobInfo, appName); long triggerNextTime = xxlJobInfo.getTriggerNextTime(); log.info("任务已添加,将在{}开始执行任务", DateUtils.formatDate(triggerNextTime)); } } 复制代码业务代码
1、UserService,包括用户注册,给用户发送欢迎消息,以及发送天气温度通知。@Service @RequiredArgsConstructor @Slf4j public class UserService { private final UserMapper userMapper; private final UserStruct userStruct; private final WeatherService weatherService; private final XxlService xxlService; /** * 假设有这样一个业务需求,每当有新用户注册,则1分钟后会给用户发送欢迎通知. * * @param userRequest 用户请求体 */ @Transactional public void register(UserRequest userRequest) { if (Objects.isNull(userRequest) || isBlank(userRequest.getUsername()) || isBlank(userRequest.getPassword())) { BusinessException.fail("账号或密码为空!"); } User user = userStruct.toUser(userRequest); userMapper.insert(user); LocalDateTime scheduleTime = LocalDateTime.now().plusMinutes(1L); XxlJobInfo xxlJobInfo = XxlJobInfo.builder().jobDesc("定时给用户发送通知").author("hresh") .scheduleType("CRON").scheduleConf(DateUtils.getCron(scheduleTime)).glueType("BEAN") .glueType("BEAN") .executorHandler("sayHelloHandler") .executorParam(user.getUsername()) .misfireStrategy("DO_NOTHING") .executorRouteStrategy("FIRST") .triggerNextTime(DateUtils.toEpochMilli(scheduleTime)) .executorBlockStrategy("SERIAL_EXECUTION").triggerStatus(1).build(); xxlService.addJob(xxlJobInfo); } public void sayHelloToUser(String username) { if (StrUtil.isBlank(username)) { log.error("用户名为空"); } User user = userMapper.selectByUserName(username); String message = "Welcome to Java,I am hresh."; log.info(user.getUsername() + " , hello, " + message); } public void pushWeatherNotification() { List users = userMapper.queryAll(); log.info("执行发送天气通知给用户的任务…"); WeatherInfo weatherInfo = weatherService.getWeather(WeatherConstant.WU_HAN); for (User user : users) { log.info(user.getUsername() + "----" + weatherInfo.toString()); } } } 复制代码
2、WeatherService,获取天气温度等信息,这里就不贴代码了。
3、UserController,只有一个用户注册方法@RestController @RequiredArgsConstructor public class UserController { private final UserService userService; @PostMapping("/register") public Result
空喜欢一场而已空欢喜也是一种画饼充饥望梅止渴的暂时满足。清欢也罢,空欢喜也好。此情此景在人间。谁说只应天上有?化蝶也把你来追。比翼双飞柳荫燕,连理叶叶互问寻。若有阴魂也至真,至爱无谁也遗恨。借来
第二天静默小区,岁月静好我在头条搞创作第二期文陈馥清1晨起做核酸早上饿得没心情赖床了,大概是昨晚油水太少。走出房间,一眼就望到窗外洒满了阳光,不远处高低错落的楼栋屋顶沐浴在金灿灿的阳光中,分外显眼。心情便
一个60多岁老人4000字的童年回忆山沟儿农村小王原创作品,打击抄袭狗以下文字是一位60多岁的老人对童年回忆的述说光阴似箭,不知不觉60年一闪而过了。今天看电视剧里的朱重八汤和徐达他们小时候放牛时候穷孩子们的快乐时光
夜雨丨刘友洪与泥土对话与泥土对话刘友洪该用什么样的语言来与泥土对话呢?我不知道。但我确信,用水泥沥青或钢筋混凝土肯定是不行的,那生硬冰冷僵直的身板,是对泥土莫大的伤害。但人们偏偏爱用坚硬的水泥沥青或钢筋
真正深爱你的女人,会有三不敢一个女人在和你来往时,不把你当回事,不在意你的感受,敢做对不起你的事情,敢践踏你的尊严,敢随意和你提散伙,显然她对你用情不深,不在意你们这段感情。爱你的女人,是会愿意对你生死相许的
人的命运为何不尽相同?因为只有自助者才会有天助有些人总是说我一定要通过自己的努力赢得属于自己的生活。你若问他们你想要过什么样的生活?很多人,吞吞吐吐说不出个所以然。其实人总会有那么一个阶段,懵懂疲惫无趣的活着。我们迷茫自卑,总
中国古人类是本土起源,还是非洲迁徙而来?人类何时起源,历史上各种说法。考古学根据发掘研究古人类化石,发现人类是由古猿逐渐进化而来的。中国境内古人类起源于何时,何地,生活状况又如何?我国是世界上发现古人类遗址最多的国家之一
啧啧称奇!显微镜下的雪花雪花,一种晶体,结构随温度的变化而变化,其又名未央花和六出,一种美丽的结晶体,它在飘落过程中成团攀联在一起,就形成雪片。生活中有些认真仔细有好奇心的朋友可能会发现这样一个现象,冬天
如果每秒一光年,能突破宇宙边缘吗?如果说一架可以达到一光年每秒的速度的超光速飞行器,那么我们要多久到达太空的尽头?一分钟后,我们已经离开了地球,来到了我们的太阳系,那里充满了五十亿年的活动慧星。四秒钟后,我们抵达了
科学家首次在室温环境下观测到拓扑绝缘体中新量子效应物理学家首次在室温环境下观测到拓扑绝缘体中的新量子效应。普林斯顿大学的研究人员发现,一种由铋和溴元素制成的拓扑绝缘体表现出特殊的量子行为,而这种行为通常只有在高压或者接近于绝对零度
外媒NASA捕捉到太阳的微笑,却预示地磁暴有可能袭击地球来源环球网环球网报道见习记者李诗睿微笑的太阳有可能给地球制造问题,美国全国公共广播电台(NPR)英国卫报美国趣味科学网站等媒体29日消息,当地时间10月26日晚间,美国国家航空航天
一段恋情能走多远,要看能不能提供情绪价值?写在最后我们终究要在关系中得到治愈,当然这种关系未必仅仅指亲密关系。我们与自己的关系与TA人的关系对待事物的关系,其实都可以从中获取情绪价值,只要能让自己内心满足,都是很好的滋养。
心寒的冬天更漫长窗外,寒风呼啸,整整一夜,没有停下来的意思,如同西北利亚的饿狼在吼叫着,一整晚都没有睡意,越来越冷天气,鬼魅魍魉的北风,透过窗户的缝隙,令人窒息的心寒。冰飕飕的,吹得人心发慌。昏黄
成年人最舒服的活法,是不在乎闲言碎语杨绛先生的话,总是说透人生人这一辈子,有人嫉妒你,有人讨厌你,有人羡慕你,有人看起你。不要在乎别人怎么看你,一定要告诉自己,身体是自己的,一定要爱惜心情也是自己的,一定要顾及,别为
这4个因素,会让无数人终生烦恼痛苦!化解办法在这里为富不仁为什么可怕?因为它会扰乱人心,败坏风气。曾仕强每日箴言全文共3000字,深度阅读需9分钟,受益终生着相是一个佛教术语,意思是执着于外相虚相或个体意识而偏离了本质。而在道家来
季节换容颜,静处去心尘时光依旧不紧不慢四季无需提醒总是懂得在恰当的时节更换衣装装扮容颜岁月恋不住时光悄悄把衣装轮换四季容颜四季模样时光弹指去如光回首无是处心落千千尘脸颊唯留皱纹沟壑红尘往事如云烟可忆难寻
又见一帘幽梦,紫菱的费云帆也丢了都说被追的时候才是一个女孩在爱情里最幸福的时候,一切都还未曾开始,你不懂事,不听话,不乖,在他眼里都是可爱,流眼泪,会心疼。一朝一暮,一颦一笑,爱是每个当下眼神里的细节,爱意。上大
早上好,今天是2022年12月01日,星期四,农历十一月初八太阳早安今天太阳十二月你好,愿我们不虚度年华,不碌碌无为,只愿我们好好把握十二月,愿2022最后一个月,所念皆所愿,所求皆所得,愿我们有钱没钱,都能回家过年。早安!过去的十一月,不
跳舞的老者黄昏的时分,寂静的色彩笼罩着广场上的一切,粗壮的树木郁郁葱葱,却沉默不如。夕阳悬在天边,即将落下,却把最后的余辉洒入人们的心中,让广场上的人们振奋起精神。广场上回旋着门温和的舞曲,
十二月文案即便年年不见,也要岁岁平安1。风已经有了冬天的味道,我的意思是这一年又快过完了。2。不要担心,十一月遗失的爱,十二月都会带给你。3。十二月,祝眉开舒展,顺问冬安。4。透过十二月的窗口看到了新年的烟火。5。希
11月30日最美早上好问候朋友美句动态美图,用好心情迎接每一天1。一声问候,送来温馨甜蜜一条信息,捎去我万般心意将心停泊在彩云升起的港湾,偷偷把幸福刻在你的心间用一缕友谊的丝线,将你我紧紧绑在岁月变迁的终点!朋友,真诚地祝愿你永远幸福平安!2
金句分享391张小娴说,离别与重逢,是人生不停上演的戏,习惯了,也就不再悲怆。2莫言说,一个人,风尘仆仆地活在这个世界上,要为喜欢自己的人而活着,这才是最好的态度。不要在不喜欢你的人那里丢掉了