---初始化项目

This commit is contained in:
2025-09-19 16:14:08 +08:00
parent 902d3d7e3b
commit afee7c03ac
767 changed files with 75809 additions and 82 deletions

View 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>

View File

@ -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;
}
}

View File

@ -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");
}

View File

@ -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";
}

View File

@ -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();
}
};
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,10 @@
package tech.powerjob.server.common.aware;
/**
* PowerJobAware
*
* @author tjq
* @since 2022/9/12
*/
public interface PowerJobAware {
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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";
}
}

View File

@ -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;
}

View File

@ -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";
}

View File

@ -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";
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,7 @@
/**
* Spring 通用能力包
*
* @author tjq
* @since 2023/7/30
*/
package tech.powerjob.server.common.spring;

View File

@ -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();
}
}
}

View File

@ -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;
/**
* 时间轮定时器
* 支持的最小精度1msThread.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;
}
}
}

View File

@ -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();
}

View File

@ -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();
}

View File

@ -0,0 +1,11 @@
package tech.powerjob.server.common.timewheel;
/**
* 时间任务接口
*
* @author tjq
* @since 2020/4/2
*/
@FunctionalInterface
public interface TimerTask extends Runnable {
}

View File

@ -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() {
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}