支持定时任务

This commit is contained in:
mxd 2022-01-06 23:39:46 +08:00
parent ac22b10597
commit 81930ab61c
15 changed files with 333 additions and 86 deletions

View File

@ -22,6 +22,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;
@ -388,6 +389,18 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
return new DataSourceMagicDynamicRegistry(dataSourceInfoMagicResourceStorage, magicDynamicDataSource);
}
@Bean
@ConditionalOnMissingBean
public TaskInfoMagicResourceStorage taskInfoMagicResourceStorage() {
return new TaskInfoMagicResourceStorage();
}
@Bean
@ConditionalOnMissingBean
public TaskMagicDynamicRegistry taskMagicDynamicRegistry(TaskInfoMagicResourceStorage taskInfoMagicResourceStorage, TaskScheduler taskScheduler) {
return new TaskMagicDynamicRegistry(taskInfoMagicResourceStorage, taskScheduler);
}
@Bean
@ConditionalOnMissingBean(MagicNotifyService.class)
@ -572,6 +585,7 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
mapping.registerController(magicWorkbenchController)
.registerController(new MagicResourceController(configuration))
.registerController(new MagicDataSourceController(configuration))
.registerController(new MagicTaskController(configuration))
.registerController(new MagicBackupController(configuration));
}
// 注册接收推送的接口

View File

@ -0,0 +1,30 @@
package org.ssssssss.magicapi.controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.ssssssss.magicapi.config.MagicConfiguration;
import org.ssssssss.magicapi.model.DebugRequest;
import org.ssssssss.magicapi.model.JsonBean;
import org.ssssssss.magicapi.model.MagicEntity;
import org.ssssssss.magicapi.script.ScriptManager;
import org.ssssssss.script.MagicScriptDebugContext;
import javax.servlet.http.HttpServletRequest;
public class MagicTaskController extends MagicController implements MagicExceptionHandler{
public MagicTaskController(MagicConfiguration configuration) {
super(configuration);
}
@PostMapping("/task/execute")
@ResponseBody
public JsonBean<Object> execute(String id, HttpServletRequest request){
MagicEntity entity = MagicConfiguration.getMagicResourceService().file(id);
notNull(entity, FILE_NOT_FOUND);
String script = entity.getScript();
DebugRequest debugRequest = DebugRequest.create(request);
MagicScriptDebugContext magicScriptContext = debugRequest.createMagicScriptContext(configuration.getDebugTimeout());
return new JsonBean<>(ScriptManager.executeScript(script, magicScriptContext));
}
}

View File

@ -168,7 +168,8 @@ public class RequestHandler extends MagicController {
return afterCompletion(requestEntity, value);
}
if (requestedFromTest) {
String sessionAndScriptId = requestEntity.getRequestedClientId() + requestEntity.getRequestedScriptId();
DebugRequest debugRequest = requestEntity.getDebugRequest();
String sessionAndScriptId = debugRequest.getRequestedClientId() + debugRequest.getRequestedScriptId();
try {
if (context instanceof MagicScriptDebugContext) {
WebSocketSessionManager.addMagicScriptContext(sessionAndScriptId, (MagicScriptDebugContext) context);
@ -358,8 +359,8 @@ public class RequestHandler extends MagicController {
} while ((parent = parent.getCause()) != null);
if (se != null && requestEntity.isRequestedFromTest()) {
Span.Line line = se.getLine();
WebSocketSessionManager.sendByClientId(requestEntity.getRequestedClientId(), EXCEPTION, Arrays.asList(
requestEntity.getRequestedScriptId(),
WebSocketSessionManager.sendByClientId(requestEntity.getDebugRequest().getRequestedClientId(), EXCEPTION, Arrays.asList(
requestEntity.getDebugRequest().getRequestedScriptId(),
se.getSimpleMessage(),
line == null ? null : Arrays.asList(line.getLineNumber(), line.getEndLineNumber(), line.getStartCol(), line.getEndCol())
));
@ -395,24 +396,13 @@ public class RequestHandler extends MagicController {
* 构建 MagicScriptContext
*/
private MagicScriptContext createMagicScriptContext(String scriptName, RequestEntity requestEntity) {
List<Integer> breakpoints = requestEntity.getRequestedBreakpoints();
DebugRequest debugRequest = requestEntity.getDebugRequest();
List<Integer> breakpoints = debugRequest.getRequestedBreakpoints();
// 构建脚本上下文
MagicScriptContext context;
// TODO 安全校验
if (requestEntity.isRequestedFromDebug() && breakpoints.size() > 0) {
MagicScriptDebugContext debugContext = new MagicScriptDebugContext(requestEntity.getRequestedBreakpoints());
String scriptId = requestEntity.getRequestedScriptId();
String clientId = requestEntity.getRequestedClientId();
debugContext.setTimeout(configuration.getDebugTimeout());
debugContext.setId(scriptId);
debugContext.setCallback(variables -> {
List<Map<String, Object>> varList = (List<Map<String, Object>>) variables.get("variables");
varList.stream().filter(it -> it.containsKey("value")).forEach(variable -> {
variable.put("value", JsonUtils.toJsonStringWithoutLog(variable.get("value")));
});
WebSocketSessionManager.sendByClientId(clientId, BREAKPOINT, scriptId, variables);
});
context = debugContext;
context = debugRequest.createMagicScriptContext(configuration.getDebugTimeout());
} else {
context = new MagicScriptContext();
}

View File

@ -0,0 +1,73 @@
package org.ssssssss.magicapi.model;
import org.ssssssss.magicapi.config.WebSocketSessionManager;
import org.ssssssss.magicapi.utils.JsonUtils;
import org.ssssssss.script.MagicScriptDebugContext;
import org.ssssssss.script.functions.ObjectConvertExtension;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.ssssssss.magicapi.config.MessageType.BREAKPOINT;
import static org.ssssssss.magicapi.model.Constants.*;
public class DebugRequest {
private HttpServletRequest request;
private DebugRequest(HttpServletRequest request) {
this.request = request;
}
public static DebugRequest create(HttpServletRequest request){
return new DebugRequest(request);
}
/**
* 获得断点
*/
public List<Integer> getRequestedBreakpoints() {
String breakpoints = request.getHeader(HEADER_REQUEST_BREAKPOINTS);
if (breakpoints != null) {
return Arrays.stream(breakpoints.split(","))
.map(val -> ObjectConvertExtension.asInt(val, -1))
.filter(it -> it > 0)
.collect(Collectors.toList());
}
return Collections.emptyList();
}
/**
* 获取测试scriptId
*/
public String getRequestedScriptId() {
return request.getHeader(HEADER_REQUEST_SCRIPT_ID);
}
/**
* 获取测试clientId
*/
public String getRequestedClientId() {
return request.getHeader(HEADER_REQUEST_CLIENT_ID);
}
public MagicScriptDebugContext createMagicScriptContext(int debugTimeout){
MagicScriptDebugContext debugContext = new MagicScriptDebugContext(getRequestedBreakpoints());
String scriptId = getRequestedScriptId();
String clientId = getRequestedClientId();
debugContext.setTimeout(debugTimeout);
debugContext.setId(scriptId);
debugContext.setCallback(variables -> {
List<Map<String, Object>> varList = (List<Map<String, Object>>) variables.get("variables");
varList.stream().filter(it -> it.containsKey("value")).forEach(variable -> {
variable.put("value", JsonUtils.toJsonStringWithoutLog(variable.get("value")));
});
WebSocketSessionManager.sendByClientId(clientId, BREAKPOINT, scriptId, variables);
});
return debugContext;
}
}

View File

@ -55,6 +55,8 @@ public interface JsonCodeConstants {
JsonCode GROUP_ID_REQUIRED = new JsonCode(0, "请选择分组");
JsonCode CRON_ID_REQUIRED = new JsonCode(0, "cron表达式不能为空");
JsonCode NAME_INVALID = new JsonCode(0, "名称不能包含特殊字符,只允许中文、数字、字母以及+_-.()的组合且不能.开头");
JsonCode DATASOURCE_KEY_INVALID = new JsonCode(0, "数据源Key不能包含特殊字符只允许中文、数字、字母以及_组合");

View File

@ -1,14 +1,11 @@
package org.ssssssss.magicapi.model;
import org.ssssssss.script.MagicScriptContext;
import org.ssssssss.script.functions.ObjectConvertExtension;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.*;
import java.util.stream.Collectors;
import static org.ssssssss.magicapi.model.Constants.*;
import java.util.Map;
import java.util.UUID;
/**
* 请求信息
@ -27,6 +24,7 @@ public class RequestEntity {
private Map<String, Object> pathVariables;
private MagicScriptContext magicScriptContext;
private Object requestBody;
private DebugRequest debugRequest;
private Map<String, Object> headers;
@ -53,6 +51,7 @@ public class RequestEntity {
public RequestEntity request(HttpServletRequest request) {
this.request = request;
this.debugRequest = DebugRequest.create(request);
return this;
}
@ -75,7 +74,7 @@ public class RequestEntity {
}
public boolean isRequestedFromDebug() {
return requestedFromTest && !getRequestedBreakpoints().isEmpty();
return requestedFromTest && !this.debugRequest.getRequestedBreakpoints().isEmpty();
}
public Map<String, Object> getParameters() {
@ -134,31 +133,7 @@ public class RequestEntity {
return this;
}
/**
* 获取测试scriptId
*/
public String getRequestedScriptId() {
return request.getHeader(HEADER_REQUEST_SCRIPT_ID);
}
/**
* 获取测试clientId
*/
public String getRequestedClientId() {
return request.getHeader(HEADER_REQUEST_CLIENT_ID);
}
/**
* 获得断点
*/
public List<Integer> getRequestedBreakpoints() {
String breakpoints = request.getHeader(HEADER_REQUEST_BREAKPOINTS);
if (breakpoints != null) {
return Arrays.stream(breakpoints.split(","))
.map(val -> ObjectConvertExtension.asInt(val, -1))
.filter(it -> it > 0)
.collect(Collectors.toList());
}
return Collections.emptyList();
public DebugRequest getDebugRequest() {
return debugRequest;
}
}

View File

@ -0,0 +1,67 @@
package org.ssssssss.magicapi.model;
import java.util.Objects;
public class TaskInfo extends PathMagicEntity{
/**
* cron 表达式
*/
private String cron;
/**
* 是否启用
*/
private boolean enabled;
public String getCron() {
return cron;
}
public void setCron(String cron) {
this.cron = cron;
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public TaskInfo copy() {
TaskInfo info = new TaskInfo();
super.copyTo(info);
info.setCron(this.cron);
info.setEnabled(this.enabled);
return info;
}
@Override
public MagicEntity simple() {
TaskInfo info = new TaskInfo();
super.simple(info);
return info;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
TaskInfo taskInfo = (TaskInfo) o;
return Objects.equals(id, taskInfo.id) &&
Objects.equals(path, taskInfo.path) &&
Objects.equals(script, taskInfo.script) &&
Objects.equals(name, taskInfo.name) &&
Objects.equals(cron, taskInfo.cron) &&
Objects.equals(enabled, taskInfo.enabled);
}
@Override
public int hashCode() {
return Objects.hash(id, path, script, name, groupId, cron, enabled);
}
}

View File

@ -68,9 +68,7 @@ public interface MagicResourceStorage<T extends MagicEntity> {
return read(resource.read());
}
default String buildMappingKey(T entity) {
return null;
}
String buildMappingKey(T entity);
default String buildScriptName(T entity) {
return null;

View File

@ -0,0 +1,27 @@
package org.ssssssss.magicapi.service.impl;
import org.ssssssss.magicapi.model.ApiInfo;
import org.ssssssss.magicapi.model.TaskInfo;
public class TaskInfoMagicResourceStorage extends AbstractPathMagicResourceStorage<TaskInfo> {
@Override
public String folder() {
return "task";
}
@Override
public Class<TaskInfo> magicClass() {
return TaskInfo.class;
}
@Override
public void validate(TaskInfo entity) {
notBlank(entity.getCron(), CRON_ID_REQUIRED);
}
@Override
public String buildMappingKey(TaskInfo info) {
return buildMappingKey(info, magicResourceService.getGroupPath(info.getGroupId()));
}
}

View File

@ -0,0 +1,71 @@
package org.ssssssss.magicapi.service.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.config.CronTask;
import org.ssssssss.magicapi.event.FileEvent;
import org.ssssssss.magicapi.event.GroupEvent;
import org.ssssssss.magicapi.model.TaskInfo;
import org.ssssssss.magicapi.provider.MagicResourceStorage;
import org.ssssssss.magicapi.script.ScriptManager;
import org.ssssssss.magicapi.service.AbstractMagicDynamicRegistry;
import org.ssssssss.script.MagicScriptContext;
import java.util.concurrent.ScheduledFuture;
public class TaskMagicDynamicRegistry extends AbstractMagicDynamicRegistry<TaskInfo> {
private final TaskScheduler taskScheduler;
private static final Logger logger = LoggerFactory.getLogger(TaskMagicDynamicRegistry.class);
public TaskMagicDynamicRegistry(MagicResourceStorage<TaskInfo> magicResourceStorage, TaskScheduler taskScheduler) {
super(magicResourceStorage);
this.taskScheduler = taskScheduler;
}
@EventListener(condition = "#event.type == 'task'")
public void onFileEvent(FileEvent event) {
processEvent(event);
}
@EventListener(condition = "#event.type == 'task'")
public void onGroupEvent(GroupEvent event) {
processEvent(event);
}
@Override
protected boolean register(MappingNode<TaskInfo> mappingNode) {
TaskInfo info = mappingNode.getEntity();
CronTask cronTask = new CronTask(() -> {
try {
ScriptManager.executeScript(info.getScript(), new MagicScriptContext());
} catch (Exception e) {
logger.error("定时任务执行出错", e);
}
}, info.getCron());
mappingNode.setMappingData(taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger()));
if(taskScheduler != null){
logger.debug("注册定时任务:[{}, {}, {}]", info.getName(), info.getPath(), info.getCron());
} else {
logger.debug("注册定时任务失败:[{}, {}, {}] 当前 TaskScheduler 为空", info.getName(), info.getPath(), info.getCron());
}
return true;
}
@Override
protected void unregister(MappingNode<TaskInfo> mappingNode) {
TaskInfo info = mappingNode.getEntity();
logger.debug("取消注册定时任务:[{}, {}, {}]", info.getName(), info.getPath(), info.getCron());
ScheduledFuture<?> scheduledFuture = (ScheduledFuture<?>) mappingNode.getMappingData();
if(scheduledFuture != null){
try {
scheduledFuture.cancel(true);
} catch (Exception ignored) {
}
}
}
}

View File

@ -1 +1 @@
import"./app.e9d5d532.js";import"./vue.7304e5c5.js";import"./vendor.5f04ef2d.js";import"./axios.23e7b955.js";const s=function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function n(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerpolicy&&(r.referrerPolicy=e.referrerpolicy),e.crossorigin==="use-credentials"?r.credentials="include":e.crossorigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(e){if(e.ep)return;e.ep=!0;const r=n(e);fetch(e.href,r)}};s();
import"./app.968aee6d.js";import"./vue.7304e5c5.js";import"./vendor.5f04ef2d.js";import"./axios.23e7b955.js";const s=function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const e of document.querySelectorAll('link[rel="modulepreload"]'))i(e);new MutationObserver(e=>{for(const r of e)if(r.type==="childList")for(const o of r.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&i(o)}).observe(document,{childList:!0,subtree:!0});function n(e){const r={};return e.integrity&&(r.integrity=e.integrity),e.referrerpolicy&&(r.referrerPolicy=e.referrerpolicy),e.crossorigin==="use-credentials"?r.credentials="include":e.crossorigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function i(e){if(e.ep)return;e.ep=!0;const r=n(e);fetch(e.href,r)}};s();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -23,12 +23,12 @@
@keyframes stretch {0% {transform: scale(1);}25% {transform: scale(1.2);}50% {transform: scale(1);}100% {transform: scale(1);}}
@keyframes blink-loading {0% {opacity: 1;}50% {opacity: 0.5;}100% {opacity: 1;}}
</style>
<script type="module" crossorigin src="./assets/index.dd6bc269.js"></script>
<script type="module" crossorigin src="./assets/index.2cdad179.js"></script>
<link rel="modulepreload" href="./assets/vue.7304e5c5.js">
<link rel="modulepreload" href="./assets/axios.23e7b955.js">
<link rel="modulepreload" href="./assets/vendor.5f04ef2d.js">
<link rel="modulepreload" href="./assets/app.e9d5d532.js">
<link rel="stylesheet" href="./assets/style.079cdc93.css">
<link rel="modulepreload" href="./assets/app.968aee6d.js">
<link rel="stylesheet" href="./assets/style.a7af0077.css">
</head>
<body>
<div class="magic-loading-wrapper" id="magic-loading-wrapper">