初步实现插件机制

This commit is contained in:
mxd
2022-01-22 22:40:39 +08:00
parent e6578fc3b3
commit 5710801fda
75 changed files with 985 additions and 469 deletions

View File

@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.ssssssss</groupId>
<artifactId>magic-api-plugins</artifactId>
<version>2.0.0-alpha.2</version>
</parent>
<artifactId>magic-api-plugin-task</artifactId>
<packaging>jar</packaging>
<name>magic-api-plugin-task</name>
<description>magic-api-plugin-task</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<!-- npm install && npm run build -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<executions>
<execution>
<id>exec-npm-install</id>
<phase>generate-resources</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>npm</executable>
<arguments>
<argument>install</argument>
</arguments>
<workingDirectory>${basedir}/src/console</workingDirectory>
</configuration>
</execution>
<execution>
<id>exec-npm-run-build</id>
<phase>generate-resources</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>npm</executable>
<arguments>
<argument>run</argument>
<argument>build</argument>
</arguments>
<workingDirectory>${basedir}/src/console</workingDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.2.0</version>
<executions>
<execution>
<id>copy-resource</id>
<phase>generate-resources</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/classes/magic-editor/plugins</outputDirectory>
<resources>
<resource>
<directory>${basedir}/src/console/dist</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,18 @@
{
"name": "magic-test",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "vite build"
},
"author": "",
"license": "ISC",
"devDependencies": {
"vue": "^3.2.26",
"@vitejs/plugin-vue": "^2.0.1",
"vite-plugin-svg-icons": "^1.1.0",
"vite": "^2.7.10"
}
}

View File

@@ -0,0 +1,49 @@
<template>
<div class="magic-task-info">
<form>
<label>{{ $i('message.enable') }}</label>
<magic-checkbox v-model:value="info.enabled" />
<label>cron</label>
<magic-input v-model:value="info.cron" :placeholder="$i('task.form.placeholder.cron')" width="250px"/>
<label>{{ $i('task.form.name') }}</label>
<magic-input v-model:value="info.name" :placeholder="$i('task.form.placeholder.name')" width="250px"/>
<label>{{ $i('task.form.path') }}</label>
<magic-input v-model:value="info.path" :placeholder="$i('task.form.placeholder.path')" width="auto" style="flex:1"/>
</form>
<div style="flex:1;padding-top:5px;">
<magic-textarea v-model:value="info.description" :placeholder="$i('task.form.placeholder.description')"/>
</div>
</div>
</template>
<script setup>
import { inject } from 'vue'
const $i = inject('i18n.format')
const info = inject('info')
</script>
<style scoped>
.magic-task-info{
display: flex;
flex-direction: column;
flex: 1;
padding: 5px;
}
.magic-task-info form{
display: flex;
}
.magic-task-info form label{
display: inline-block;
width: 75px;
height: 22px;
line-height: 22px;
font-weight: 400;
text-align: right;
padding: 0 5px;
}
.magic-task-info form :deep(.magic-checkbox){
width: 22px;
height: 22px;
}
.magic-task-info form :deep(.magic-textarea){
margin: 5px;
}
</style>

View File

@@ -0,0 +1,16 @@
export default {
task: {
title: 'Task Info',
name: 'Task',
form: {
name: 'Task Name',
path: 'Task Path',
placeholder: {
cron: 'Please Enter Cron Expression',
name: 'Please Enter Task Name',
path: 'Please Enter Task Path',
description: 'Please Enter Task Description'
}
}
},
}

View File

@@ -0,0 +1,16 @@
export default {
task: {
title: '定时任务信息',
name: '定时任务',
form: {
name: '任务名称',
path: '任务路径',
placeholder: {
cron: '请输入Cron表达式',
name: '请输入任务名称',
path: '请输入任务路径',
description: '请输入任务描述'
}
}
}
}

View File

@@ -0,0 +1 @@
<svg class="icon" style="width: 1em;height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden;" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M512.78747336 189.40294037A372.25009177 372.25009177 0 1 1 512.73122556 933.8468761a372.25009177 372.25009177 0 0 1 0-744.50018353z m20.02433313 179.43151904h-39.93616901a6.69352725 6.69352725 0 0 0-6.69352725 6.69352725V604.2328627c0 2.19367667 1.01246621 4.1623613 2.75615881 5.39982038l137.41416897 100.23415877a6.63727863 6.63727863 0 0 0 9.28094102-1.4624511l23.79295651-32.39891977a6.5247822 6.5247822 0 0 0-1.51869891-9.22469321L539.44908511 581.17113175V375.52798666a6.69352725 6.69352725 0 0 0-6.63727862-6.69352726zM711.28710712 90.125a24.80542356 24.80542356 0 0 1-1e-8 49.61084629H314.23159262a24.80542356 24.80542356 0 0 1 0-49.61084629h397.1117623z" /></svg>

After

Width:  |  Height:  |  Size: 863 B

View File

@@ -0,0 +1,35 @@
import MagicTask from './service/magic-task.js'
import localZhCN from './i18n/zh-cn.js'
import localEn from './i18n/en.js'
import MagicTaskInfo from './components/magic-task-info.vue'
import 'vite-plugin-svg-icons/register'
export default (opt) => {
const i18n = opt.i18n
// 添加i18n 国际化信息
i18n.add('zh-cn', localZhCN)
i18n.add('en', localEn)
return {
// 左侧资源
resource: [{
// 资源类型,和后端存储结构一致
type: 'task',
// 展示图标
icon: '#magic-task-task', // #开头表示图标在插件中
// 展示名称
title: 'task.name',
// 运行服务
service: MagicTask(opt.bus, opt.constants, i18n.format, opt.Message, opt.request),
}],
// 底部工具条
toolbars: [{
// 当打开的资源类型为 task 时显示
type: 'task',
// 工具条展示的标题
title: 'task.title',
// 展示图标
icon: 'parameter',
// 对应的组件
component: MagicTaskInfo,
}]
}
}

View File

@@ -0,0 +1,41 @@
export default function (bus, constants, $i, Message, request) {
return {
// svg text
getIcon: item => 'task',
// 任务名称
name: $i('task.name'),
// 执行测试的逻辑
doTest: (opened) => {
opened.running = true
const info = opened.item
const requestConfig = {
baseURL: constants.SERVER_URL,
url: '/task/execute',
method: 'POST',
responseType: 'json',
headers: {},
withCredentials: true
}
bus.$emit(Message.SWITCH_TOOLBAR, 'log')
requestConfig.headers[constants.HEADER_REQUEST_CLIENT_ID] = constants.CLIENT_ID
requestConfig.headers[constants.HEADER_REQUEST_SCRIPT_ID] = opened.item.id
requestConfig.headers[constants.HEADER_MAGIC_TOKEN] = constants.HEADER_MAGIC_TOKEN_VALUE
// 设置断点
requestConfig.headers[constants.HEADER_REQUEST_BREAKPOINTS] = (opened.decorations || []).filter(it => it.options.linesDecorationsClassName === 'breakpoints').map(it => it.range.startLineNumber).join(',')
const fullName = opened.path()
bus.status(`开始测试定时任务${fullName}`)
request.sendPost('/task/execute', { id: info.id }, requestConfig).success(res => {
opened.running = false
}).end(() => {
bus.status(`定时任务${fullName}测试完毕`)
opened.running = false
})
},
// 是否允许执行测试
runnable: true,
// 是否需要填写路径
requirePath: true,
// 合并
merge: item => item
}
}

View File

@@ -0,0 +1,37 @@
import vue from '@vitejs/plugin-vue'
import viteSvgIcons from 'vite-plugin-svg-icons'
import path from 'path'
import pkg from './package.json'
export default {
base: './',
build: {
minify: false,
cssCodeSplit: true, // 将组件的 style 打包到 js 文件中
outDir: 'dist',
lib: {
target: 'esnext',
formats: ['iife'],
entry: path.resolve(__dirname, 'src/index.js'),
name: 'MagicTask',
fileName: (format) => `magic-task.${pkg.version}.${format}.js`
},
rollupOptions: {
// 确保外部化处理那些你不想打包进库的依赖
external: ['vue'],
output: {
// 在 UMD 构建模式下为这些外部化的依赖提供一个全局变量
globals: {
vue: 'Vue'
}
}
}
},
plugins: [
vue(),
viteSvgIcons({
iconDirs: [path.resolve(process.cwd(), 'src/icons')],
symbolId: 'magic-task-[name]'
}),
]
}

View File

@@ -0,0 +1,70 @@
package org.ssssssss.magicapi.task.model;
import org.ssssssss.magicapi.core.model.MagicEntity;
import org.ssssssss.magicapi.core.model.PathMagicEntity;
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

@@ -0,0 +1,27 @@
package org.ssssssss.magicapi.task.service;
import org.ssssssss.magicapi.core.service.AbstractPathMagicResourceStorage;
import org.ssssssss.magicapi.task.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,89 @@
package org.ssssssss.magicapi.task.service;
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.core.config.MagicConfiguration;
import org.ssssssss.magicapi.core.event.FileEvent;
import org.ssssssss.magicapi.core.event.GroupEvent;
import org.ssssssss.magicapi.core.service.AbstractMagicDynamicRegistry;
import org.ssssssss.magicapi.core.service.MagicResourceStorage;
import org.ssssssss.magicapi.task.model.TaskInfo;
import org.ssssssss.magicapi.utils.ScriptManager;
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
public boolean register(TaskInfo entity) {
unregister(entity);
return super.register(entity);
}
@Override
protected boolean register(MappingNode<TaskInfo> mappingNode) {
TaskInfo info = mappingNode.getEntity();
CronTask cronTask = new CronTask(() -> {
TaskInfo entity = mappingNode.getEntity();
String scriptName = MagicConfiguration.getMagicResourceService().getScriptName(entity);
if(entity.isEnabled()){
try {
logger.info("定时任务:[{}]开始执行", scriptName);
MagicScriptContext magicScriptContext = new MagicScriptContext();
magicScriptContext.setScriptName(scriptName);
ScriptManager.executeScript(entity.getScript(), magicScriptContext);
} catch (Exception e) {
logger.error("定时任务执行出错", e);
} finally {
logger.info("定时任务:[{}]执行完毕", scriptName);
}
}
}, info.getCron());
mappingNode.setMappingData(taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger()));
if(taskScheduler != null){
logger.debug("注册定时任务:[{},{}]", MagicConfiguration.getMagicResourceService().getScriptName(info), info.getCron());
} else {
logger.debug("注册定时任务失败:[{}, {}] 当前 TaskScheduler 为空", MagicConfiguration.getMagicResourceService().getScriptName(info), 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 e) {
String scriptName = MagicConfiguration.getMagicResourceService().getScriptName(info);
logger.warn("定时任务:[{}]取消失败", scriptName, e);
}
}
}
}

View File

@@ -0,0 +1,40 @@
package org.ssssssss.magicapi.task.starter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.ssssssss.magicapi.core.config.MagicPluginConfiguration;
import org.ssssssss.magicapi.core.model.Plugin;
import org.ssssssss.magicapi.core.web.MagicControllerRegister;
import org.ssssssss.magicapi.task.service.TaskInfoMagicResourceStorage;
import org.ssssssss.magicapi.task.service.TaskMagicDynamicRegistry;
import org.ssssssss.magicapi.task.web.MagicTaskController;
@Configuration
@EnableScheduling
public class MagicAPITaskConfiguration implements MagicPluginConfiguration{
@Bean
@ConditionalOnMissingBean
public TaskInfoMagicResourceStorage taskInfoMagicResourceStorage() {
return new TaskInfoMagicResourceStorage();
}
@Bean
@ConditionalOnMissingBean
public TaskMagicDynamicRegistry taskMagicDynamicRegistry(TaskInfoMagicResourceStorage taskInfoMagicResourceStorage, TaskScheduler taskScheduler) {
return new TaskMagicDynamicRegistry(taskInfoMagicResourceStorage, taskScheduler);
}
@Override
public Plugin plugin() {
return new Plugin("定时任务", "magic-task.1.0.0.iife.js");
}
@Override
public MagicControllerRegister controllerRegister() {
return (mapping, configuration) -> mapping.registerController(new MagicTaskController(configuration));
}
}

View File

@@ -0,0 +1,39 @@
package org.ssssssss.magicapi.task.web;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.ssssssss.magicapi.core.config.MagicConfiguration;
import org.ssssssss.magicapi.core.logging.MagicLoggerContext;
import org.ssssssss.magicapi.core.model.DebugRequest;
import org.ssssssss.magicapi.core.model.JsonBean;
import org.ssssssss.magicapi.core.model.MagicEntity;
import org.ssssssss.magicapi.core.web.MagicController;
import org.ssssssss.magicapi.core.web.MagicExceptionHandler;
import org.ssssssss.magicapi.utils.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);
MagicLoggerContext.SESSION.set(debugRequest.getRequestedClientId());
try {
MagicScriptDebugContext magicScriptContext = debugRequest.createMagicScriptContext(configuration.getDebugTimeout());
magicScriptContext.setScriptName(MagicConfiguration.getMagicResourceService().getScriptName(entity));
return new JsonBean<>(ScriptManager.executeScript(script, magicScriptContext));
} finally {
MagicLoggerContext.SESSION.remove();
}
}
}

View File

@@ -0,0 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.ssssssss.magicapi.task.starter.MagicAPITaskConfiguration