调查了下用Spring boot集成Quartz来实现定时任务的动态管理,记下来备用。
主要使用 Spring boot、Quartz、Mybatis实现,其中Quartz任务在SqlServer数据库保存,业务数据库是Oracle。
需要实现数据源的动态管理,定时任务使用Spring IOC管理等功能。
创建Maven project
POM中依赖如下:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.10.RELEASE</version>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.boot.version>1.5.10.RELEASE</spring.boot.version>
<spring.cloud.version>Edgware.SR2</spring.cloud.version>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Spring Boot Test 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- GSON -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
<!-- 代码简化 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 日志 Log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- Log4j2 异步支持 -->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.3.6</version>
</dependency>
<!-- 使用 @ConfigurationProperties @Value 使用 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.github.pagehelper/pagehelper-spring-boot-starter -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.8</version>
</dependency>
<!--ojdbc -->
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0</version>
</dependency>
<!-- sqlserver -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>sqljdbc4</artifactId>
<version>4.0</version>
</dependency>
<!-- api document -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.2.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.2.2</version>
</dependency>
<!--junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${java.version}</source>
<target>${java.version}</target>
<skip>true</skip>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<!-- 忽略Test包 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.demo.service.job.JobManagerApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>
上面需要注意的是ojdbc的jar需要手动安装到本地maven仓库。
配置Quartz
在SqlServer中创建Quartz需要的数据库和表,这里的建表语句可以自行到Quartz官网下载;
在src/main/resources下新建quartz.properties文件用来配置Quartz:
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false
# 线程池配置
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 5
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
#任务持久化
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.dataSource = qzDS
org.quartz.jobStore.misfireThreshold = 5000
org.quartz.dataSource.qzDS.driver = com.microsoft.sqlserver.jdbc.SQLServerDriver
org.quartz.dataSource.qzDS.URL = jdbc:sqlserver://127.0.0.1;DatabaseName=QUARTZ_DB
org.quartz.dataSource.qzDS.user =
org.quartz.dataSource.qzDS.password =
org.quartz.dataSource.qzDS.maxConnections = 10
编写代码,使用配置文件中的配置,并且把定时任务交给Spring IOC处理
自定义任务工厂类,将定时任务交给Spring
@Component
public class QuartzJobFactory extends AdaptableJobFactory {
@Autowired
private AutowireCapableBeanFactory autowireCapableBeanFactory;
/**
* @see org.springframework.scheduling.quartz.AdaptableJobFactory#createJobInstance(org.quartz.spi.TriggerFiredBundle)
*/
@Override
protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
Object jobInstance = super.createJobInstance(bundle);
// 实现Job的IOC管理
autowireCapableBeanFactory.autowireBean(jobInstance);
return jobInstance;
}
}
配置定时任务调度器
@Configuration
public class SchedulerConfig {
@Autowired
private QuartzJobFactory jobFactory;
/**
* 定时任务初始化监听器
*
* @Title: initListener
*/
@Bean
public QuartzInitializerListener initListener() {
return new QuartzInitializerListener();
}
/**
* 任务调度器
*
* @Title: scheduler
* @throws IOException
*/
@Bean(name = "Scheduler")
public Scheduler scheduler() throws IOException {
return schedulerFactoryBean().getScheduler();
}
/**
* 任务调度器工厂
*
* @Title: schedulerFactoryBean
* @throws IOException
*/
@Bean(name = "SchedulerFactory")
public SchedulerFactoryBean schedulerFactoryBean() throws IOException {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setQuartzProperties(quartzProperties());
factory.setOverwriteExistingJobs(true);
factory.setStartupDelay(15);
factory.setJobFactory(jobFactory);
return factory;
}
/**
* 读取定时任务配置
*
* @Title: quartzProperties
* @throws IOException
*/
@Bean
public Properties quartzProperties() throws IOException {
PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
propertiesFactoryBean.afterPropertiesSet();
return propertiesFactoryBean.getObject();
}
}
配置多数据源
定义application.properties
server.port = 8001
spring.application.name =job-manager
#business data source#
biz.datasource.type = com.alibaba.druid.pool.DruidDataSource
biz.datasource.url = jdbc:oracle:thin:@127.0.0.1:1521:orcl
biz.datasource.driver-class-name = oracle.jdbc.OracleDriver
biz.datasource.username =
biz.datasource.password =
#job data sources#
job.datasource.type = com.alibaba.druid.pool.DruidDataSource
job.datasource.url = jdbc:sqlserver://127.0.0.1;DatabaseName=QUARTZ_DB
job.datasource.driver-class-name = com.microsoft.sqlserver.jdbc.SQLServerDriver
job.datasource.username =
job.datasource.password =
#druid#
spring.druid.initialSize = 10
spring.druid.minIdle = 10
spring.druid.maxActive = 20
spring.druid.maxWait = 60000
spring.druid.maxOpenPreparedStatements = 50
spring.druid.validationQuery = select count(0) from dual
spring.druid.testWhileIdle = true
#Mybatis#
mybatis.mapper-locations = classpath:mapper/mybatis-sqlmap-*.xml
mybatis.type-aliases-package = com.demo.service.job.entity
#PageHelper#
pagehelper.autoDialect = true
pagehelper.closeConn = true
pagehelper.offset-as-page-num = false
使用Aspect实现动态切换数据源
创建数据源名称枚举
public enum DataSourceNameEnum {
/** 业务库 */
BUSINESS("biz"),
/** 定时任务库 */
QUARTZ_JOB("job");
/** 数据源名称 */
private String name;
/**
*
* 构造函数
*
* @param name
*/
private DataSourceNameEnum(String name) {
this.name = name;
}
/**
* 获取:数据源名称
*/
public String getName() {
return name;
}
}
自定义数据源注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface DataSourceAnnotation {
/** 默认使用业务数据库 */
DataSourceNameEnum value() default DataSourceNameEnum.BUSINESS;
}
数据源上下文管理器
public class DataSourceContextHolder {
final static ThreadLocal<String> local = new ThreadLocal<>();
public static void setDataSourceName(String name) {
local.set(name);
}
public static String getDataSourceName() {
return local.get();
}
}
实现动态数据源
@Log4j2
public class DynamicDataSource extends AbstractRoutingDataSource {
/**
* @see org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource#determineCurrentLookupKey()
*/
@Override
protected Object determineCurrentLookupKey() {
String dataSourceName = DataSourceContextHolder.getDataSourceName();
log.debug("Current data source name is {}.", dataSourceName);
return dataSourceName;
}
}
数据源配置管理
@Configuration
public class DataSourceConfig {
@Bean(name = "biz")
@ConfigurationProperties(prefix = "biz.datasource")
public DataSource bizDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "job")
@ConfigurationProperties(prefix = "job.datasource")
public DataSource jobDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "dataSource")
@Primary
public DataSource dataSource() {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
DataSource biz = bizDataSource();
DataSource job = jobDataSource();
dynamicDataSource.setDefaultTargetDataSource(biz);
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceNameEnum.BUSINESS.getName(), biz);
targetDataSources.put(DataSourceNameEnum.QUARTZ_JOB.getName(), job);
dynamicDataSource.setTargetDataSources(targetDataSources);
return dynamicDataSource;
}
}
使用AOP实现数据源切换
@Aspect
@Order(1)
@Component
@Log4j2
public class DataSourceAspect {
@Pointcut("execution(* com.demo.service.job.service.impl.*Impl.*(..))")
public void aspect() {}
@Before("aspect()")
private void before(JoinPoint point) {
Object target = point.getTarget();
String methodName = point.getSignature().getName();
Class<?> clazz = target.getClass();
Class<?>[] parameterTypes = ((MethodSignature) point.getSignature()).getMethod().getParameterTypes();
Method method = null;
try {
method = clazz.getMethod(methodName, parameterTypes);
} catch (Exception e) {
log.error("Get {}.{} ERROR", clazz.getName(), methodName);
}
if (null != method && method.isAnnotationPresent(DataSourceAnnotation.class)) {
DataSourceAnnotation dataSource = method.getAnnotation(DataSourceAnnotation.class);
String name = dataSource.value().getName();
DataSourceContextHolder.setDataSourceName(name);
log.debug("Switch Data Source To {}.", name);
}
}
}
最重要的是,关闭Spring boot自动数据源配置
// 禁用数据源自动配置
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
// 事务处理在数据源切换完成后
@EnableTransactionManagement(order = 2)
public class JobManagerApplication {
public static void main(String[] args) {
SpringApplication.run(JobManagerApplication.class, args);
}
}
定时任务动态管理的实现
自定义定时任务接口
public interface BaseQuartzJob extends Job {
void execute(JobExecutionContext executionContext) throws JobExecutionException;
}
自定义任务
@Log4j2
public class PrintJob implements BaseQuartzJob {
@Override
public void execute(JobExecutionContext executionContext) throws JobExecutionException {
log.info("execute printjob now");
}
}
@Log4j2
public class FyAssetJob implements BaseQuartzJob {
@Autowired
private TAssetFyLmsRunLogService assetFyLmsRunLogService;
@Override
public void execute(JobExecutionContext executionContext) throws JobExecutionException {
log.info("=== check run log begin===");
TAssetFyLmsRunLogPageParam param = new TAssetFyLmsRunLogPageParam();
param.setPageNum(1);
param.setPageSize(12);
List<TAssetFyLmsRunLog> list = assetFyLmsRunLogService.getAll(param);
if (null != list && !list.isEmpty()) {
for (TAssetFyLmsRunLog item : list) {
log.debug(item.toString());
}
}
log.info("=== check run log end===");
}
}
其中,第二个任务,使用了Spring IOC的Bean。
定时任务管理
@Log4j2
@RestController
public class QuartzJobController {
@Autowired
@Qualifier("Scheduler")
private Scheduler scheduler;
@Autowired
private QuartzJobInfoService quartzJobInfoService;
/**
* 分页检索
*
* @Title: list
*/
@ApiOperation(value = "分页检索",
notes = "")
@RequestMapping(value = "/list",
method = RequestMethod.POST)
public ResponseEntity<BaseResponse<BasePageResult<QuartzJobInfo>>> list(
@RequestBody BaseRequest<QuartzJobPageParam> req) {
BaseResponse<BasePageResult<QuartzJobInfo>> response = new BaseResponse<>();
BasePageResult<QuartzJobInfo> result = quartzJobInfoService.list(req);
response.setResult(result);
return new ResponseEntity<BaseResponse<BasePageResult<QuartzJobInfo>>>(response, HttpStatus.OK);
}
/**
* 新增任务
*
* @Title: add
*/
@ApiOperation(value = "新增任务",
notes = "")
@RequestMapping(value = "/add",
method = RequestMethod.POST)
public ResponseEntity<BaseResponse<String>> add(@RequestBody BaseRequest<QuarzJobParam> req) {
BaseResponse<String> response = new BaseResponse<>();
String code = ResultCodeEnum.SUCCESS.getCode();
String result = "新增定时任务成功!";
// get parameter fro request
QuarzJobParam item = req.getParam();
String jobClassName = item.getJobClassName();
String jobGroupName = item.getJobGroup();
String cronExpression = item.getCornExpression();
try {
// build jobDetail and trigger
JobDetail jobDetail = JobBuilder.newJob(getClass(jobClassName).getClass())
.withIdentity(jobClassName, jobGroupName).build();
// 表达式调度构建器(即任务执行的时间)
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
// 按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobClassName, jobGroupName)
.withSchedule(scheduleBuilder).build();
// 执行调度
scheduler.start();
scheduler.scheduleJob(jobDetail, trigger);
} catch (SchedulerException e) {
code = ResultCodeEnum.ERROR.getCode();
result = "调度定时任务失败!";
log.error("调度定时任务失败:{}", e.getMessage());
} catch (Exception e) {
code = ResultCodeEnum.ERROR.getCode();
result = "构建定时任务失败!";
log.error("构建定时任务失败:{}", e.getMessage());
}
response.setCode(code);
response.setResult(result);
return new ResponseEntity<BaseResponse<String>>(response, HttpStatus.OK);
}
/**
* 暂停任务
*
* @Title: pause
*/
@ApiOperation(value = "暂停任务",
notes = "")
@RequestMapping(value = "/pause",
method = RequestMethod.POST)
public ResponseEntity<BaseResponse<String>> pause(@RequestBody BaseRequest<QuarzJobParam> req) {
BaseResponse<String> response = new BaseResponse<>();
String code = ResultCodeEnum.SUCCESS.getCode();
String result = "暂停定时任务成功!";
// get parameter fro request
QuarzJobParam item = req.getParam();
String jobClassName = item.getJobClassName();
String jobGroupName = item.getJobGroup();
try {
scheduler.pauseJob(JobKey.jobKey(jobClassName, jobGroupName));
} catch (SchedulerException e) {
code = ResultCodeEnum.ERROR.getCode();
result = "暂停定时任务失败!";
log.error("暂停定时任务失败:{}", e.getMessage());
}
response.setCode(code);
response.setResult(result);
return new ResponseEntity<BaseResponse<String>>(response, HttpStatus.OK);
}
/**
* 恢复任务
*
* @Title: resume
*/
@ApiOperation(value = "恢复任务",
notes = "")
@RequestMapping(value = "/resume",
method = RequestMethod.POST)
public ResponseEntity<BaseResponse<String>> resume(@RequestBody BaseRequest<QuarzJobParam> req) {
BaseResponse<String> response = new BaseResponse<>();
String code = ResultCodeEnum.SUCCESS.getCode();
String result = "恢复定时任务成功!";
// get parameter fro request
QuarzJobParam item = req.getParam();
String jobClassName = item.getJobClassName();
String jobGroupName = item.getJobGroup();
try {
scheduler.resumeJob(JobKey.jobKey(jobClassName, jobGroupName));
} catch (SchedulerException e) {
code = ResultCodeEnum.ERROR.getCode();
result = "恢复定时任务失败!";
log.error("恢复定时任务失败:{}", e.getMessage());
}
response.setCode(code);
response.setResult(result);
return new ResponseEntity<BaseResponse<String>>(response, HttpStatus.OK);
}
/**
* 删除任务
*
* @Title: remove
*/
@ApiOperation(value = "删除任务",
notes = "")
@RequestMapping(value = "/remove",
method = RequestMethod.POST)
public ResponseEntity<BaseResponse<String>> remove(@RequestBody BaseRequest<QuarzJobParam> req) {
BaseResponse<String> response = new BaseResponse<>();
String code = ResultCodeEnum.SUCCESS.getCode();
String result = "删除定时任务成功!";
// get parameter fro request
QuarzJobParam item = req.getParam();
String jobClassName = item.getJobClassName();
String jobGroupName = item.getJobGroup();
try {
scheduler.pauseTrigger(TriggerKey.triggerKey(jobClassName, jobGroupName));
scheduler.unscheduleJob(TriggerKey.triggerKey(jobClassName, jobGroupName));
scheduler.deleteJob(JobKey.jobKey(jobClassName, jobGroupName));
} catch (SchedulerException e) {
code = ResultCodeEnum.ERROR.getCode();
result = "删除定时任务失败!";
log.error("删除定时任务失败:{}", e.getMessage());
}
response.setCode(code);
response.setResult(result);
return new ResponseEntity<BaseResponse<String>>(response, HttpStatus.OK);
}
/**
* 更新任务触发器
*
* @Title: refresh
*/
@ApiOperation(value = "更新任务",
notes = "")
@RequestMapping(value = "/refresh",
method = RequestMethod.POST)
public ResponseEntity<BaseResponse<String>> refresh(@RequestBody BaseRequest<QuarzJobParam> req) {
BaseResponse<String> response = new BaseResponse<>();
String code = ResultCodeEnum.SUCCESS.getCode();
String result = "更新定时任务成功!";
// get parameter fro request
QuarzJobParam item = req.getParam();
String jobClassName = item.getJobClassName();
String jobGroupName = item.getJobGroup();
String cronExpression = item.getCornExpression();
try {
TriggerKey triggerKey = TriggerKey.triggerKey(jobClassName, jobGroupName);
// 表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
// 按新的cronExpression表达式重新构建trigger
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
// 按新的trigger重新设置job执行
scheduler.rescheduleJob(triggerKey, trigger);
} catch (SchedulerException e) {
code = ResultCodeEnum.ERROR.getCode();
result = "更新定时任务失败!";
log.error("更新定时任务失败:{}", e.getMessage());
}
response.setCode(code);
response.setResult(result);
return new ResponseEntity<BaseResponse<String>>(response, HttpStatus.OK);
}
/**
* 获得任务类
*
* @Title: getClass
* @throws Exception
*/
private static BaseQuartzJob getClass(String className) throws Exception {
Class<?> clazz = Class.forName(className);
return (BaseQuartzJob) clazz.newInstance();
}
}
主要的代码已经帖完了,看一下工程架构
启动项目,使用SWAGGER查看API
可以使用swagger直接进行测试,将我们编写的两个定时任务添加到定时任务调度器中。
下图是定时任务的执行日志