調查了下用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直接進行測試,將我們編寫的兩個定時任務添加到定時任務調度器中。
下圖是定時任務的執行日誌