完成 xxl-job 的接入

This commit is contained in:
YunaiV 2022-06-25 10:18:04 +08:00
parent 4381d938be
commit 3774afe553
27 changed files with 284 additions and 715 deletions

View File

@ -34,6 +34,7 @@
<!-- Config 配置中心相关 --> <!-- Config 配置中心相关 -->
<apollo.version>1.9.2</apollo.version> <apollo.version>1.9.2</apollo.version>
<!-- Job 定时任务相关 --> <!-- Job 定时任务相关 -->
<xxl-job.version>2.3.1</xxl-job.version>
<!-- 服务保障相关 --> <!-- 服务保障相关 -->
<lock4j.version>2.2.0</lock4j.version> <lock4j.version>2.2.0</lock4j.version>
<resilience4j.version>1.7.1</resilience4j.version> <resilience4j.version>1.7.1</resilience4j.version>
@ -251,6 +252,11 @@
<!-- Config 配置中心相关 --> <!-- Config 配置中心相关 -->
<!-- Job 定时任务相关 --> <!-- Job 定时任务相关 -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>${xxl-job.version}</version>
</dependency>
<dependency> <dependency>
<groupId>cn.iocoder.cloud</groupId> <groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-job</artifactId> <artifactId>yudao-spring-boot-starter-job</artifactId>

View File

@ -1,13 +1,10 @@
package cn.iocoder.yudao.framework.tenant.config; package cn.iocoder.yudao.framework.tenant.config;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect; import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnoreAspect;
import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor; import cn.iocoder.yudao.framework.tenant.core.db.TenantDatabaseInterceptor;
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob; import cn.iocoder.yudao.framework.tenant.core.job.TenantJobAspect;
import cn.iocoder.yudao.framework.tenant.core.job.TenantJobHandlerDecorator;
import cn.iocoder.yudao.framework.tenant.core.mq.TenantChannelInterceptor; import cn.iocoder.yudao.framework.tenant.core.mq.TenantChannelInterceptor;
import cn.iocoder.yudao.framework.tenant.core.mq.TenantFunctionAroundWrapper; import cn.iocoder.yudao.framework.tenant.core.mq.TenantFunctionAroundWrapper;
import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter; import cn.iocoder.yudao.framework.tenant.core.security.TenantSecurityWebFilter;
@ -19,6 +16,7 @@ import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import cn.iocoder.yudao.module.system.api.tenant.TenantApi; import cn.iocoder.yudao.module.system.api.tenant.TenantApi;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import com.xxl.job.core.executor.XxlJobExecutor;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -103,19 +101,25 @@ public class YudaoTenantAutoConfiguration {
@Override @Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (!(bean instanceof JobHandler)) { if (!(bean instanceof XxlJobExecutor)) {
return bean; return bean;
} }
// TenantJob 注解的情况下才会进行处理 // // TenantJob 注解的情况下才会进行处理
if (!AnnotationUtil.hasAnnotation(bean.getClass(), TenantJob.class)) { // if (!AnnotationUtil.hasAnnotation(bean.getClass(), TenantJob.class)) {
return bean; // return bean;
} // }
//
// 使用 TenantJobHandlerDecorator 装饰 // // 使用 TenantJobHandlerDecorator 装饰
return new TenantJobHandlerDecorator(tenantFrameworkService, (JobHandler) bean); // return new TenantJobHandlerDecorator(tenantFrameworkService, (JobHandler) bean);
return bean;
} }
}; };
} }
@Bean
public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) {
return new TenantJobAspect(tenantFrameworkService);
}
} }

View File

@ -0,0 +1,76 @@
package cn.iocoder.yudao.framework.tenant.core.job;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Aspect
@RequiredArgsConstructor
@Slf4j
public class TenantJobAspect {
private final TenantFrameworkService tenantFrameworkService;
@Around("@annotation(xxlJob)")
public Object around(ProceedingJoinPoint joinPoint, XxlJob xxlJob) throws Throwable {
// 如果非多租户 Job则跳过
TenantJob tenantJob = getClassAnnotation(joinPoint, TenantJob.class);
if (tenantJob == null) {
return joinPoint.proceed();
}
// 如果是多租户 Job则会按照租户逐个执行 Job 的逻辑
execute(joinPoint, xxlJob);
return null; // JobHandler 无返回
}
private void execute(ProceedingJoinPoint joinPoint, XxlJob xxlJob) {
// 获得租户列表
List<Long> tenantIds = tenantFrameworkService.getTenantIds();
if (CollUtil.isEmpty(tenantIds)) {
return;
}
// 逐个租户执行 Job
Map<Long, String> results = new ConcurrentHashMap<>();
tenantIds.parallelStream().forEach(tenantId -> { // TODO 芋艿先通过 parallel 实现并行1多个租户是一条执行日志2异常的情况
TenantUtils.execute(tenantId, () -> {
try {
joinPoint.proceed();
} catch (Throwable e) {
results.put(tenantId, ExceptionUtil.getRootCauseMessage(e));
// 打印异常
XxlJobHelper.log(StrUtil.format("[多租户({}) 执行任务({}),发生异常:{}]",
tenantId, xxlJob.value(), ExceptionUtils.getStackTrace(e)));
}
});
});
// 如果 results 非空说明发生了异常标记 XXL-Job 执行失败
if (CollUtil.isNotEmpty(results)) {
XxlJobHelper.handleFail(JsonUtils.toJsonString(results));
}
}
@SuppressWarnings("SameParameterValue")
private static <T extends Annotation> T getClassAnnotation(ProceedingJoinPoint joinPoint, Class<T> annotationClass) {
return ((MethodSignature) joinPoint.getSignature()).getMethod().getDeclaringClass().getAnnotation(annotationClass);
}
}

View File

@ -1,58 +0,0 @@
package cn.iocoder.yudao.framework.tenant.core.job;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
import lombok.AllArgsConstructor;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 多租户 JobHandler 装饰器
* 任务执行时会按照租户逐个执行 Job 的逻辑
*
* 注意需要保证 JobHandler 的幂等性因为 Job 因为某个租户执行失败重试时之前执行成功的租户也会再次执行
*
* @author 芋道源码
*/
@AllArgsConstructor
public class TenantJobHandlerDecorator implements JobHandler {
private final TenantFrameworkService tenantFrameworkService;
/**
* 被装饰的 Job
*/
private final JobHandler jobHandler;
@Override
public final String execute(String param) throws Exception {
// 获得租户列表
List<Long> tenantIds = tenantFrameworkService.getTenantIds();
if (CollUtil.isEmpty(tenantIds)) {
return null;
}
// 逐个租户执行 Job
Map<Long, String> results = new ConcurrentHashMap<>();
tenantIds.parallelStream().forEach(tenantId -> { // TODO 芋艿先通过 parallel 实现并行1多个租户是一条执行日志2异常的情况
try {
// 设置租户
TenantContextHolder.setTenantId(tenantId);
// 执行 Job
String result = jobHandler.execute(param);
// 添加结果
results.put(tenantId, result);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
TenantContextHolder.clear();
}
});
return JsonUtils.toJsonString(results);
}
}

View File

@ -12,10 +12,7 @@
<packaging>jar</packaging> <packaging>jar</packaging>
<name>${project.artifactId}</name> <name>${project.artifactId}</name>
<description>任务拓展 <description>任务拓展,基于 XXL-Job 实现</description>
1. 定时任务,基于 Quartz 拓展
2. 异步任务,基于 Spring Async 拓展
</description>
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<dependencies> <dependencies>
@ -24,10 +21,22 @@
<artifactId>yudao-common</artifactId> <artifactId>yudao-common</artifactId>
</dependency> </dependency>
<!-- Job 定时任务相关 --> <!-- Spring 核心 -->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId> <artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<!-- Job 相关 -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
</dependency> </dependency>
<!-- 工具类相关 --> <!-- 工具类相关 -->

View File

@ -0,0 +1,99 @@
package cn.iocoder.yudao.framework.quartz.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;
import javax.validation.Valid;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* XXL-Job 配置类
*/
@ConfigurationProperties("xxl.job")
@Validated
@Data
public class XxlJobProperties {
/**
* 是否开启默认为 true 关闭
*/
private Boolean enabled = true;
/**
* 访问令牌
*/
private String accessToken;
/**
* 控制器配置
*/
@NotNull(message = "控制器配置不能为空")
private AdminProperties admin;
/**
* 执行器配置
*/
@NotNull(message = "执行器配置不能为空")
private ExecutorProperties executor;
/**
* XXL-Job 调度器配置类
*/
@Data
@Valid
public static class AdminProperties {
/**
* 调度器地址
*/
@NotEmpty(message = "调度器地址不能为空")
private String addresses;
}
/**
* XXL-Job 执行器配置类
*/
@Data
@Valid
public static class ExecutorProperties {
/**
* 默认端口
*
* 这里使用 -1 表示随机
*/
private static final Integer PORT_DEFAULT = -1;
/**
* 默认日志保留天数
*
* 如果想永久保留则设置为 -1
*/
private static final Integer LOG_RETENTION_DAYS_DEFAULT = 30;
/**
* 应用名
*/
@NotEmpty(message = "应用名不能为空")
private String appName;
/**
* 执行器的 IP
*/
private String ip;
/**
* 执行器的 Port
*/
private Integer port = PORT_DEFAULT;
/**
* 日志地址
*/
@NotEmpty(message = "日志地址不能为空")
private String logPath;
/**
* 日志保留天数
*/
private Integer logRetentionDays = LOG_RETENTION_DAYS_DEFAULT;
}
}

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.framework.quartz.config;
import cn.iocoder.yudao.framework.quartz.core.scheduler.SchedulerManager;
import org.quartz.Scheduler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* 定时任务 Configuration
*/
@Configuration
@EnableScheduling // 开启 Spring 自带的定时任务
public class YudaoQuartzAutoConfiguration {
@Bean
public SchedulerManager schedulerManager(Scheduler scheduler) {
return new SchedulerManager(scheduler);
}
}

View File

@ -1,45 +1,35 @@
package cn.iocoder.mall.xxljob.config; package cn.iocoder.yudao.framework.quartz.config;
import com.xxl.job.core.executor.XxlJobExecutor; import com.xxl.job.core.executor.XxlJobExecutor;
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor; import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.slf4j.Logger; import lombok.extern.slf4j.Slf4j;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.util.Objects;
/** /**
* XXL-Job 自动配置类 * XXL-Job 自动配置类
*
* @author 芋道源码
*/ */
@Configuration @Configuration
@ConditionalOnClass(XxlJobSpringExecutor.class) @ConditionalOnClass(XxlJobSpringExecutor.class)
@ConditionalOnProperty(prefix = "xxl.job", name = "enabled", havingValue = "true", matchIfMissing = true) @ConditionalOnProperty(prefix = "xxl.job", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties({XxlJobProperties.class}) @EnableConfigurationProperties({XxlJobProperties.class})
public class XxlJobAutoConfiguration { @EnableScheduling // 开启 Spring 自带的定时任务
@Slf4j
private static final Logger LOGGER = LoggerFactory.getLogger(XxlJobAutoConfiguration.class); public class YudaoXxlJobAutoConfiguration {
private final XxlJobProperties properties;
public XxlJobAutoConfiguration(XxlJobProperties properties) {
this.properties = properties;
}
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public XxlJobExecutor xxlJobExecutor() { public XxlJobExecutor xxlJobExecutor(XxlJobProperties properties) {
LOGGER.info("初始化 XXL-Job 执行器的配置"); log.info("[xxlJobExecutor][初始化 XXL-Job 执行器的配置]");
XxlJobProperties.AdminProperties admin = properties.getAdmin();
// 参数校验 XxlJobProperties.ExecutorProperties executor = properties.getExecutor();
XxlJobProperties.AdminProperties admin = this.properties.getAdmin();
XxlJobProperties.ExecutorProperties executor = this.properties.getExecutor();
Objects.requireNonNull(admin, "xxl job admin properties must not be null.");
Objects.requireNonNull(executor, "xxl job executor properties must not be null.");
// 初始化执行器 // 初始化执行器
XxlJobExecutor xxlJobExecutor = new XxlJobSpringExecutor(); XxlJobExecutor xxlJobExecutor = new XxlJobSpringExecutor();
@ -49,7 +39,7 @@ public class XxlJobAutoConfiguration {
xxlJobExecutor.setLogPath(executor.getLogPath()); xxlJobExecutor.setLogPath(executor.getLogPath());
xxlJobExecutor.setLogRetentionDays(executor.getLogRetentionDays()); xxlJobExecutor.setLogRetentionDays(executor.getLogRetentionDays());
xxlJobExecutor.setAdminAddresses(admin.getAddresses()); xxlJobExecutor.setAdminAddresses(admin.getAddresses());
xxlJobExecutor.setAccessToken(this.properties.getAccessToken()); xxlJobExecutor.setAccessToken(properties.getAccessToken());
return xxlJobExecutor; return xxlJobExecutor;
} }

View File

@ -1,14 +0,0 @@
package cn.iocoder.yudao.framework.quartz.core.enums;
/**
* Quartz Job Data key 枚举
*/
public enum JobDataKeyEnum {
JOB_ID,
JOB_HANDLER_NAME,
JOB_HANDLER_PARAM,
JOB_RETRY_COUNT, // 最大重试次数
JOB_RETRY_INTERVAL, // 每次重试间隔
}

View File

@ -1,19 +0,0 @@
package cn.iocoder.yudao.framework.quartz.core.handler;
/**
* 任务处理器
*
* @author 芋道源码
*/
public interface JobHandler {
/**
* 执行任务
*
* @param param 参数
* @return 结果
* @throws Exception 异常
*/
String execute(String param) throws Exception;
}

View File

@ -1,113 +0,0 @@
package cn.iocoder.yudao.framework.quartz.core.handler;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.thread.ThreadUtil;
import cn.iocoder.yudao.framework.quartz.core.enums.JobDataKeyEnum;
import cn.iocoder.yudao.framework.quartz.core.service.JobLogFrameworkService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.PersistJobDataAfterExecution;
import org.springframework.context.ApplicationContext;
import org.springframework.scheduling.quartz.QuartzJobBean;
import javax.annotation.Resource;
import java.util.Date;
import static cn.hutool.core.exceptions.ExceptionUtil.getRootCauseMessage;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.diff;
/**
* 基础 Job 调用者负责调用 {@link JobHandler#execute(String)} 执行任务
*
* @author 芋道源码
*/
@DisallowConcurrentExecution
@PersistJobDataAfterExecution
@Slf4j
public class JobHandlerInvoker extends QuartzJobBean {
@Resource
private ApplicationContext applicationContext;
@Resource
private JobLogFrameworkService jobLogFrameworkService;
@Override
protected void executeInternal(JobExecutionContext executionContext) throws JobExecutionException {
// 第一步获得 Job 数据
Long jobId = executionContext.getMergedJobDataMap().getLong(JobDataKeyEnum.JOB_ID.name());
String jobHandlerName = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_NAME.name());
String jobHandlerParam = executionContext.getMergedJobDataMap().getString(JobDataKeyEnum.JOB_HANDLER_PARAM.name());
int refireCount = executionContext.getRefireCount();
int retryCount = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_COUNT.name(), 0);
int retryInterval = (Integer) executionContext.getMergedJobDataMap().getOrDefault(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), 0);
// 第二步执行任务
Long jobLogId = null;
Date startTime = new Date();
String data = null;
Throwable exception = null;
try {
// 记录 Job 日志初始
jobLogId = jobLogFrameworkService.createJobLog(jobId, startTime, jobHandlerName, jobHandlerParam, refireCount + 1);
// 执行任务
data = this.executeInternal(jobHandlerName, jobHandlerParam);
} catch (Throwable ex) {
exception = ex;
}
// 第三步记录执行日志
this.updateJobLogResultAsync(jobLogId, startTime, data, exception, executionContext);
// 第四步处理有异常的情况
handleException(exception, refireCount, retryCount, retryInterval);
}
private String executeInternal(String jobHandlerName, String jobHandlerParam) throws Exception {
// 获得 JobHandler 对象
JobHandler jobHandler = applicationContext.getBean(jobHandlerName, JobHandler.class);
Assert.notNull(jobHandler, "JobHandler 不会为空");
// 执行任务
return jobHandler.execute(jobHandlerParam);
}
private void updateJobLogResultAsync(Long jobLogId, Date startTime, String data, Throwable exception,
JobExecutionContext executionContext) {
Date endTime = new Date();
// 处理是否成功
boolean success = exception == null;
if (!success) {
data = getRootCauseMessage(exception);
}
// 更新日志
try {
jobLogFrameworkService.updateJobLogResultAsync(jobLogId, endTime, (int) diff(endTime, startTime), success, data);
} catch (Exception ex) {
log.error("[executeInternal][Job({}) logId({}) 记录执行日志失败({}/{})]",
executionContext.getJobDetail().getKey(), jobLogId, success, data);
}
}
private void handleException(Throwable exception,
int refireCount, int retryCount, int retryInterval) throws JobExecutionException {
// 如果有异常则进行重试
if (exception == null) {
return;
}
// 情况一如果到达重试上限则直接抛出异常即可
if (refireCount >= retryCount) {
throw new JobExecutionException(exception);
}
// 情况二如果未到达重试上限 sleep 一定间隔时间然后重试
// 这里使用 sleep 来实现主要还是希望实现比较简单因为同一时间不会存在大量失败的 Job
if (retryInterval > 0) {
ThreadUtil.sleep(retryInterval);
}
// 第二个参数refireImmediately = true表示立即重试
throw new JobExecutionException(exception, true);
}
}

View File

@ -1,130 +0,0 @@
package cn.iocoder.yudao.framework.quartz.core.scheduler;
import cn.iocoder.yudao.framework.quartz.core.enums.JobDataKeyEnum;
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandlerInvoker;
import org.quartz.*;
/**
* {@link org.quartz.Scheduler} 的管理器负责创建任务
*
* 考虑到实现的简洁性我们使用 jobHandlerName 作为唯一标识
* 1. Job {@link JobDetail#getKey()}
* 2. Trigger {@link Trigger#getKey()}
*
* 另外jobHandlerName 对应到 Spring Bean 的名字直接调用
*
* @author 芋道源码
*/
public class SchedulerManager {
private final Scheduler scheduler;
public SchedulerManager(Scheduler scheduler) {
this.scheduler = scheduler;
}
/**
* 添加 Job Quartz
*
* @param jobId 任务编号
* @param jobHandlerName 任务处理器的名字
* @param jobHandlerParam 任务处理器的参数
* @param cronExpression CRON 表达式
* @param retryCount 重试次数
* @param retryInterval 重试间隔
* @throws SchedulerException 添加异常
*/
public void addJob(Long jobId, String jobHandlerName, String jobHandlerParam, String cronExpression,
Integer retryCount, Integer retryInterval)
throws SchedulerException {
// 创建 JobDetail 对象
JobDetail jobDetail = JobBuilder.newJob(JobHandlerInvoker.class)
.usingJobData(JobDataKeyEnum.JOB_ID.name(), jobId)
.usingJobData(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName)
.withIdentity(jobHandlerName).build();
// 创建 Trigger 对象
Trigger trigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval);
// 新增调度
scheduler.scheduleJob(jobDetail, trigger);
}
/**
* 更新 Job Quartz
*
* @param jobHandlerName 任务处理器的名字
* @param jobHandlerParam 任务处理器的参数
* @param cronExpression CRON 表达式
* @param retryCount 重试次数
* @param retryInterval 重试间隔
* @throws SchedulerException 更新异常
*/
public void updateJob(String jobHandlerName, String jobHandlerParam, String cronExpression,
Integer retryCount, Integer retryInterval)
throws SchedulerException {
// 创建新 Trigger 对象
Trigger newTrigger = this.buildTrigger(jobHandlerName, jobHandlerParam, cronExpression, retryCount, retryInterval);
// 修改调度
scheduler.rescheduleJob(new TriggerKey(jobHandlerName), newTrigger);
}
/**
* 删除 Quartz 中的 Job
*
* @param jobHandlerName 任务处理器的名字
* @throws SchedulerException 删除异常
*/
public void deleteJob(String jobHandlerName) throws SchedulerException {
scheduler.deleteJob(new JobKey(jobHandlerName));
}
/**
* 暂停 Quartz 中的 Job
*
* @param jobHandlerName 任务处理器的名字
* @throws SchedulerException 暂停异常
*/
public void pauseJob(String jobHandlerName) throws SchedulerException {
scheduler.pauseJob(new JobKey(jobHandlerName));
}
/**
* 启动 Quartz 中的 Job
*
* @param jobHandlerName 任务处理器的名字
* @throws SchedulerException 启动异常
*/
public void resumeJob(String jobHandlerName) throws SchedulerException {
scheduler.resumeJob(new JobKey(jobHandlerName));
scheduler.resumeTrigger(new TriggerKey(jobHandlerName));
}
/**
* 立即触发一次 Quartz 中的 Job
*
* @param jobId 任务编号
* @param jobHandlerName 任务处理器的名字
* @param jobHandlerParam 任务处理器的参数
* @throws SchedulerException 触发异常
*/
public void triggerJob(Long jobId, String jobHandlerName, String jobHandlerParam)
throws SchedulerException {
JobDataMap data = new JobDataMap(); // 无需重试所以不设置 retryCount retryInterval
data.put(JobDataKeyEnum.JOB_ID.name(), jobId);
data.put(JobDataKeyEnum.JOB_HANDLER_NAME.name(), jobHandlerName);
data.put(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam);
// 触发任务
scheduler.triggerJob(new JobKey(jobHandlerName), data);
}
private Trigger buildTrigger(String jobHandlerName, String jobHandlerParam, String cronExpression,
Integer retryCount, Integer retryInterval) {
return TriggerBuilder.newTrigger()
.withIdentity(jobHandlerName)
.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression))
.usingJobData(JobDataKeyEnum.JOB_HANDLER_PARAM.name(), jobHandlerParam)
.usingJobData(JobDataKeyEnum.JOB_RETRY_COUNT.name(), retryCount)
.usingJobData(JobDataKeyEnum.JOB_RETRY_INTERVAL.name(), retryInterval)
.build();
}
}

View File

@ -1,44 +0,0 @@
package cn.iocoder.yudao.framework.quartz.core.service;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Date;
/**
* Job 日志 Framework Service 接口
*
* @author 芋道源码
*/
public interface JobLogFrameworkService {
/**
* 创建 Job 日志
*
* @param jobId 任务编号
* @param beginTime 开始时间
* @param jobHandlerName Job 处理器的名字
* @param jobHandlerParam Job 处理器的参数
* @param executeIndex 第几次执行
* @return Job 日志的编号
*/
Long createJobLog(@NotNull(message = "任务编号不能为空") Long jobId,
@NotNull(message = "开始时间") Date beginTime,
@NotEmpty(message = "Job 处理器的名字不能为空") String jobHandlerName,
String jobHandlerParam,
@NotNull(message = "第几次执行不能为空") Integer executeIndex);
/**
* 更新 Job 日志的执行结果
*
* @param logId 日志编号
* @param endTime 结束时间因为是异步避免记录时间不准去
* @param duration 运行时长单位毫秒
* @param success 是否成功
* @param result 成功数据
*/
void updateJobLogResultAsync(@NotNull(message = "日志编号不能为空") Long logId,
@NotNull(message = "结束时间不能为空") Date endTime,
@NotNull(message = "运行时长不能为空") Integer duration,
boolean success, String result);
}

View File

@ -1,54 +0,0 @@
package cn.iocoder.yudao.framework.quartz.core.util;
import org.quartz.CronExpression;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* Quartz Cron 表达式的工具类
*
* @author 芋道源码
*/
public class CronUtils {
/**
* 校验 CRON 表达式是否有效
*
* @param cronExpression CRON 表达式
* @return 是否有效
*/
public static boolean isValid(String cronExpression) {
return CronExpression.isValidExpression(cronExpression);
}
/**
* 基于 CRON 表达式获得下 n 个满足执行的时间
*
* @param cronExpression CRON 表达式
* @param n 数量
* @return 满足条件的执行时间
*/
public static List<Date> getNextTimes(String cronExpression, int n) {
// 获得 CronExpression 对象
CronExpression cron;
try {
cron = new CronExpression(cronExpression);
} catch (ParseException e) {
throw new IllegalArgumentException(e.getMessage());
}
// 从当前开始计算n 个满足条件的
Date now = new Date();
List<Date> nextTimes = new ArrayList<>(n);
for (int i = 0; i < n; i++) {
Date nextTime = cron.getNextValidTimeAfter(now);
nextTimes.add(nextTime);
// 切换现在为下一个触发时间
now = nextTime;
}
return nextTimes;
}
}

View File

@ -1,7 +1,5 @@
/** /**
* 1. 定时任务采用 Quartz 实现进程内的任务执行 * 1. 定时任务基于 XXL-Job 实现
* 考虑到高可用使用 Quartz 自带的 MySQL 集群方案
*
* 2. 异步任务采用 Spring Async 异步执行 * 2. 异步任务采用 Spring Async 异步执行
*/ */
package cn.iocoder.yudao.framework.quartz; package cn.iocoder.yudao.framework.quartz;

View File

@ -1,3 +1,3 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.yudao.framework.quartz.config.YudaoQuartzAutoConfiguration,\ cn.iocoder.yudao.framework.quartz.config.YudaoXxlJobAutoConfiguration,\
cn.iocoder.yudao.framework.quartz.config.YudaoAsyncAutoConfiguration cn.iocoder.yudao.framework.quartz.config.YudaoAsyncAutoConfiguration

View File

@ -94,11 +94,11 @@
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency> </dependency>
<!-- Job 定时任务相关 TODO 芋艿:暂时去掉 --> <!-- Job 定时任务相关 -->
<!-- <dependency>--> <dependency>
<!-- <groupId>cn.iocoder.cloud</groupId>--> <groupId>cn.iocoder.cloud</groupId>
<!-- <artifactId>yudao-spring-boot-starter-job</artifactId>--> <artifactId>yudao-spring-boot-starter-job</artifactId>
<!-- </dependency>--> </dependency>
<!-- 消息队列相关 --> <!-- 消息队列相关 -->
<dependency> <dependency>

View File

@ -72,8 +72,10 @@ spring:
name-server: 127.0.0.1:9876 # RocketMQ Namesrv 地址 name-server: 127.0.0.1:9876 # RocketMQ Namesrv 地址
--- #################### 定时任务相关配置 #################### --- #################### 定时任务相关配置 ####################
xxl:
--- #################### 配置中心相关配置 #################### job:
admin:
addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址
--- #################### 服务保障相关配置 #################### --- #################### 服务保障相关配置 ####################

View File

@ -83,8 +83,10 @@ spring:
name-server: 127.0.0.1:9876 # RocketMQ Namesrv 地址 name-server: 127.0.0.1:9876 # RocketMQ Namesrv 地址
--- #################### 定时任务相关配置 #################### --- #################### 定时任务相关配置 ####################
xxl:
--- #################### 配置中心相关配置 #################### job:
admin:
addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址
--- #################### 服务保障相关配置 #################### --- #################### 服务保障相关配置 ####################

View File

@ -84,6 +84,15 @@ spring:
id: ${spring.application.name}:${server.port} # 编号Spring Cloud Alibaba 建议使用“应用:端口”的格式 id: ${spring.application.name}:${server.port} # 编号Spring Cloud Alibaba 建议使用“应用:端口”的格式
destination: springCloudBus # 目标消息队列,默认为 springCloudBus destination: springCloudBus # 目标消息队列,默认为 springCloudBus
--- #################### 定时任务相关配置 ####################
xxl:
job:
executor:
appname: ${spring.application.name} # 执行器 AppName
logpath: ${user.home}/logs/xxl-job/${spring.application.name} # 执行器运行日志文件存储磁盘路径
accessToken: default_token # 执行器通讯TOKEN
--- #################### 芋道相关配置 #################### --- #################### 芋道相关配置 ####################
yudao: yudao:

View File

@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.system.job.demo;
import cn.iocoder.yudao.framework.tenant.core.job.TenantJob;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.springframework.stereotype.Component;
@Component
@TenantJob
public class DemoJob {
@XxlJob("demoJob")
public void execute() {
System.out.println("美滋滋");
}
}

View File

@ -72,6 +72,10 @@ spring:
name-server: 127.0.0.1:9876 # RocketMQ Namesrv 地址 name-server: 127.0.0.1:9876 # RocketMQ Namesrv 地址
--- #################### 定时任务相关配置 #################### --- #################### 定时任务相关配置 ####################
xxl:
job:
admin:
addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址
--- #################### 服务保障相关配置 #################### --- #################### 服务保障相关配置 ####################

View File

@ -83,6 +83,11 @@ spring:
--- #################### 定时任务相关配置 #################### --- #################### 定时任务相关配置 ####################
xxl:
job:
admin:
addresses: http://127.0.0.1:9090/xxl-job-admin # 调度中心部署跟地址
--- #################### 服务保障相关配置 #################### --- #################### 服务保障相关配置 ####################
# Lock4j 配置项 # Lock4j 配置项

View File

@ -82,6 +82,15 @@ spring:
id: ${spring.application.name}:${server.port} # 编号Spring Cloud Alibaba 建议使用“应用:端口”的格式 id: ${spring.application.name}:${server.port} # 编号Spring Cloud Alibaba 建议使用“应用:端口”的格式
destination: springCloudBus # 目标消息队列,默认为 springCloudBus destination: springCloudBus # 目标消息队列,默认为 springCloudBus
--- #################### 定时任务相关配置 ####################
xxl:
job:
executor:
appname: ${spring.application.name} # 执行器 AppName
logpath: ${user.home}/logs/xxl-job/${spring.application.name} # 执行器运行日志文件存储磁盘路径
accessToken: default_token # 执行器通讯TOKEN
--- #################### 芋道相关配置 #################### --- #################### 芋道相关配置 ####################
yudao: yudao:
@ -110,6 +119,7 @@ yudao:
- /admin-api/system/captcha/get-image # 获取图片验证码,和租户无关 - /admin-api/system/captcha/get-image # 获取图片验证码,和租户无关
- /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号 - /admin-api/system/sms/callback/* # 短信回调接口,无法带上租户编号
- /rpc-api/system/tenant/valid # 防止递归。避免调用 /rpc-api/system/tenant/valid 接口时,又去触发 /rpc-api/system/tenant/valid 去校验 - /rpc-api/system/tenant/valid # 防止递归。避免调用 /rpc-api/system/tenant/valid 接口时,又去触发 /rpc-api/system/tenant/valid 去校验
- /rpc-api/system/tenant/id-list # 获得租户列表的时候,无需传递租户编号
- /rpc-api/system/error-code/* # 错误码的自动创建与下载的接口,无法带上租户编号 - /rpc-api/system/error-code/* # 错误码的自动创建与下载的接口,无法带上租户编号
ignore-tables: ignore-tables:
- system_tenant - system_tenant

View File

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>common</artifactId>
<groupId>cn.iocoder.mall</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>mall-spring-boot-starter-xxl-job</artifactId>
<dependencies>
<!-- Spring 核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<optional>true</optional>
</dependency>
<!-- Job 相关 -->
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -1,172 +0,0 @@
package cn.iocoder.mall.xxljob.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* XXL-Job 配置类
*/
@ConfigurationProperties("xxl.job")
public class XxlJobProperties {
/**
* 是否开启默认为 true 关闭
*/
private Boolean enabled = true;
/**
* 访问令牌
*/
private String accessToken;
/**
* 控制器配置
*/
private AdminProperties admin;
/**
* 执行器配置
*/
private ExecutorProperties executor;
public Boolean getEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
if (enabled != null) {
this.enabled = enabled;
}
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
if (accessToken != null && accessToken.trim().length() > 0) {
this.accessToken = accessToken;
}
}
public AdminProperties getAdmin() {
return admin;
}
public void setAdmin(AdminProperties admin) {
this.admin = admin;
}
public ExecutorProperties getExecutor() {
return executor;
}
public void setExecutor(ExecutorProperties executor) {
this.executor = executor;
}
/**
* XXL-Job 调度器配置类
*/
public static class AdminProperties {
/**
* 调度器地址
*/
private String addresses;
public String getAddresses() {
return addresses;
}
public void setAddresses(String addresses) {
this.addresses = addresses;
}
@Override
public String toString() {
return "AdminProperties{" +
"addresses='" + addresses + '\'' +
'}';
}
}
/**
* XXL-Job 执行器配置类
*/
public static class ExecutorProperties {
/**
* 默认端口
*
* 这里使用 -1 表示随机
*/
private static final Integer PORT_DEFAULT = -1;
/**
* 默认日志保留天数
*
* 默认为 -1不清理永久保留
*/
private static final Integer LOG_RETENTION_DAYS_DEFAULT = -1;
/**
* 应用名
*/
private String appName;
/**
* 执行器的 IP
*/
private String ip;
/**
* 执行器的 Port
*/
private Integer port = PORT_DEFAULT;
/**
* 日志地址
*/
private String logPath;
/**
* 日志保留天数
*/
private Integer logRetentionDays = LOG_RETENTION_DAYS_DEFAULT;
public String getAppName() {
return appName;
}
public void setAppName(String appName) {
this.appName = appName;
}
public String getLogPath() {
return logPath;
}
public void setLogPath(String logPath) {
this.logPath = logPath;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public Integer getPort() {
return port;
}
public void setPort(Integer port) {
this.port = port;
}
public Integer getLogRetentionDays() {
return logRetentionDays;
}
public void setLogRetentionDays(Integer logRetentionDays) {
this.logRetentionDays = logRetentionDays;
}
}
}

View File

@ -1,2 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.iocoder.mall.xxljob.config.XxlJobAutoConfiguration