---初始化项目
This commit is contained in:
28
powerjob-server/powerjob-server-common/pom.xml
Normal file
28
powerjob-server/powerjob-server-common/pom.xml
Normal file
@ -0,0 +1,28 @@
|
||||
<?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>powerjob-server</artifactId>
|
||||
<groupId>tech.powerjob</groupId>
|
||||
<version>5.1.2</version>
|
||||
<relativePath>../pom.xml</relativePath>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>powerjob-server-common</artifactId>
|
||||
<version>${project.parent.version}</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>8</maven.compiler.source>
|
||||
<maven.compiler.target>8</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@ -0,0 +1,23 @@
|
||||
package tech.powerjob.server.common;
|
||||
|
||||
|
||||
/**
|
||||
* @author Echo009
|
||||
* @since 2022/10/2
|
||||
*/
|
||||
public class Holder<T> {
|
||||
|
||||
private T value;
|
||||
|
||||
public Holder(T value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public T get() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void set(T value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package tech.powerjob.server.common;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 统一定义日志
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2023/3/25
|
||||
*/
|
||||
public class Loggers {
|
||||
|
||||
/**
|
||||
* Web 层统一日志
|
||||
*/
|
||||
public static final Logger WEB = LoggerFactory.getLogger("P_SERVER_LOGGER_WEB");
|
||||
}
|
||||
@ -0,0 +1,42 @@
|
||||
package tech.powerjob.server.common;
|
||||
|
||||
/**
|
||||
* 配置文件 key
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2020/8/2
|
||||
*/
|
||||
public class PowerJobServerConfigKey {
|
||||
|
||||
/**
|
||||
* akka 协议端口号
|
||||
*/
|
||||
public static final String AKKA_PORT = "oms.akka.port";
|
||||
/**
|
||||
* http 协议端口号
|
||||
*/
|
||||
public static final String HTTP_PORT = "oms.http.port";
|
||||
/**
|
||||
* MU 协议端口号
|
||||
*/
|
||||
public static final String MU_PORT = "oms.mu.port";
|
||||
/**
|
||||
* 自定义数据库表前缀
|
||||
*/
|
||||
public static final String TABLE_PREFIX = "oms.table-prefix";
|
||||
/**
|
||||
* 是否使用 mongoDB
|
||||
*/
|
||||
public static final String MONGODB_ENABLE = "oms.mongodb.enable";
|
||||
/**
|
||||
* 是否启用 Swagger-UI,默认关闭
|
||||
*/
|
||||
public static final String SWAGGER_UI_ENABLE = "oms.swagger.enable";
|
||||
|
||||
/**
|
||||
* 钉钉报警相关
|
||||
*/
|
||||
public static final String DING_APP_KEY = "oms.alarm.ding.app-key";
|
||||
public static final String DING_APP_SECRET = "oms.alarm.ding.app-secret";
|
||||
public static final String DING_AGENT_ID = "oms.alarm.ding.agent-id";
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
package tech.powerjob.server.common;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.RejectedExecutionHandler;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* 拒绝策略
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2020/11/28
|
||||
*/
|
||||
@Slf4j
|
||||
public class RejectedExecutionHandlerFactory {
|
||||
|
||||
private static final AtomicLong COUNTER = new AtomicLong();
|
||||
|
||||
/**
|
||||
* 拒绝执行,抛出 RejectedExecutionException
|
||||
* @param source name for log
|
||||
* @return A handler for tasks that cannot be executed by ThreadPool
|
||||
*/
|
||||
public static RejectedExecutionHandler newAbort(String source) {
|
||||
return (r, e) -> {
|
||||
log.error("[{}] ThreadPool[{}] overload, the task[{}] will be Abort, Maybe you need to adjust the ThreadPool config!", source, e, r);
|
||||
throw new RejectedExecutionException("Task " + r.toString() +
|
||||
" rejected from " + source);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接丢弃该任务
|
||||
* @param source log name
|
||||
* @return A handler for tasks that cannot be executed by ThreadPool
|
||||
*/
|
||||
public static RejectedExecutionHandler newDiscard(String source) {
|
||||
return (r, p) -> {
|
||||
log.error("[{}] ThreadPool[{}] overload, the task[{}] will be Discard, Maybe you need to adjust the ThreadPool config!", source, p, r);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用线程运行
|
||||
* @param source log name
|
||||
* @return A handler for tasks that cannot be executed by ThreadPool
|
||||
*/
|
||||
public static RejectedExecutionHandler newCallerRun(String source) {
|
||||
return (r, p) -> {
|
||||
log.error("[{}] ThreadPool[{}] overload, the task[{}] will run by caller thread, Maybe you need to adjust the ThreadPool config!", source, p, r);
|
||||
if (!p.isShutdown()) {
|
||||
r.run();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 新线程运行
|
||||
* @param source log name
|
||||
* @return A handler for tasks that cannot be executed by ThreadPool
|
||||
*/
|
||||
public static RejectedExecutionHandler newThreadRun(String source) {
|
||||
return (r, p) -> {
|
||||
log.error("[{}] ThreadPool[{}] overload, the task[{}] will run by a new thread!, Maybe you need to adjust the ThreadPool config!", source, p, r);
|
||||
if (!p.isShutdown()) {
|
||||
String threadName = source + "-T-" + COUNTER.getAndIncrement();
|
||||
log.info("[{}] create new thread[{}] to run job", source, threadName);
|
||||
new Thread(r, threadName).start();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package tech.powerjob.server.common;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.base.Splitter;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Splitter & Joiner
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2020/5/27
|
||||
*/
|
||||
public class SJ {
|
||||
|
||||
public static final Splitter COMMA_SPLITTER = Splitter.on(",");
|
||||
public static final Joiner COMMA_JOINER = Joiner.on(",");
|
||||
|
||||
public static final Joiner MONITOR_JOINER = Joiner.on("|").useForNull("-");
|
||||
|
||||
private static final Splitter.MapSplitter MAP_SPLITTER = Splitter.onPattern(";").withKeyValueSeparator(":");
|
||||
|
||||
public static Map<String, String> splitKvString(String kvString) {
|
||||
return MAP_SPLITTER.split(kvString);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package tech.powerjob.server.common.aware;
|
||||
|
||||
/**
|
||||
* PowerJobAware
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2022/9/12
|
||||
*/
|
||||
public interface PowerJobAware {
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
package tech.powerjob.server.common.aware;
|
||||
|
||||
import tech.powerjob.server.common.module.ServerInfo;
|
||||
|
||||
/**
|
||||
* notify server info
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2022/9/12
|
||||
*/
|
||||
public interface ServerInfoAware extends PowerJobAware {
|
||||
|
||||
void setServerInfo(ServerInfo serverInfo);
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package tech.powerjob.server.common.constants;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 容器类型
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2020/5/15
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ContainerSourceType {
|
||||
|
||||
FatJar(1, "Jar文件"),
|
||||
Git(2, "Git代码库");
|
||||
|
||||
private final int v;
|
||||
private final String des;
|
||||
|
||||
public static ContainerSourceType of(int v) {
|
||||
for (ContainerSourceType type : values()) {
|
||||
if (type.v == v) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("unknown ContainerSourceType of " + v);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
package tech.powerjob.server.common.constants;
|
||||
|
||||
/**
|
||||
* 扩展 key
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2024/12/8
|
||||
*/
|
||||
public interface ExtensionKey {
|
||||
|
||||
interface App {
|
||||
String allowedBecomeAdminByPassword = "allowedBecomeAdminByPassword";
|
||||
}
|
||||
|
||||
interface PwjbUser {
|
||||
String allowedChangePwd = "allowedChangePwd";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package tech.powerjob.server.common.constants;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 任务实例类型
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2020/5/29
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum InstanceType {
|
||||
|
||||
NORMAL(1),
|
||||
WORKFLOW(2);
|
||||
|
||||
private final int v;
|
||||
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package tech.powerjob.server.common.constants;
|
||||
|
||||
/**
|
||||
* 线程池
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2022/9/12
|
||||
*/
|
||||
public class PJThreadPool {
|
||||
|
||||
/**
|
||||
* 定时调度用线程池
|
||||
*/
|
||||
public static final String TIMING_POOL = "PowerJobTimingPool";
|
||||
|
||||
/**
|
||||
* 后台任务异步线程池
|
||||
*/
|
||||
public static final String BACKGROUND_POOL = "PowerJobBackgroundPool";
|
||||
|
||||
/**
|
||||
* 本地数据库专用线程池
|
||||
*/
|
||||
public static final String LOCAL_DB_POOL = "PowerJobLocalDbPool";
|
||||
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package tech.powerjob.server.common.module;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* current server info
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2022/9/12
|
||||
*/
|
||||
@Data
|
||||
public class ServerInfo {
|
||||
|
||||
private Long id;
|
||||
|
||||
private String ip;
|
||||
|
||||
private long bornTime;
|
||||
|
||||
private String version = "UNKNOWN";
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
package tech.powerjob.server.common.module;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import tech.powerjob.common.model.DeployedContainerInfo;
|
||||
import tech.powerjob.common.model.SystemMetrics;
|
||||
import tech.powerjob.common.request.WorkerHeartbeat;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* worker info
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2021/2/7
|
||||
*/
|
||||
@Data
|
||||
@Slf4j
|
||||
public class WorkerInfo {
|
||||
|
||||
private String address;
|
||||
|
||||
/**
|
||||
* 上一次worker在线时间(取 server 端时间)
|
||||
*/
|
||||
private long lastActiveTime;
|
||||
/**
|
||||
* 上一次worker在线时间(取 worker 端时间)
|
||||
*/
|
||||
private long lastActiveWorkerTime;
|
||||
|
||||
private String protocol;
|
||||
|
||||
private String client;
|
||||
|
||||
private String tag;
|
||||
|
||||
private int lightTaskTrackerNum;
|
||||
|
||||
private int heavyTaskTrackerNum;
|
||||
|
||||
private long lastOverloadTime;
|
||||
|
||||
private boolean overloading;
|
||||
|
||||
private SystemMetrics systemMetrics;
|
||||
|
||||
private List<DeployedContainerInfo> containerInfos;
|
||||
|
||||
private static final long WORKER_TIMEOUT_MS = 60000;
|
||||
|
||||
public void refresh(WorkerHeartbeat workerHeartbeat) {
|
||||
address = workerHeartbeat.getWorkerAddress();
|
||||
lastActiveTime = System.currentTimeMillis();
|
||||
lastActiveWorkerTime = workerHeartbeat.getHeartbeatTime();
|
||||
protocol = workerHeartbeat.getProtocol();
|
||||
client = workerHeartbeat.getClient();
|
||||
tag = workerHeartbeat.getTag();
|
||||
systemMetrics = workerHeartbeat.getSystemMetrics();
|
||||
containerInfos = workerHeartbeat.getContainerInfos();
|
||||
|
||||
lightTaskTrackerNum = workerHeartbeat.getLightTaskTrackerNum();
|
||||
heavyTaskTrackerNum = workerHeartbeat.getHeavyTaskTrackerNum();
|
||||
|
||||
if (workerHeartbeat.isOverload()) {
|
||||
overloading = true;
|
||||
lastOverloadTime = System.currentTimeMillis();
|
||||
log.warn("[WorkerInfo] worker {} is overload!", getAddress());
|
||||
} else {
|
||||
overloading = false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean timeout() {
|
||||
long timeout = System.currentTimeMillis() - lastActiveTime;
|
||||
return timeout > WORKER_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
public boolean overload() {
|
||||
return overloading;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,95 @@
|
||||
package tech.powerjob.server.common.spring.condition;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.annotation.Condition;
|
||||
import org.springframework.context.annotation.ConditionContext;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||
import tech.powerjob.common.utils.CollectionUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* PropertyAndOneBeanCondition
|
||||
* 存在多个接口实现时的唯一规则
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2023/7/30
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class PropertyAndOneBeanCondition implements Condition {
|
||||
|
||||
/**
|
||||
* 配置中存在任意一个 Key 即可加载该 Bean,空代表不校验
|
||||
* @return Keys
|
||||
*/
|
||||
protected abstract List<String> anyConfigKey();
|
||||
|
||||
/**
|
||||
* Bean 唯一性校验,空代表不校验
|
||||
* @return beanType
|
||||
*/
|
||||
protected abstract Class<?> beanType();
|
||||
|
||||
@Override
|
||||
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||
|
||||
boolean anyCfgExist = checkAnyConfigExist(context);
|
||||
log.info("[PropertyAndOneBeanCondition] [{}] check any config exist result with keys={}: {}", thisName(), anyConfigKey(), anyCfgExist);
|
||||
if (!anyCfgExist) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Class<?> beanType = beanType();
|
||||
if (beanType == null) {
|
||||
return true;
|
||||
}
|
||||
boolean exist = checkBeanExist(context);
|
||||
log.info("[PropertyAndOneBeanCondition] [{}] bean of type[{}] exist check result: {}", thisName(), beanType.getSimpleName(), exist);
|
||||
if (exist) {
|
||||
log.info("[PropertyAndOneBeanCondition] [{}] bean of type[{}] already exist, skip load!", thisName(), beanType.getSimpleName());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean checkAnyConfigExist(ConditionContext context) {
|
||||
Environment environment = context.getEnvironment();
|
||||
|
||||
List<String> keys = anyConfigKey();
|
||||
|
||||
if (CollectionUtils.isEmpty(keys)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 判断前缀是否符合,任意满足即可
|
||||
for (String key : keys) {
|
||||
if (StringUtils.isNotEmpty(environment.getProperty(key))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean checkBeanExist(ConditionContext context) {
|
||||
|
||||
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
|
||||
if (beanFactory == null) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
beanFactory.getBean(beanType());
|
||||
return true;
|
||||
} catch (NoSuchBeanDefinitionException ignore) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String thisName() {
|
||||
return this.getClass().getSimpleName();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Spring 通用能力包
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2023/7/30
|
||||
*/
|
||||
package tech.powerjob.server.common.spring;
|
||||
@ -0,0 +1,33 @@
|
||||
package tech.powerjob.server.common.thread;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.concurrent.RejectedExecutionHandler;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* @author Echo009
|
||||
* @since 2022/10/12
|
||||
*/
|
||||
@Slf4j
|
||||
public class NewThreadRunRejectedExecutionHandler implements RejectedExecutionHandler {
|
||||
|
||||
private static final AtomicLong COUNTER = new AtomicLong();
|
||||
|
||||
private final String source;
|
||||
|
||||
public NewThreadRunRejectedExecutionHandler(String source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rejectedExecution(Runnable r, ThreadPoolExecutor p) {
|
||||
log.error("[{}] ThreadPool[{}] overload, the task[{}] will run by a new thread!, Maybe you need to adjust the ThreadPool config!", source, p, r);
|
||||
if (!p.isShutdown()) {
|
||||
String threadName = source + "-T-" + COUNTER.getAndIncrement();
|
||||
log.info("[{}] create new thread[{}] to run job", source, threadName);
|
||||
new Thread(r, threadName).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,334 @@
|
||||
package tech.powerjob.server.common.timewheel;
|
||||
|
||||
import tech.powerjob.common.utils.CommonUtils;
|
||||
import tech.powerjob.common.utils.SysUtils;
|
||||
import tech.powerjob.server.common.RejectedExecutionHandlerFactory;
|
||||
import com.google.common.collect.Queues;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.Queue;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 时间轮定时器
|
||||
* 支持的最小精度:1ms(Thread.sleep本身不精确导致精度没法提高)
|
||||
* 最小误差:1ms,理由同上
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2020/4/2
|
||||
*/
|
||||
@Slf4j
|
||||
public class HashedWheelTimer implements Timer {
|
||||
|
||||
private final long tickDuration;
|
||||
private final HashedWheelBucket[] wheel;
|
||||
private final int mask;
|
||||
|
||||
private final Indicator indicator;
|
||||
|
||||
private final long startTime;
|
||||
|
||||
private final Queue<HashedWheelTimerFuture> waitingTasks = Queues.newLinkedBlockingQueue();
|
||||
private final Queue<HashedWheelTimerFuture> canceledTasks = Queues.newLinkedBlockingQueue();
|
||||
|
||||
private final ExecutorService taskProcessPool;
|
||||
|
||||
public HashedWheelTimer(long tickDuration, int ticksPerWheel) {
|
||||
this(tickDuration, ticksPerWheel, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 新建时间轮定时器
|
||||
* @param tickDuration 时间间隔,单位毫秒(ms)
|
||||
* @param ticksPerWheel 轮盘个数
|
||||
* @param processThreadNum 处理任务的线程个数,0代表不启用新线程(如果定时任务需要耗时操作,请启用线程池)
|
||||
*/
|
||||
public HashedWheelTimer(long tickDuration, int ticksPerWheel, int processThreadNum) {
|
||||
|
||||
this.tickDuration = tickDuration;
|
||||
|
||||
// 初始化轮盘,大小格式化为2的N次,可以使用 & 代替取余
|
||||
int ticksNum = CommonUtils.formatSize(ticksPerWheel);
|
||||
wheel = new HashedWheelBucket[ticksNum];
|
||||
for (int i = 0; i < ticksNum; i++) {
|
||||
wheel[i] = new HashedWheelBucket();
|
||||
}
|
||||
mask = wheel.length - 1;
|
||||
|
||||
// 初始化执行线程池
|
||||
if (processThreadNum <= 0) {
|
||||
taskProcessPool = null;
|
||||
}else {
|
||||
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("HashedWheelTimer-Executor-%d").build();
|
||||
// 这里需要调整一下队列大小
|
||||
BlockingQueue<Runnable> queue = Queues.newLinkedBlockingQueue(8192);
|
||||
int core = Math.max(SysUtils.availableProcessors(), processThreadNum);
|
||||
// 基本都是 io 密集型任务
|
||||
taskProcessPool = new ThreadPoolExecutor(core, 2 * core,
|
||||
60, TimeUnit.SECONDS,
|
||||
queue, threadFactory, RejectedExecutionHandlerFactory.newCallerRun("PowerJobTimeWheelPool"));
|
||||
}
|
||||
|
||||
startTime = System.currentTimeMillis();
|
||||
|
||||
// 启动后台线程
|
||||
indicator = new Indicator();
|
||||
new Thread(indicator, "HashedWheelTimer-Indicator").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimerFuture schedule(TimerTask task, long delay, TimeUnit unit) {
|
||||
|
||||
long targetTime = System.currentTimeMillis() + unit.toMillis(delay);
|
||||
HashedWheelTimerFuture timerFuture = new HashedWheelTimerFuture(task, targetTime);
|
||||
|
||||
// 直接运行到期、过期任务
|
||||
if (delay <= 0) {
|
||||
runTask(timerFuture);
|
||||
return timerFuture;
|
||||
}
|
||||
|
||||
// 写入阻塞队列,保证并发安全(性能进一步优化可以考虑 Netty 的 Multi-Producer-Single-Consumer队列)
|
||||
waitingTasks.add(timerFuture);
|
||||
return timerFuture;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<TimerTask> stop() {
|
||||
indicator.stop.set(true);
|
||||
taskProcessPool.shutdown();
|
||||
while (!taskProcessPool.isTerminated()) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
}catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
return indicator.getUnprocessedTasks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 包装 TimerTask,维护预期执行时间、总圈数等数据
|
||||
*/
|
||||
private final class HashedWheelTimerFuture implements TimerFuture {
|
||||
|
||||
// 预期执行时间
|
||||
private final long targetTime;
|
||||
private final TimerTask timerTask;
|
||||
|
||||
// 所属的时间格,用于快速删除该任务
|
||||
private HashedWheelBucket bucket;
|
||||
// 总圈数
|
||||
private long totalTicks;
|
||||
// 当前状态 0 - 初始化等待中,1 - 运行中,2 - 完成,3 - 已取消
|
||||
private int status;
|
||||
|
||||
// 状态枚举值
|
||||
private static final int WAITING = 0;
|
||||
private static final int RUNNING = 1;
|
||||
private static final int FINISHED = 2;
|
||||
private static final int CANCELED = 3;
|
||||
|
||||
public HashedWheelTimerFuture(TimerTask timerTask, long targetTime) {
|
||||
|
||||
this.targetTime = targetTime;
|
||||
this.timerTask = timerTask;
|
||||
this.status = WAITING;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimerTask getTask() {
|
||||
return timerTask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
if (status == WAITING) {
|
||||
status = CANCELED;
|
||||
canceledTasks.add(this);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return status == CANCELED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDone() {
|
||||
return status == FINISHED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间格(本质就是链表,维护了这个时刻可能需要执行的所有任务)
|
||||
*/
|
||||
private final class HashedWheelBucket extends LinkedList<HashedWheelTimerFuture> {
|
||||
|
||||
public void expireTimerTasks(long currentTick) {
|
||||
|
||||
removeIf(timerFuture -> {
|
||||
|
||||
// processCanceledTasks 后外部操作取消任务会导致 BUCKET 中仍存在 CANCELED 任务的情况
|
||||
if (timerFuture.status == HashedWheelTimerFuture.CANCELED) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (timerFuture.status != HashedWheelTimerFuture.WAITING) {
|
||||
log.warn("[HashedWheelTimer] impossible, please fix the bug");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 本轮直接调度
|
||||
if (timerFuture.totalTicks <= currentTick) {
|
||||
|
||||
if (timerFuture.totalTicks < currentTick) {
|
||||
log.warn("[HashedWheelTimer] timerFuture.totalTicks < currentTick, please fix the bug");
|
||||
}
|
||||
|
||||
try {
|
||||
// 提交执行
|
||||
runTask(timerFuture);
|
||||
}catch (Exception ignore) {
|
||||
} finally {
|
||||
timerFuture.status = HashedWheelTimerFuture.FINISHED;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void runTask(HashedWheelTimerFuture timerFuture) {
|
||||
timerFuture.status = HashedWheelTimerFuture.RUNNING;
|
||||
if (taskProcessPool == null) {
|
||||
timerFuture.timerTask.run();
|
||||
}else {
|
||||
taskProcessPool.submit(timerFuture.timerTask);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟指针转动
|
||||
*/
|
||||
private class Indicator implements Runnable {
|
||||
|
||||
private long tick = 0;
|
||||
|
||||
private final AtomicBoolean stop = new AtomicBoolean(false);
|
||||
private final CountDownLatch latch = new CountDownLatch(1);
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
|
||||
while (!stop.get()) {
|
||||
|
||||
// 1. 将任务从队列推入时间轮
|
||||
pushTaskToBucket();
|
||||
// 2. 处理取消的任务
|
||||
processCanceledTasks();
|
||||
// 3. 等待指针跳向下一刻
|
||||
tickTack();
|
||||
// 4. 执行定时任务
|
||||
int currentIndex = (int) (tick & mask);
|
||||
HashedWheelBucket bucket = wheel[currentIndex];
|
||||
bucket.expireTimerTasks(tick);
|
||||
|
||||
tick ++;
|
||||
}
|
||||
latch.countDown();
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟指针转动,当返回时指针已经转到了下一个刻度
|
||||
*/
|
||||
private void tickTack() {
|
||||
|
||||
// 下一次调度的绝对时间
|
||||
long nextTime = startTime + (tick + 1) * tickDuration;
|
||||
long sleepTime = nextTime - System.currentTimeMillis();
|
||||
|
||||
if (sleepTime > 0) {
|
||||
try {
|
||||
Thread.sleep(sleepTime);
|
||||
}catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理被取消的任务
|
||||
*/
|
||||
private void processCanceledTasks() {
|
||||
while (true) {
|
||||
HashedWheelTimerFuture canceledTask = canceledTasks.poll();
|
||||
if (canceledTask == null) {
|
||||
return;
|
||||
}
|
||||
// 从链表中删除该任务(bucket为null说明还没被正式推入时间格中,不需要处理)
|
||||
if (canceledTask.bucket != null) {
|
||||
canceledTask.bucket.remove(canceledTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将队列中的任务推入时间轮中
|
||||
*/
|
||||
private void pushTaskToBucket() {
|
||||
|
||||
while (true) {
|
||||
HashedWheelTimerFuture timerTask = waitingTasks.poll();
|
||||
if (timerTask == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 总共的偏移量
|
||||
long offset = timerTask.targetTime - startTime;
|
||||
// 总共需要走的指针步数
|
||||
timerTask.totalTicks = offset / tickDuration;
|
||||
// 取余计算 bucket index
|
||||
int index = (int) (timerTask.totalTicks & mask);
|
||||
HashedWheelBucket bucket = wheel[index];
|
||||
|
||||
// TimerTask 维护 Bucket 引用,用于删除该任务
|
||||
timerTask.bucket = bucket;
|
||||
|
||||
if (timerTask.status == HashedWheelTimerFuture.WAITING) {
|
||||
bucket.add(timerTask);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Set<TimerTask> getUnprocessedTasks() {
|
||||
try {
|
||||
latch.await();
|
||||
}catch (Exception ignore) {
|
||||
}
|
||||
|
||||
Set<TimerTask> tasks = Sets.newHashSet();
|
||||
|
||||
Consumer<HashedWheelTimerFuture> consumer = timerFuture -> {
|
||||
if (timerFuture.status == HashedWheelTimerFuture.WAITING) {
|
||||
tasks.add(timerFuture.timerTask);
|
||||
}
|
||||
};
|
||||
|
||||
waitingTasks.forEach(consumer);
|
||||
for (HashedWheelBucket bucket : wheel) {
|
||||
bucket.forEach(consumer);
|
||||
}
|
||||
return tasks;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,23 @@
|
||||
package tech.powerjob.server.common.timewheel;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 定时器
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2020/4/2
|
||||
*/
|
||||
public interface Timer {
|
||||
|
||||
/**
|
||||
* 调度定时任务
|
||||
*/
|
||||
TimerFuture schedule(TimerTask task, long delay, TimeUnit unit);
|
||||
|
||||
/**
|
||||
* 停止所有调度任务
|
||||
*/
|
||||
Set<TimerTask> stop();
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package tech.powerjob.server.common.timewheel;
|
||||
|
||||
/**
|
||||
* TimerFuture
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2020/4/3
|
||||
*/
|
||||
public interface TimerFuture {
|
||||
|
||||
TimerTask getTask();
|
||||
|
||||
/**
|
||||
* Attempts to cancel execution of this task. This attempt will
|
||||
* fail if the task has already completed, has already been cancelled,
|
||||
* or could not be cancelled for some other reason. If successful,
|
||||
* and this task has not started when {@code cancel} is called,
|
||||
* this task should never run. If the task has already started,
|
||||
* then the {@code mayInterruptIfRunning} parameter determines
|
||||
* whether the thread executing this task should be interrupted in
|
||||
* an attempt to stop the task.
|
||||
*
|
||||
* <p>After this method returns, subsequent calls to {@link #isDone} will
|
||||
* always return {@code true}. Subsequent calls to {@link #isCancelled}
|
||||
* will always return {@code true} if this method returned {@code true}.
|
||||
*
|
||||
*/
|
||||
boolean cancel();
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this task was cancelled before it completed
|
||||
* normally.
|
||||
*
|
||||
* @return {@code true} if this task was cancelled before it completed
|
||||
*/
|
||||
boolean isCancelled();
|
||||
|
||||
/**
|
||||
* Returns {@code true} if this task completed.
|
||||
*
|
||||
* Completion may be due to normal termination, an exception, or
|
||||
* cancellation -- in all of these cases, this method will return
|
||||
* {@code true}.
|
||||
*
|
||||
* @return {@code true} if this task completed
|
||||
*/
|
||||
boolean isDone();
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package tech.powerjob.server.common.timewheel;
|
||||
|
||||
/**
|
||||
* 时间任务接口
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2020/4/2
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TimerTask extends Runnable {
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package tech.powerjob.server.common.timewheel.holder;
|
||||
|
||||
import tech.powerjob.server.common.timewheel.HashedWheelTimer;
|
||||
import tech.powerjob.server.common.timewheel.Timer;
|
||||
|
||||
/**
|
||||
* 时间轮单例
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2020/4/5
|
||||
*/
|
||||
public class HashedWheelTimerHolder {
|
||||
|
||||
/**
|
||||
* 非精确时间轮,每 5S 走一格
|
||||
*/
|
||||
public static final Timer INACCURATE_TIMER = new HashedWheelTimer(5000, 16, 0);
|
||||
|
||||
private HashedWheelTimerHolder() {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
package tech.powerjob.server.common.timewheel.holder;
|
||||
|
||||
import tech.powerjob.common.utils.SysUtils;
|
||||
import tech.powerjob.server.common.timewheel.HashedWheelTimer;
|
||||
import tech.powerjob.server.common.timewheel.Timer;
|
||||
import tech.powerjob.server.common.timewheel.TimerFuture;
|
||||
import tech.powerjob.server.common.timewheel.TimerTask;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 定时调度任务实例
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2020/7/25
|
||||
*/
|
||||
public class InstanceTimeWheelService {
|
||||
|
||||
private static final Map<Long, TimerFuture> CARGO = Maps.newConcurrentMap();
|
||||
|
||||
/**
|
||||
* 精确调度时间轮,每 1MS 走一格
|
||||
*/
|
||||
private static final Timer TIMER = new HashedWheelTimer(1, 4096, SysUtils.availableProcessors() * 4);
|
||||
/**
|
||||
* 非精确调度时间轮,用于处理高延迟任务,每 10S 走一格
|
||||
*/
|
||||
private static final Timer SLOW_TIMER = new HashedWheelTimer(10000, 12, 0);
|
||||
|
||||
/**
|
||||
* 支持取消的时间间隔,低于该阈值则不会放进 CARGO
|
||||
*/
|
||||
private static final long MIN_INTERVAL_MS = 1000;
|
||||
/**
|
||||
* 长延迟阈值
|
||||
*/
|
||||
private static final long LONG_DELAY_THRESHOLD_MS = 60000;
|
||||
|
||||
/**
|
||||
* 定时调度
|
||||
* @param uniqueId 唯一 ID,必须是 snowflake 算法生成的 ID
|
||||
* @param delayMS 延迟毫秒数
|
||||
* @param timerTask 需要执行的目标方法
|
||||
*/
|
||||
public static void schedule(Long uniqueId, Long delayMS, TimerTask timerTask) {
|
||||
if (delayMS <= LONG_DELAY_THRESHOLD_MS) {
|
||||
realSchedule(uniqueId, delayMS, timerTask);
|
||||
return;
|
||||
}
|
||||
|
||||
long expectTriggerTime = System.currentTimeMillis() + delayMS;
|
||||
TimerFuture longDelayTask = SLOW_TIMER.schedule(() -> {
|
||||
CARGO.remove(uniqueId);
|
||||
realSchedule(uniqueId, expectTriggerTime - System.currentTimeMillis(), timerTask);
|
||||
}, delayMS - LONG_DELAY_THRESHOLD_MS, TimeUnit.MILLISECONDS);
|
||||
CARGO.put(uniqueId, longDelayTask);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 TimerFuture
|
||||
* @param uniqueId 唯一 ID
|
||||
* @return TimerFuture
|
||||
*/
|
||||
public static TimerFuture fetchTimerFuture(Long uniqueId) {
|
||||
return CARGO.get(uniqueId);
|
||||
}
|
||||
|
||||
|
||||
private static void realSchedule(Long uniqueId, Long delayMS, TimerTask timerTask) {
|
||||
TimerFuture timerFuture = TIMER.schedule(() -> {
|
||||
CARGO.remove(uniqueId);
|
||||
timerTask.run();
|
||||
}, delayMS, TimeUnit.MILLISECONDS);
|
||||
if (delayMS > MIN_INTERVAL_MS) {
|
||||
CARGO.put(uniqueId, timerFuture);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
package tech.powerjob.server.common.utils;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import tech.powerjob.common.utils.DigestUtils;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
public class AESUtil {
|
||||
|
||||
|
||||
private static final String ALGORITHM = "AES";
|
||||
private static final String TRANSFORMATION = "AES/GCM/NoPadding";
|
||||
private static final int KEY_SIZE = 256; // AES 256-bit
|
||||
private static final int GCM_NONCE_LENGTH = 12; // GCM nonce length (12 bytes)
|
||||
private static final int GCM_TAG_LENGTH = 16; // GCM authentication tag length (16 bytes)
|
||||
|
||||
// SecureRandom 实例,用于生成 nonce
|
||||
private static final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
/**
|
||||
* 生成密钥
|
||||
*
|
||||
* @param key 传入的密钥字符串,必须是 32 字节(256 位)长度
|
||||
* @return SecretKeySpec 实例
|
||||
*/
|
||||
private static SecretKeySpec getKey(String key) {
|
||||
byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
|
||||
// 不足 32 字节,则使用 MD5 转为 32 位
|
||||
if (keyBytes.length != KEY_SIZE / 8) {
|
||||
keyBytes = DigestUtils.md5(key).getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
return new SecretKeySpec(keyBytes, ALGORITHM);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密
|
||||
*
|
||||
* @param data 要加密的数据
|
||||
* @param key 加密密钥
|
||||
* @return 加密后的数据(Base64 编码),包含 nonce
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static String encrypt(String data, String key) {
|
||||
byte[] nonce = new byte[GCM_NONCE_LENGTH];
|
||||
secureRandom.nextBytes(nonce); // 生成随机的 nonce
|
||||
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, getKey(key), gcmParameterSpec);
|
||||
|
||||
byte[] encryptedData = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
// 将 nonce 和密文连接在一起,然后进行 Base64 编码
|
||||
byte[] combinedData = new byte[nonce.length + encryptedData.length];
|
||||
System.arraycopy(nonce, 0, combinedData, 0, nonce.length);
|
||||
System.arraycopy(encryptedData, 0, combinedData, nonce.length, encryptedData.length);
|
||||
|
||||
return Base64.getEncoder().encodeToString(combinedData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密
|
||||
*
|
||||
* @param encryptedData 要解密的数据(Base64 编码),包含 nonce
|
||||
* @param key 解密密钥
|
||||
* @return 解密后的数据
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static String decrypt(String encryptedData, String key) {
|
||||
byte[] combinedData = Base64.getDecoder().decode(encryptedData);
|
||||
|
||||
// 提取 nonce
|
||||
byte[] nonce = new byte[GCM_NONCE_LENGTH];
|
||||
System.arraycopy(combinedData, 0, nonce, 0, nonce.length);
|
||||
|
||||
// 提取实际的加密数据
|
||||
byte[] encryptedText = new byte[combinedData.length - nonce.length];
|
||||
System.arraycopy(combinedData, nonce.length, encryptedText, 0, encryptedText.length);
|
||||
|
||||
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
|
||||
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, nonce);
|
||||
cipher.init(Cipher.DECRYPT_MODE, getKey(key), gcmParameterSpec);
|
||||
|
||||
byte[] decryptedData = cipher.doFinal(encryptedText);
|
||||
return new String(decryptedData, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
package tech.powerjob.server.common.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.Signature;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ExpressionParser;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
* AOP Utils
|
||||
*
|
||||
* @author tjq
|
||||
* @since 1/16/21
|
||||
*/
|
||||
@Slf4j
|
||||
public class AOPUtils {
|
||||
|
||||
private static final ExpressionParser PARSER = new SpelExpressionParser();
|
||||
private static final ParameterNameDiscoverer DISCOVERER = new LocalVariableTableParameterNameDiscoverer();
|
||||
|
||||
public static String parseRealClassName(JoinPoint joinPoint) {
|
||||
return joinPoint.getSignature().getDeclaringType().getSimpleName();
|
||||
}
|
||||
|
||||
public static Method parseMethod(ProceedingJoinPoint joinPoint) {
|
||||
Signature pointSignature = joinPoint.getSignature();
|
||||
if (!(pointSignature instanceof MethodSignature)) {
|
||||
throw new IllegalArgumentException("this annotation should be used on a method!");
|
||||
}
|
||||
MethodSignature signature = (MethodSignature) pointSignature;
|
||||
Method method = signature.getMethod();
|
||||
if (method.getDeclaringClass().isInterface()) {
|
||||
try {
|
||||
method = joinPoint.getTarget().getClass().getDeclaredMethod(pointSignature.getName(), method.getParameterTypes());
|
||||
} catch (SecurityException | NoSuchMethodException e) {
|
||||
ExceptionUtils.rethrow(e);
|
||||
}
|
||||
}
|
||||
return method;
|
||||
}
|
||||
|
||||
public static <T> T parseSpEl(Method method, Object[] arguments, String spEl, Class<T> clazz, T defaultResult) {
|
||||
String[] params = DISCOVERER.getParameterNames(method);
|
||||
assert params != null;
|
||||
|
||||
EvaluationContext context = new StandardEvaluationContext();
|
||||
for (int len = 0; len < params.length; len++) {
|
||||
context.setVariable(params[len], arguments[len]);
|
||||
}
|
||||
try {
|
||||
Expression expression = PARSER.parseExpression(spEl);
|
||||
return expression.getValue(context, clazz);
|
||||
} catch (Exception e) {
|
||||
log.error("[AOPUtils] parse SpEL failed for method[{}], please concat @tjq to fix the bug!", method.getName(), e);
|
||||
return defaultResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,114 @@
|
||||
package tech.powerjob.server.common.utils;
|
||||
|
||||
import tech.powerjob.common.utils.CommonUtils;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
import org.springframework.util.DigestUtils;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.*;
|
||||
import java.net.URLEncoder;
|
||||
|
||||
/**
|
||||
* 文件工具类,统一文件存放地址
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2020/5/15
|
||||
*/
|
||||
public class OmsFileUtils {
|
||||
|
||||
private static final String USER_HOME = System.getProperty("user.home", "oms");
|
||||
private static final String COMMON_PATH = USER_HOME + "/powerjob/server/";
|
||||
|
||||
/**
|
||||
* 获取在线日志的存放路径
|
||||
* @return 在线日志的存放路径
|
||||
*/
|
||||
public static String genLogDirPath() {
|
||||
return COMMON_PATH + "online_log/";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用于构建容器的 jar 文件存放路径
|
||||
* @return 路径
|
||||
*/
|
||||
public static String genContainerJarPath() {
|
||||
return COMMON_PATH + "container/";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取临时目录(固定目录)
|
||||
* @return 目录
|
||||
*/
|
||||
public static String genTemporaryPath() {
|
||||
return COMMON_PATH + "temporary/";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取临时目录(随机目录,不会重复),用完记得删除
|
||||
* @return 临时目录
|
||||
*/
|
||||
public static String genTemporaryWorkPath() {
|
||||
return genTemporaryPath() + CommonUtils.genUUID() + "/";
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 H2 数据库工作目录
|
||||
* @return H2 工作目录
|
||||
*/
|
||||
public static String genH2BasePath() {
|
||||
return COMMON_PATH + "h2/";
|
||||
}
|
||||
public static String genH2WorkPath() {
|
||||
return genH2BasePath() + CommonUtils.genUUID() + "/";
|
||||
}
|
||||
|
||||
/**
|
||||
* 将文本写入文件
|
||||
* @param content 文本内容
|
||||
* @param file 文件
|
||||
*/
|
||||
public static void string2File(String content, File file) {
|
||||
try(FileWriter fw = new FileWriter(file)) {
|
||||
fw.write(content);
|
||||
}catch (IOException ie) {
|
||||
ExceptionUtils.rethrow(ie);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出文件(对外下载功能)
|
||||
* @param file 文件
|
||||
* @param response HTTP响应
|
||||
* @throws IOException 异常
|
||||
*/
|
||||
public static void file2HttpResponse(File file, HttpServletResponse response) throws IOException {
|
||||
|
||||
response.setContentType("application/octet-stream");
|
||||
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(file.getName(), "UTF-8"));
|
||||
|
||||
byte[] buffer = new byte[4096];
|
||||
try (BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());
|
||||
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
|
||||
|
||||
// https://github.com/PowerJob/PowerJob/pull/939/files
|
||||
int cnt;
|
||||
while ((cnt = bis.read(buffer)) != -1) {
|
||||
bos.write(buffer, 0, cnt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算文件的 MD5
|
||||
* @param f 文件
|
||||
* @return md5
|
||||
* @throws IOException 异常
|
||||
*/
|
||||
public static String md5(File f) throws IOException {
|
||||
String md5;
|
||||
try(FileInputStream fis = new FileInputStream(f)) {
|
||||
md5 = DigestUtils.md5DigestAsHex(fis);
|
||||
}
|
||||
return md5;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package tech.powerjob.server.common.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URL;
|
||||
import java.util.Objects;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* 加载配置文件
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2020/5/18
|
||||
*/
|
||||
@Slf4j
|
||||
public class PropertyUtils {
|
||||
|
||||
private static final Properties PROPERTIES = new Properties();
|
||||
|
||||
public static Properties getProperties() {
|
||||
return PROPERTIES;
|
||||
}
|
||||
|
||||
public static void init() {
|
||||
URL propertiesURL =PropertyUtils.class.getClassLoader().getResource("application.properties");
|
||||
Objects.requireNonNull(propertiesURL);
|
||||
try (InputStream is = propertiesURL.openStream()) {
|
||||
PROPERTIES.load(is);
|
||||
}catch (Exception e) {
|
||||
ExceptionUtils.rethrow(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package tech.powerjob.server.common.utils;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Spring ApplicationContext 工具类
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2020/4/7
|
||||
*/
|
||||
@Component
|
||||
public class SpringUtils implements ApplicationContextAware {
|
||||
|
||||
private static ApplicationContext context;
|
||||
|
||||
public static <T> T getBean(Class<T> clz) {
|
||||
return context.getBean(clz);
|
||||
}
|
||||
|
||||
public static Object getBean(String beanName) {
|
||||
return context.getBean(beanName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
|
||||
context = ctx;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
package tech.powerjob.server.common.utils;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.google.common.collect.Maps;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 开发团队专用测试工具
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2023/7/31
|
||||
*/
|
||||
public class TestUtils {
|
||||
|
||||
private static final String TEST_CONFIG_NAME = "/.powerjob_test";
|
||||
|
||||
public static final String KEY_PHONE_NUMBER = "phone";
|
||||
|
||||
public static final String KEY_MONGO_URI = "mongoUri";
|
||||
|
||||
/**
|
||||
* 获取本地的测试配置,主要用于存放一些密钥
|
||||
* @return 测试配置
|
||||
*/
|
||||
public static Map<String, Object> fetchTestConfig() {
|
||||
try {
|
||||
// 后续本地测试,密钥相关的内容统一存入 .powerjob_test 中,方便管理
|
||||
String content = FileUtils.readFileToString(new File(System.getProperty("user.home").concat(TEST_CONFIG_NAME)), StandardCharsets.UTF_8);
|
||||
if (StringUtils.isNotEmpty(content)) {
|
||||
return JSONObject.parseObject(content);
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
return Maps.newHashMap();
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,100 @@
|
||||
package tech.powerjob.server.common.utils;
|
||||
|
||||
import tech.powerjob.common.RemoteConstant;
|
||||
import com.google.common.collect.Lists;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.net.ntp.NTPUDPClient;
|
||||
import org.apache.commons.net.ntp.NtpV3Packet;
|
||||
import org.apache.commons.net.ntp.TimeInfo;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 时间工具
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2020/5/19
|
||||
*/
|
||||
@Slf4j
|
||||
public class TimeUtils {
|
||||
|
||||
/**
|
||||
* NTP 授时服务器(阿里云 -> 交大 -> 水果)
|
||||
*/
|
||||
private static final List<String> NTP_SERVER_LIST = Lists.newArrayList("ntp.aliyun.com", "ntp.sjtu.edu.cn", "time1.apple.com");
|
||||
/**
|
||||
* 最大误差 5S
|
||||
*/
|
||||
private static final long MAX_OFFSET = 5000;
|
||||
|
||||
/**
|
||||
* 根据蔡勒公式计算任意一个日期是星期几
|
||||
* @param year 年
|
||||
* @param month 月
|
||||
* @param day 日
|
||||
* @return 中国星期
|
||||
*/
|
||||
public static int calculateWeek(int year, int month, int day) {
|
||||
if (month == 1) {
|
||||
month = 13;
|
||||
year--;
|
||||
}
|
||||
if (month == 2) {
|
||||
month = 14;
|
||||
year--;
|
||||
}
|
||||
int y = year % 100;
|
||||
int c = year /100 ;
|
||||
int h = (y + (y / 4) + (c / 4) - (2 * c) + ((26 * (month + 1)) / 10) + day - 1) % 7;
|
||||
//可能是负值,因此计算除以7的余数之后需要判断是大于等于0还是小于0,如果小于0则将余数加7。
|
||||
if (h < 0){
|
||||
h += 7;
|
||||
}
|
||||
|
||||
// 国内理解中星期日为 7
|
||||
if (h == 0) {
|
||||
return 7;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
public static void check() throws TimeCheckException {
|
||||
|
||||
NTPUDPClient timeClient = new NTPUDPClient();
|
||||
|
||||
try {
|
||||
timeClient.setDefaultTimeout((int) RemoteConstant.DEFAULT_TIMEOUT_MS);
|
||||
for (String address : NTP_SERVER_LIST) {
|
||||
try {
|
||||
TimeInfo t = timeClient.getTime(InetAddress.getByName(address));
|
||||
NtpV3Packet ntpV3Packet = t.getMessage();
|
||||
log.info("[TimeUtils] use ntp server: {}, request result: {}", address, ntpV3Packet);
|
||||
// RFC-1305标准:https://tools.ietf.org/html/rfc1305
|
||||
// 忽略传输误差吧...也就几十毫秒的事(阿里云给力啊!)
|
||||
long local = System.currentTimeMillis();
|
||||
long ntp = ntpV3Packet.getTransmitTimeStamp().getTime();
|
||||
long offset = local - ntp;
|
||||
if (Math.abs(offset) > MAX_OFFSET) {
|
||||
String msg = String.format("inaccurate server time(local:%d, ntp:%d), please use ntp update to calibration time", local, ntp);
|
||||
throw new TimeCheckException(msg);
|
||||
}
|
||||
return;
|
||||
}catch (Exception ignore) {
|
||||
log.warn("[TimeUtils] ntp server: {} may down!", address);
|
||||
}
|
||||
}
|
||||
throw new TimeCheckException("no available ntp server, maybe alibaba, sjtu and apple are both collapse");
|
||||
}finally {
|
||||
timeClient.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
public static final class TimeCheckException extends RuntimeException {
|
||||
public TimeCheckException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
package tech.powerjob.server.common.utils;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/**
|
||||
* AESUtilTest
|
||||
*
|
||||
* @author tjq
|
||||
* @since 2024/8/10
|
||||
*/
|
||||
class AESUtilTest {
|
||||
|
||||
@Test
|
||||
void testAes() throws Exception {
|
||||
|
||||
String sk = "ChinaNo.1_ChinaNo.1_ChinaNo.1";
|
||||
|
||||
String txt = "kyksjdfh";
|
||||
|
||||
String encrypt = AESUtil.encrypt(txt, sk);
|
||||
System.out.println(encrypt);
|
||||
String decrypt = AESUtil.decrypt(encrypt, sk);
|
||||
System.out.println(decrypt);
|
||||
|
||||
assert txt.equals(decrypt);
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user