构建数据库表格(Oracle)
CREATE TABLE SCENE_REPTILE_CONFIG (
EVENT_TYPE VARCHAR2(16) NULL,
WEB_NAME VARCHAR2(100) NULL,
WEB_URL VARCHAR2(100) NULL,
AREA VARCHAR2(255) NULL,
KEYWORD VARCHAR2(255) NULL,
START_TIME VARCHAR2(100) NULL,
END_TIME VARCHAR2(100) NULL,
RUN_TIME VARCHAR2(64) NULL,
IS_ENABLE VARCHAR2(16) NULL,
UUID VARCHAR2(100) NOT NULL,
UPDATE_TIME VARCHAR2(100) NOT NULL,
START_TIME_REAL VARCHAR2(100) NULL,
END_TIME_REAL VARCHAR2(100) NULL,
RUN_TIME_REAL VARCHAR2(100) NULL
);
pom文件用到的依赖
<dependencies>
<!-- spring boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.6.6</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.26</version>
</dependency>
<!--httpclient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.12</version>
</dependency>
<!--json-->
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<classifier>jdk15</classifier>
<version>2.4</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.1</version>
<scope>compile</scope>
</dependency>
<!--Java HTML解析库-->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.10.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.10.0</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.oracle.ojdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>19.3.0.0</version>
</dependency>
<!-- 解决报错:java.sql.SQLException: 不支持的字符集 (在类路径中添加 orai18n.jar): ZHS16GBK-->
<dependency>
<groupId>com.oracle.database.nls</groupId>
<artifactId>orai18n</artifactId>
<version>19.3.0.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.70</version>
<scope>compile</scope>
</dependency>
</dependencies>
application.properties
# 配置数据库驱动,这里用的是Oracle数据库
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
# 连接数据库的地址
spring.datasource.url=jdbc:oracle:thin:@ip:端口号:服务名
spring.datasource.username=用户名
spring.datasource.password=密码
# 配置日志文件位置
logging.file.name=./log/aaa.log
# 配置日志文件大小
logging.file.max-size=300MB
实体类
import com.alibaba.excel.annotation.ExcelProperty;
import com.alibaba.excel.annotation.write.style.ColumnWidth;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* @version 1.0
* @date 2024/4/15 9:34
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@TableName(value ="SCENE_REPTILE_CONFIG")
public class SceneReptileConfig implements Serializable {
/**
* 唯一id
*/
@TableField(value = "UUID")
private String uuid;
/**
* 事件类型
*/
@ColumnWidth(15)
@ExcelProperty(value = { "事件类型" }, index = 0)
@TableField(value = "EVENT_TYPE")
private String eventType;
/**
* 网址中文名称
*/
@ColumnWidth(15)
@ExcelProperty(value = { "网站名称" }, index = 1)
@TableField(value = "WEB_NAME")
private String webName;
/**
* 网址url
*/
@ColumnWidth(15)
@ExcelProperty(value = { "网址URL" }, index = 2)
@TableField(value = "WEB_URL")
private String webUrl;
/**
* 区域
*/
@ColumnWidth(15)
@ExcelProperty(value = { "区域" }, index = 3)
@TableField(value = "AREA")
private String area;
/**
* 关键字
*/
@ColumnWidth(15)
@ExcelProperty(value = { "关键字" }, index = 4)
@TableField(value = "KEYWORD")
private String keyword;
/**
* 任务开始时间(前端展示配置 ) 0:立即执行
*/
@ColumnWidth(15)
@ExcelProperty(value = { "任务开始时间" }, index = 5)
@TableField(value = "START_TIME")
private String startTime;
/**
* 任务结束时间(前端展示配置 )0:无限期
*/
@TableField(value = "END_TIME")
private String endTime;
/**
* 执行频率(前端展示配置 )0:只执行一次
*/
@TableField(value = "RUN_TIME")
private String runTime;
/**
* 是否启用:1 启用、0 禁止
*/
@ColumnWidth(15)
@ExcelProperty(value = { "操作: 1 启用、0 禁止" }, index = 6)
@TableField(value = "IS_ENABLE")
private String isEnable;
/**
* 更新时间
*/
@TableField(value = "UPDATE_TIME")
private String updateTime;
/**
* 任务开始时间(后端实际读取配置)
*/
@TableField(value = "START_TIME_REAL")
private String startTimeReal;
/**
* 任务结束时间(后端实际读取配置)
*/
@TableField(value = "END_TIME_REAL")
private String endTimeReal;
/**
* 执行频率(后端实际读取配置)必须是cron表达式
*/
@TableField(value = "RUN_TIME_REAL")
private String runTimeReal;
}
自定义线程池
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* @version 1.0
* @date 2024/4/22 18:57
*/
@Configuration
public class DynamicScheduledPoolConfig {
private static int poolSize = 10;//核心线程
private static int awaitTermination = 60;//挂壁时间
private static String threadNamePrefix = "task-job-";
private static volatile ThreadPoolTaskScheduler taskScheduler;
@Bean
public static Executor getMySchedulerPool(){
if(taskScheduler == null){
synchronized (ThreadPoolTaskScheduler.class){
if(taskScheduler == null){
taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setThreadNamePrefix(threadNamePrefix);
taskScheduler.setPoolSize(poolSize);
// taskScheduler.setThreadFactory(Executors.defaultThreadFactory());
taskScheduler.setAwaitTerminationSeconds(awaitTermination);//演示挂壁的时间为60s
/**设置为false,关闭线程池中的任务时,直接执行shutdownNow() 延时关闭 开启*/
taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
taskScheduler.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());//线程池对拒绝任务(无线程可用的)的处理策略
taskScheduler.initialize();//初始化
}
}
}
return taskScheduler;
}
}
mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
import java.util.Map;
/**
* @version 1.0
* @date 2024/4/15 9:55
*/
@Mapper
public interface SceneReptileConfigMapper extends BaseMapper<SceneReptileConfig> {
}
读取数据库中的配置并执行定时任务
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.config.TriggerTask;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
@EnableScheduling
@Component
public class SchedulingConfigure implements SchedulingConfigurer {
Logger logger = LoggerFactory.getLogger(this.getClass());
// 用于存储所有计划任务及其相关信息 key:任务的UUID value:ScheduledTaskWrapper对象
private final Map<String, ScheduledTaskWrapper> taskMap = new HashMap<>();
// 任务注册器,用于注册和管理计划任务
private ScheduledTaskRegistrar taskRegistrar;
@Autowired
private SceneReptileConfigMapper sceneReptileConfigMapper;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
this.taskRegistrar = taskRegistrar;
// 使用自定义的线程池
taskRegistrar.setScheduler(DynamicScheduledPoolConfig.getMySchedulerPool());
// 初始化任务配置
List<SceneReptileConfig> configList = sceneReptileConfigMapper.selectList(null);
// 根据配置情况执行对应的任务
for (SceneReptileConfig config : configList) {
addOrUpdateTask(config);
}
}
@Scheduled(fixedRate = 60000) // 每分钟检查一次数据库
public void checkForNewTasks() {
List<SceneReptileConfig> newConfigList = sceneReptileConfigMapper.selectList(null);
for (SceneReptileConfig newConfig : newConfigList) {
addOrUpdateTask(newConfig);
}
}
// 保存和管理ScheduledTaskWrapper实例
private void addOrUpdateTask(SceneReptileConfig config) {
String taskId = config.getUuid();
ScheduledTaskWrapper existingTask = taskMap.get(taskId);
// 如果任务列表为空或者发生变化
if (existingTask == null || isConfigChanged(existingTask.getConfig(), config)) {
if (existingTask != null) {
// 取消现有任务,如果任务正在执行,中断并移除该任务
existingTask.getFuture().cancel(true);
taskMap.remove(taskId);
}
// 创建一个新的Runnable任务,该任务会调用doMyJob方法并传入新的配置
Runnable taskRunnable = () -> doMyJob(config);
Trigger trigger = createTrigger(config);
TriggerTask newTask = new TriggerTask(taskRunnable, trigger);
// 使用任务注册器的调度器来调度新的任务
ScheduledFuture<?> future = taskRegistrar.getScheduler().schedule(newTask.getRunnable(), newTask.getTrigger());
// 将新的任务和配置以及ScheduledFuture一起添加到任务映射中
taskMap.put(taskId, new ScheduledTaskWrapper(newTask, config, future));
}
}
// 在这里处理具体的业务逻辑
private void doMyJob(SceneReptileConfig config) {
// 如果当前任务时间在范围内
if(isCurrentDateBetween(config.getStartTimeReal(),config.getEndTimeReal())){
// 是否启用:1启用 0禁用 2启用一次
if ("1".equals(config.getIsEnable())) {
useWhichTask(config);
}else if("0".equals(config.getRunTime())){
useWhichTask(config);
// 执行完一次之后,停止该规则
QueryWrapper<SceneReptileConfig> queryWrapper = new QueryWrapper();
queryWrapper.eq("UUID",config.getUuid());
config.setIsEnable("0");
sceneReptileConfigMapper.update(config,queryWrapper);
}
}
}
// 执行具体事件类型任务
private void useWhichTask(SceneReptileConfig config){
switch (config.getEventType()) {
case "A任务":
// 执行对应的任务
break;
case "B任务":
// 执行对应的任务
break;
case "C任务":
// 执行对应的任务
break;
default:
// 未知事件类型
logger.info("未知事件类型:" + config.getEventType());
break;
}
}
// 检查当前日期是否在开始日期和结束日期之间
private boolean isCurrentDateBetween(String startTime, String endTime) {
// 定义日期格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
// 将字符串转换为LocalDate对象
LocalDate start = LocalDate.parse(startTime, formatter);
LocalDate end = LocalDate.parse(endTime, formatter);
LocalDate today = LocalDate.now(); // 获取当前日期
// 如果结束日期是今天的日期,也认为今天在范围内
return !today.isBefore(start) && !today.isAfter(end);
}
// 判断配置信息是否有变化
private boolean isConfigChanged(SceneReptileConfig oldConfig, SceneReptileConfig newConfig) {
return !oldConfig.getUpdateTime().equals(newConfig.getUpdateTime());
}
// 允许在实例化时传递ScheduledFuture对象
private static class ScheduledTaskWrapper {
private final TriggerTask task;
private final SceneReptileConfig config;
private final ScheduledFuture<?> future;
public ScheduledTaskWrapper(TriggerTask task, SceneReptileConfig config, ScheduledFuture<?> future) {
this.task = task;
this.config = config;
this.future = future;
}
public TriggerTask getTask() {
return task;
}
public SceneReptileConfig getConfig() {
return config;
}
public ScheduledFuture<?> getFuture() {
return future;
}
}
private Trigger createTrigger(SceneReptileConfig config) {
return new CronTrigger(config.getRunTimeReal());
}
}