Merge branch '2.x' into dev

# Conflicts:
#	README.md
#	magic-api-spring-boot-starter/pom.xml
#	magic-api/pom.xml
#	magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicGroupController.java
#	magic-api/src/main/java/org/ssssssss/magicapi/model/JsonCodeConstants.java
#	magic-api/src/main/java/org/ssssssss/magicapi/provider/impl/MagicDatabaseBackupService.java
#	magic-editor/pom.xml
#	magic-editor/src/console/package.json
#	magic-editor/src/console/src/scripts/parsing/ast.js
#	magic-editor/src/console/src/scripts/parsing/parser.js
#	pom.xml
This commit is contained in:
mxd 2022-02-27 20:23:22 +08:00
commit 92864e9fd8
395 changed files with 8248 additions and 30839 deletions

View File

@ -52,7 +52,7 @@ magic-api 是一个基于Java的接口快速开发框架编写接口将通过
<dependency>
<groupId>org.ssssssss</groupId>
<artifactId>magic-api-spring-boot-starter</artifactId>
<version>1.7.5</version>
<version>2.0.0-beta.1</version>
</dependency>
```
## 修改application.properties

View File

@ -1,6 +1,6 @@
> 0.7.x版本之后仅需要一张表两个字段建表语句如下
```sql
CREATE TABLE `magic_api_file` (
CREATE TABLE `magic_api_file_v2` (
`file_path` varchar(512) NOT NULL,
`file_content` mediumtext,
PRIMARY KEY (`file_path`)
@ -8,14 +8,14 @@ CREATE TABLE `magic_api_file` (
```
### 备份表建表语句
```sql
CREATE TABLE `magic_api_backup` (
CREATE TABLE `magic_backup_record_v2` (
`id` varchar(32) NOT NULL COMMENT '原对象ID',
`create_date` bigint(13) NOT NULL COMMENT '备份时间',
`tag` varchar(32) DEFAULT NULL COMMENT '标签',
`type` varchar(32) DEFAULT NULL COMMENT '类型',
`name` varchar(64) DEFAULT NULL COMMENT '原名称',
`content` mediumtext COMMENT '备份内容',
`content` blob COMMENT '备份内容',
`create_by` varchar(64) DEFAULT NULL COMMENT '操作人',
PRIMARY KEY (`id`,`create_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
```
```

View File

@ -1,22 +0,0 @@
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
ALTER TABLE `magic_api_info` ADD COLUMN `api_group_prefix` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分组前缀' AFTER `api_group_name`;
ALTER TABLE `magic_api_info` ADD COLUMN `api_output` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '输出结果' AFTER `api_group_prefix`;
-- ----------------------------
-- Table structure for magic_api_info_his
-- ----------------------------
CREATE TABLE `magic_api_info_his` (
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'api_id',
`api_method` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请求方法',
`api_path` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '请求路径',
`api_script` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '接口脚本',
`api_parameter` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '接口参数',
`api_option` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '接口选项',
`api_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '接口名称',
`api_group_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '接口分组',
`api_group_prefix` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '分组前缀',
`api_output` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '输出结果',
`api_create_time` bigint(20) NULL DEFAULT NULL COMMENT '创建时间',
`api_update_time` bigint(20) NULL DEFAULT NULL COMMENT '修改时间'
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'MagicAPI接口历史记录' ROW_FORMAT = Dynamic;

View File

@ -1,29 +0,0 @@
-- 创建分组表
CREATE TABLE `magic_group` (
`id` varchar(32) NOT NULL,
`group_name` varchar(64) NULL COMMENT '组名',
`group_type` varchar(1) NULL COMMENT '组类型1接口分组2函数分组',
`group_path` varchar(64) NULL COMMENT '分组路径',
`parent_id` varchar(32) NULL COMMENT '父级ID',
`deleted` char(1) NULL DEFAULT 0 COMMENT '是否被删除10',
PRIMARY KEY (`id`)
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'MagicAPI分组信息表' ROW_FORMAT = Dynamic;
-- 插入分组数据
insert into magic_group select md5(uuid()),api_group_name,'1',api_group_prefix,'0','0' from magic_api_info group by api_group_name,api_group_prefix;
-- 修改字段
ALTER TABLE `magic_api_info` ADD COLUMN `api_group_id` varchar(32) NULL COMMENT '分组ID' AFTER `api_name`;
ALTER TABLE `magic_api_info` CHANGE COLUMN `api_output` `api_response_body` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '输出结果' AFTER `api_group_id`;
ALTER TABLE `magic_api_info` ADD COLUMN `api_description` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '接口描述' AFTER `api_response_body`;
ALTER TABLE `magic_api_info` ADD COLUMN `api_request_body` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '请求体' AFTER `api_group_id`, ADD COLUMN `api_request_header` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '请求Header' AFTER `api_request_body`;
ALTER TABLE `magic_api_info_his` ADD COLUMN `api_group_id` varchar(32) NULL COMMENT '分组ID' AFTER `api_name`;
ALTER TABLE `magic_api_info_his` CHANGE COLUMN `api_output` `api_response_body` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '输出结果' AFTER `api_group_id`;
ALTER TABLE `magic_api_info_his` ADD COLUMN `api_description` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '接口描述' AFTER `api_response_body`;
ALTER TABLE `magic_api_info_his` ADD COLUMN `api_request_body` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '请求体' AFTER `api_group_id`, ADD COLUMN `api_request_header` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '请求Header' AFTER `api_request_body`;
-- 赋值api_group_id字段
UPDATE magic_api_info mai JOIN magic_group mg ON mg.group_name = mai.api_group_name AND mg.group_path = mai.api_group_prefix SET mai.api_group_id = mg.id;
-- 对关联不上的,归根节点
UPDATE magic_api_info SET api_group_id = '0' where api_group_id IS NULL;
-- 删除字段
ALTER TABLE `magic_api_info` DROP COLUMN `api_group_name`,DROP COLUMN `api_group_prefix`;
ALTER TABLE `magic_api_info_his` DROP COLUMN `api_group_name`,DROP COLUMN `api_group_prefix`;

View File

@ -1,34 +0,0 @@
CREATE TABLE `magic_function`
(
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键',
`function_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '函数名称',
`function_path` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '函数路径',
`function_parameter` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '参数列表',
`function_return_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '返回值类型',
`function_script` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '脚本',
`function_group_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '所属分组',
`function_description` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '函数描述',
`function_create_time` bigint(20) NULL DEFAULT NULL COMMENT '创建时间',
`function_update_time` bigint(20) NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB
CHARACTER SET = utf8mb4
COLLATE = utf8mb4_general_ci COMMENT = 'MagicAPI 函数表'
ROW_FORMAT = Dynamic;
CREATE TABLE `magic_function_his`
(
`id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'function_id',
`function_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '函数名称',
`function_path` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '函数路径',
`function_parameter` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '参数列表',
`function_return_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '返回值类型',
`function_script` mediumtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '脚本',
`function_group_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '所属分组',
`function_description` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '函数描述',
`function_create_time` bigint(20) NULL DEFAULT NULL COMMENT '创建时间',
`function_update_time` bigint(20) NULL DEFAULT NULL COMMENT '修改时间'
) ENGINE = InnoDB
CHARACTER SET = utf8mb4
COLLATE = utf8mb4_general_ci COMMENT = 'MagicAPI 函数历史记录'
ROW_FORMAT = Dynamic;

View File

@ -1,49 +0,0 @@
import 'org.ssssssss.magicapi.adapter.Resource' as root;
import 'org.ssssssss.magicapi.provider.GroupServiceProvider';
import 'org.ssssssss.magicapi.provider.ApiServiceProvider';
import 'org.ssssssss.magicapi.provider.FunctionServiceProvider';
import 'org.ssssssss.magicapi.utils.IoUtils' as IoUtils
import 'org.ssssssss.magicapi.utils.JsonUtils' as JsonUtils
import 'java.io.File' as File;
var ds = db.camel(); //如果之前保存在其他库这里可以修改为db.xxx.camel();
var apiSql = """ select * from magic_api_info """;
var groupSql = """ select * from magic_group where deleted = '0' """;
var functionSql = """ select * from magic_function """;
// 替换key去除前缀将首字母小写。
var replaceKey = (it,src) => it.replaceKey(src,'').replaceKey(it => it.substring(0,1).toLowerCase() + it.substring(1));
// list转tree
var toTree = (list,parentId)=>list.filter(it => it.parentId == parentId).each(it => it.children = toTree(list,it.id))
// 查询分组列表
var groupList = ds.select(groupSql).map(it => replaceKey(it,"group"));
// 将接口分组转为tree
var apiTree = toTree(groupList.filter(it => it.type == '1'),'0');
// 将函数分组转为tree
var functionTree = toTree(groupList.filter(it => it.type == '2'),'0');
// 记录分组所在路径
var groups = {};
// 处理分组
var processGroup = (parent,list)=>{
if(!parent.exists()){
parent.mkdir();
}
list.each(it => {
var resource = parent.getResource(it.name);
resource.mkdir();
groups[it.id] = resource;
// 防止序列化children
var children = it.remove('children');
resource.getResource('group.json').write(JsonUtils.toJsonString(it))
if(children){
processGroup(resource,children);
}
});
}
// 处理接口分组
processGroup(root.getResource("api"),apiTree);
// 处理函数分组
processGroup(root.getResource("function"),functionTree);
// 处理接口
ds.select(apiSql).map(it => replaceKey(it,'api')).each(it => groups[it.groupId].getResource(it.name + '.ms').write(ApiServiceProvider.serialize(it)));
// 处理函数
ds.select(functionSql).map(it => replaceKey(it,'function')).each(it => groups[it.groupId].getResource(it.name + '.ms').write(FunctionServiceProvider.serialize(it)));
return 'ok';

View File

@ -0,0 +1,27 @@
<?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-beta.1</version>
</parent>
<artifactId>magic-api-plugin-cluster</artifactId>
<packaging>jar</packaging>
<name>magic-api-plugin-cluster</name>
<description>magic-api-plugin-cluster</description>
<dependencies>
<dependency>
<groupId>org.ssssssss</groupId>
<artifactId>magic-api-plugin-redis</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,28 @@
package org.ssssssss.magicapi.cluster;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.UUID;
/**
* 集群配置
*
* @author mxd
* @since 1.2.0
*/
@ConfigurationProperties(prefix = "magic-api.cluster")
public class ClusterConfig {
/**
* redis 通道
*/
private String channel = "magic-api:notify:channel";
public String getChannel() {
return channel;
}
public void setChannel(String channel) {
this.channel = channel;
}
}

View File

@ -1,66 +1,50 @@
package org.ssssssss.magicapi.spring.boot.starter;
package org.ssssssss.magicapi.cluster;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.ssssssss.magicapi.adapter.Resource;
import org.ssssssss.magicapi.adapter.resource.RedisResource;
import org.ssssssss.magicapi.model.MagicNotify;
import org.ssssssss.magicapi.modules.RedisModule;
import org.ssssssss.magicapi.provider.MagicAPIService;
import org.ssssssss.magicapi.provider.MagicNotifyService;
import org.ssssssss.magicapi.core.config.MagicAPIProperties;
import org.ssssssss.magicapi.core.config.MagicPluginConfiguration;
import org.ssssssss.magicapi.core.model.MagicNotify;
import org.ssssssss.magicapi.core.model.Plugin;
import org.ssssssss.magicapi.core.service.MagicAPIService;
import org.ssssssss.magicapi.core.service.MagicNotifyService;
import org.ssssssss.magicapi.utils.JsonUtils;
import java.util.Objects;
/**
* redis配置
*
* @author mxd
*/
@ConditionalOnClass(RedisConnectionFactory.class)
@Configuration
@AutoConfigureBefore(MagicAPIAutoConfiguration.class)
public class MagicRedisAutoConfiguration {
private final static Logger logger = LoggerFactory.getLogger(MagicRedisAutoConfiguration.class);
@EnableConfigurationProperties(ClusterConfig.class)
@Configuration
public class MagicClusterConfiguration implements MagicPluginConfiguration {
private final ClusterConfig config;
private final MagicAPIProperties properties;
private final StringRedisTemplate stringRedisTemplate;
public MagicRedisAutoConfiguration(MagicAPIProperties properties, ObjectProvider<StringRedisTemplate> stringRedisTemplateProvider) {
private final Logger logger = LoggerFactory.getLogger(MagicClusterConfiguration.class);
public MagicClusterConfiguration(MagicAPIProperties properties, ClusterConfig config, ObjectProvider<StringRedisTemplate> stringRedisTemplateProvider) {
this.properties = properties;
this.config = config;
this.stringRedisTemplate = stringRedisTemplateProvider.getIfAvailable();
}
/**
* 注入redis模块
*/
@Bean
public RedisModule redisFunctions(RedisConnectionFactory connectionFactory) {
return new RedisModule(connectionFactory);
}
/**
* 使用Redis存储
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "magic-api", name = "resource.type", havingValue = "redis")
public Resource magicRedisResource(RedisConnectionFactory connectionFactory) {
ResourceConfig resource = properties.getResource();
return new RedisResource(new StringRedisTemplate(connectionFactory), resource.getPrefix(), resource.isReadonly());
@Override
public Plugin plugin() {
return new Plugin("Cluster");
}
/**
@ -68,18 +52,24 @@ public class MagicRedisAutoConfiguration {
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "magic-api", name = "cluster-config.enable", havingValue = "true")
public MagicNotifyService magicNotifyService() {
return magicNotify -> stringRedisTemplate.convertAndSend(properties.getClusterConfig().getChannel(), Objects.requireNonNull(JsonUtils.toJsonString(magicNotify)));
return magicNotify -> stringRedisTemplate.convertAndSend(config.getChannel(), Objects.requireNonNull(JsonUtils.toJsonString(magicNotify)));
}
/**
* 消息处理服务
*/
@Bean
@ConditionalOnMissingBean
public MagicSynchronizationService magicSynchronizationService(MagicNotifyService magicNotifyService) {
return new MagicSynchronizationService(magicNotifyService, properties.getInstanceId());
}
/**
* 集群通知监听
*/
@Bean
@ConditionalOnProperty(prefix = "magic-api", name = "cluster-config.enable", havingValue = "true")
public RedisMessageListenerContainer magicRedisMessageListenerContainer(RedisConnectionFactory redisConnectionFactory, MagicAPIService magicAPIService) {
ClusterConfig config = properties.getClusterConfig();
logger.info("开启集群通知监听, Redis channel: {}", config.getChannel());
RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer();
redisMessageListenerContainer.setConnectionFactory(redisConnectionFactory);

View File

@ -0,0 +1,54 @@
package org.ssssssss.magicapi.cluster;
import org.springframework.context.event.EventListener;
import org.ssssssss.magicapi.core.event.*;
import org.ssssssss.magicapi.core.config.Constants;
import org.ssssssss.magicapi.core.model.MagicNotify;
import org.ssssssss.magicapi.core.service.MagicNotifyService;
public class MagicSynchronizationService {
private final MagicNotifyService magicNotifyService;
/**
* 当前实例ID
*/
private final String instanceId;
public MagicSynchronizationService(MagicNotifyService magicNotifyService, String instanceId) {
this.magicNotifyService = magicNotifyService;
this.instanceId = instanceId;
}
@EventListener(condition = "#event.source != T(org.ssssssss.magicapi.core.config.Constants).EVENT_SOURCE_NOTIFY")
public void onFolderEvent(GroupEvent event) {
switch (event.getAction()) {
case CREATE:
case SAVE:
case MOVE:
case DELETE:
magicNotifyService.sendNotify(new MagicNotify(instanceId, event.getGroup().getId(), event.getAction(), event.getType()));
break;
}
}
@EventListener(condition = "#event.source != T(org.ssssssss.magicapi.core.config.Constants).EVENT_SOURCE_NOTIFY")
public void onFileEvent(FileEvent event) {
if (Constants.EVENT_SOURCE_NOTIFY.equals(event.getSource())) {
return;
}
switch (event.getAction()) {
case CREATE:
case SAVE:
case MOVE:
case DELETE:
magicNotifyService.sendNotify(new MagicNotify(instanceId, event.getEntity().getId(), event.getAction(), Constants.EVENT_TYPE_FILE));
break;
}
}
@EventListener(condition = "#event.action == T(org.ssssssss.magicapi.core.event.EventAction).CLEAR && #event.source != T(org.ssssssss.magicapi.core.config.Constants).EVENT_SOURCE_NOTIFY")
public void onClearEvent(MagicEvent event){
magicNotifyService.sendNotify(new MagicNotify(instanceId, null, event.getAction(), null));
}
}

View File

@ -0,0 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.ssssssss.magicapi.cluster.MagicClusterConfiguration

View File

@ -0,0 +1,21 @@
<?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-beta.1</version>
</parent>
<artifactId>magic-api-plugin-elasticsearch</artifactId>
<packaging>jar</packaging>
<name>magic-api-plugin-elasticsearch</name>
<description>magic-api-plugin-elasticsearch</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,49 @@
package org.ssssssss.magicapi.elasticsearch;
import org.elasticsearch.client.RestClient;
import java.io.IOException;
import java.util.Map;
public class ElasticSearchConnection extends ElasticSearchRest {
public ElasticSearchConnection(RestClient restClient, String endpoint) {
super(restClient);
super.endpoint(endpoint);
}
public ElasticSearchConnection parameter(String key, String value) {
if (value != null) {
parameters.put(key, value);
}
return this;
}
public ElasticSearchConnection parameters(Map<String, String> params) {
if (params != null) {
parameters.putAll(params);
}
return this;
}
public Object put(Object data) throws IOException {
return processResponse(json(data).doPut());
}
public Object delete() throws IOException {
return processResponse(doDelete());
}
public Object delete(Object data) throws IOException {
return processResponse(json(data).doDelete());
}
public Object post(Object data) throws IOException {
return processResponse(json(data).doPost());
}
public Object get() throws IOException {
return processResponse(doGet());
}
}

View File

@ -0,0 +1,76 @@
package org.ssssssss.magicapi.elasticsearch;
import org.elasticsearch.client.RestClient;
import org.ssssssss.magicapi.utils.JsonUtils;
import org.ssssssss.script.annotation.Comment;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class ElasticSearchIndex {
private final RestClient restClient;
private final String name;
private final String type;
public ElasticSearchIndex(RestClient restClient, String name, String type) {
this.restClient = restClient;
this.name = name;
this.type = type;
}
@Comment("根据`_id`保存,当存在时更新,不存在时插入")
public Object save(@Comment(value = "_id", name = "_id")String _id, @Comment(value = "保存对象", name = "data")Object data) throws IOException {
return connect("/%s/%s/%s", this.name, this.type, _id).post(data);
}
@Comment("不指定`_id`插入")
public Object insert(@Comment(value = "插入对象", name = "data")Object data) throws IOException {
return connect("/%s/%s", this.name, this.type).post(data);
}
@Comment("指定`_id`插入,当`_id`存在时不会更新")
public Object insert(@Comment(value = "_id", name = "_id")String _id, @Comment(value = "插入对象", name = "data")Object data) throws IOException {
return connect("/%s/%s/%s/_create", this.name, this.type, _id).post(data);
}
@Comment("根据`id`删除")
public Object delete(@Comment(value = "id", name = "id")String id) throws IOException {
return connect("/%s/%s/%s", this.name, this.type, id).delete();
}
@Comment("批量保存,当包含`id`时,则使用该列值匹配保存")
public Object bulkSave(@Comment(value = "保存内容", name = "list") List<Map<String, Object>> list) throws IOException {
StringBuilder builder = new StringBuilder();
list.forEach(item -> {
Object id = item.get("id");
if(id != null){
builder.append(String.format("{ \"index\":{ \"_id\": \"%s\" } }\r\n", id));
} else {
builder.append("{ \"index\":{} }\r\n");
}
builder.append(JsonUtils.toJsonStringWithoutPretty(item));
builder.append("\r\n");
});
return connect("/%s/%s/_bulk", this.name, this.type).post(builder.toString());
}
@Comment("根据`_id`修改")
public Object update(@Comment(value = "_id", name = "_id")String _id, @Comment(value = "修改项", name = "data")Object data) throws IOException {
return connect("/%s/%s/%s", this.name, this.type, _id).post(Collections.singletonMap("doc", data));
}
@Comment("搜索")
public Object search(@Comment(value = "搜索`DSL`语句", name = "dsl")Map<String, Object> dsl) throws IOException {
return connect("/%s/_search", this.name).post(dsl);
}
private ElasticSearchConnection connect(String format, Object... args) {
return new ElasticSearchConnection(this.restClient, String.format(format, args));
}
}

View File

@ -0,0 +1,26 @@
package org.ssssssss.magicapi.elasticsearch;
import org.elasticsearch.client.RestClient;
import org.ssssssss.magicapi.core.annotation.MagicModule;
import org.ssssssss.script.annotation.Comment;
@MagicModule("elasticsearch")
public class ElasticSearchModule {
private static final String DOC = "_doc";
private final RestClient restClient;
public ElasticSearchModule(RestClient restClient) {
this.restClient = restClient;
}
@Comment(value = "ElasticSearch REST API")
public ElasticSearchConnection rest(String url){
return new ElasticSearchConnection(this.restClient, url);
}
public ElasticSearchIndex index(String indexName){
return new ElasticSearchIndex(this.restClient, indexName, DOC);
}
}

View File

@ -0,0 +1,94 @@
package org.ssssssss.magicapi.elasticsearch;
import org.apache.http.HttpEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.nio.entity.NStringEntity;
import org.apache.http.util.EntityUtils;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.RestClient;
import org.ssssssss.magicapi.utils.JsonUtils;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public class ElasticSearchRest {
private final RestClient restClient;
private String method;
private String endpoint = "/";
private HttpEntity entity;
protected final Map<String, String> parameters = new HashMap<>();
public ElasticSearchRest(RestClient restClient) {
this.restClient = restClient;
}
ElasticSearchRest endpoint(String endpoint){
this.endpoint = endpoint;
return this;
}
Response doGet() throws IOException {
this.method = "GET";
return execute();
}
Response doPost() throws IOException {
this.method = "POST";
return execute();
}
Response doDelete() throws IOException {
this.method = "DELETE";
return execute();
}
Response doPut() throws IOException {
this.method = "PUT";
return execute();
}
ElasticSearchRest json(Object data){
if(data == null){
return this;
}
String json = null;
if(data instanceof CharSequence){
json = data.toString();
} else {
json = JsonUtils.toJsonString(data);
}
if(json != null){
this.entity = new NStringEntity(json, ContentType.APPLICATION_JSON);
}
return this;
}
private Response execute() throws IOException {
Request request = new Request(method, this.endpoint);
request.addParameters(parameters);
request.setEntity(entity);
return this.restClient.performRequest(request);
}
Object processResponse(Response response) throws IOException {
int code = response.getStatusLine().getStatusCode();
if (code >= 200 && code < 300) { // 2xx
HttpEntity entity = response.getEntity();
String resp = EntityUtils.toString(entity, StandardCharsets.UTF_8);
ContentType contentType = ContentType.get(entity);
if (Objects.equals(ContentType.APPLICATION_JSON.getMimeType(), contentType.getMimeType())) {
return JsonUtils.readValue(resp, Object.class);
}
}
return response;
}
}

View File

@ -0,0 +1,23 @@
package org.ssssssss.magicapi.elasticsearch;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.ssssssss.magicapi.core.config.MagicPluginConfiguration;
import org.ssssssss.magicapi.core.model.Plugin;
@Configuration
public class MagicElasticSearchConfiguration implements MagicPluginConfiguration {
@Override
public Plugin plugin() {
return new Plugin("ElasticSearch");
}
@Bean
@ConditionalOnBean(RestHighLevelClient.class)
public ElasticSearchModule elasticSearchModule(RestHighLevelClient restHighLevelClient){
return new ElasticSearchModule(restHighLevelClient.getLowLevelClient());
}
}

View File

@ -0,0 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.ssssssss.magicapi.elasticsearch.MagicElasticSearchConfiguration

View File

@ -0,0 +1,21 @@
<?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-beta.1</version>
</parent>
<artifactId>magic-api-plugin-mongo</artifactId>
<packaging>jar</packaging>
<name>magic-api-plugin-mongo</name>
<description>magic-api-plugin-mongo</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -1,31 +1,28 @@
package org.ssssssss.magicapi.spring.boot.starter;
package org.ssssssss.magicapi.mongo;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.ssssssss.magicapi.modules.MongoCollectionExtension;
import org.ssssssss.magicapi.modules.MongoFindIterableExtension;
import org.ssssssss.magicapi.modules.MongoModule;
import org.ssssssss.magicapi.core.config.MagicPluginConfiguration;
import org.ssssssss.magicapi.core.model.Plugin;
import org.ssssssss.script.reflection.JavaReflection;
/**
* mongo配置
*
* @author mxd
*/
@Configuration
@ConditionalOnBean(MongoTemplate.class)
@AutoConfigureBefore(MagicAPIAutoConfiguration.class)
public class MagicMongoAutoConfiguration {
public class MagicMongoConfiguration implements MagicPluginConfiguration {
@Override
public Plugin plugin() {
return new Plugin("Mongo");
}
/**
* 注入mongo模块
*/
@Bean
@ConditionalOnMissingBean
public MongoModule mongoFunctions(MongoTemplate mongoTemplate) {
JavaReflection.registerMethodExtension(MongoCollection.class, new MongoCollectionExtension());
JavaReflection.registerMethodExtension(FindIterable.class, new MongoFindIterableExtension());

View File

@ -1,4 +1,4 @@
package org.ssssssss.magicapi.modules;
package org.ssssssss.magicapi.mongo;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCollection;

View File

@ -1,4 +1,4 @@
package org.ssssssss.magicapi.modules;
package org.ssssssss.magicapi.mongo;
import com.mongodb.client.FindIterable;
import com.mongodb.client.MongoCursor;

View File

@ -1,4 +1,4 @@
package org.ssssssss.magicapi.modules;
package org.ssssssss.magicapi.mongo;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
@ -8,15 +8,16 @@ import org.bson.conversions.Bson;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.ssssssss.magicapi.config.MagicModule;
import org.ssssssss.magicapi.model.Constants;
import org.ssssssss.magicapi.core.config.Constants;
import org.ssssssss.magicapi.core.annotation.MagicModule;
import org.ssssssss.script.convert.ClassImplicitConvert;
import org.ssssssss.script.functions.DynamicAttribute;
import org.ssssssss.script.reflection.JavaInvoker;
import org.ssssssss.script.reflection.JavaReflection;
import org.ssssssss.script.runtime.Variables;
import java.beans.Transient;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
@ -24,7 +25,8 @@ import java.util.Map;
*
* @author mxd
*/
public class MongoModule extends HashMap<String, MongoModule.MongoDataBaseGetter> implements MagicModule, ClassImplicitConvert {
@MagicModule("mongo")
public class MongoModule implements ClassImplicitConvert, DynamicAttribute<MongoModule.MongoDataBaseGetter, MongoModule.MongoDataBaseGetter> {
private static final Logger logger = LoggerFactory.getLogger(MongoModule.class);
@ -54,23 +56,19 @@ public class MongoModule extends HashMap<String, MongoModule.MongoDataBaseGetter
}
@Override
public MongoDataBaseGetter get(Object databaseName) {
@Transient
public MongoDataBaseGetter getDynamicAttribute(String databaseName) {
try {
if (databaseName == null) {
return null;
}
MongoDatabase database = (MongoDatabase) invoker.invoke0(factory, null, new Object[]{databaseName.toString()});
MongoDatabase database = (MongoDatabase) invoker.invoke0(factory, null, new Object[]{databaseName});
return new MongoDataBaseGetter(database);
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
@Override
public String getModuleName() {
return "mongo";
}
@Override
public boolean support(Class<?> from, Class<?> to) {
return Map.class.isAssignableFrom(from) && (Bson.class.isAssignableFrom(to));
@ -81,7 +79,7 @@ public class MongoModule extends HashMap<String, MongoModule.MongoDataBaseGetter
return new Document((Map<String, Object>) source);
}
public static class MongoDataBaseGetter extends HashMap<String, MongoCollection<Document>> {
public static class MongoDataBaseGetter implements DynamicAttribute<MongoCollection<Document>, MongoCollection<Document>> {
MongoDatabase database;
@ -90,8 +88,9 @@ public class MongoModule extends HashMap<String, MongoModule.MongoDataBaseGetter
}
@Override
public MongoCollection<Document> get(Object key) {
return database.getCollection(key.toString());
@Transient
public MongoCollection<Document> getDynamicAttribute(String key) {
return database.getCollection(key);
}
}
}

View File

@ -0,0 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.ssssssss.magicapi.mongo.MagicMongoConfiguration

View File

@ -0,0 +1,21 @@
<?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-beta.1</version>
</parent>
<artifactId>magic-api-plugin-redis</artifactId>
<packaging>jar</packaging>
<name>magic-api-plugin-redis</name>
<description>magic-api-plugin-redis</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,46 @@
package org.ssssssss.magicapi.redis;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.ssssssss.magicapi.core.config.MagicAPIProperties;
import org.ssssssss.magicapi.core.config.MagicPluginConfiguration;
import org.ssssssss.magicapi.core.config.Resource;
import org.ssssssss.magicapi.core.model.Plugin;
@Configuration
public class MagicRedisConfiguration implements MagicPluginConfiguration {
private final MagicAPIProperties properties;
public MagicRedisConfiguration(MagicAPIProperties properties) {
this.properties = properties;
}
/**
* 使用Redis存储
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "magic-api", name = "resource.type", havingValue = "redis")
public org.ssssssss.magicapi.core.resource.Resource magicRedisResource(RedisConnectionFactory connectionFactory) {
Resource resource = properties.getResource();
return new RedisResource(new StringRedisTemplate(connectionFactory), resource.getPrefix(), resource.isReadonly());
}
/**
* 注入redis模块
*/
@Bean
public RedisModule redisFunctions(RedisConnectionFactory connectionFactory) {
return new RedisModule(connectionFactory);
}
@Override
public Plugin plugin() {
return new Plugin("Redis");
}
}

View File

@ -1,20 +1,20 @@
package org.ssssssss.magicapi.modules;
package org.ssssssss.magicapi.redis;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.ssssssss.magicapi.config.MagicModule;
import org.ssssssss.magicapi.core.annotation.MagicModule;
import org.ssssssss.script.functions.DynamicMethod;
import java.util.ArrayList;
import java.util.List;
import java.util.*;
/**
* redis模块
*
* @author mxd
*/
public class RedisModule implements MagicModule, DynamicMethod {
@MagicModule("redis")
public class RedisModule implements DynamicMethod {
private final StringRedisTemplate redisTemplate;
@ -22,11 +22,6 @@ public class RedisModule implements MagicModule, DynamicMethod {
this.redisTemplate = new StringRedisTemplate(connectionFactory);
}
@Override
public String getModuleName() {
return "redis";
}
/**
* 序列化
*/
@ -40,13 +35,13 @@ public class RedisModule implements MagicModule, DynamicMethod {
/**
* 反序列化
*/
@SuppressWarnings("unchecked")
private Object deserialize(Object value) {
if (value != null) {
if (value instanceof byte[]) {
return this.redisTemplate.getStringSerializer().deserialize((byte[]) value);
}
if (value instanceof List) {
@SuppressWarnings("unchecked")
List<Object> valueList = (List<Object>) value;
List<Object> resultList = new ArrayList<>(valueList.size());
for (Object val : valueList) {
@ -54,6 +49,12 @@ public class RedisModule implements MagicModule, DynamicMethod {
}
return resultList;
}
if(value instanceof Map){
Map<Object, Object> map = (Map<Object, Object>) value;
LinkedHashMap<Object, Object> newMap = new LinkedHashMap<>(map.size());
map.forEach((key, val) -> newMap.put(deserialize(key), deserialize(val)));
return newMap;
}
}
return value;
}

View File

@ -1,4 +1,4 @@
package org.ssssssss.magicapi.adapter.resource;
package org.ssssssss.magicapi.redis;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -6,7 +6,8 @@ import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.ssssssss.magicapi.adapter.Resource;
import org.ssssssss.magicapi.core.resource.KeyValueResource;
import org.ssssssss.magicapi.core.resource.Resource;
import java.nio.charset.StandardCharsets;
import java.util.*;

View File

@ -0,0 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.ssssssss.magicapi.redis.MagicRedisConfiguration

View File

@ -0,0 +1,31 @@
<?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-beta.1</version>
</parent>
<artifactId>magic-api-plugin-swagger</artifactId>
<packaging>jar</packaging>
<name>magic-api-plugin-swagger</name>
<description>magic-api-plugin-swagger</description>
<properties>
<swagger.version>2.9.2</swagger.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -1,4 +1,4 @@
package org.ssssssss.magicapi.spring.boot.starter;
package org.ssssssss.magicapi.swagger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
@ -11,10 +11,13 @@ import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Primary;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.ssssssss.magicapi.config.MappingHandlerMapping;
import org.ssssssss.magicapi.provider.GroupServiceProvider;
import org.ssssssss.magicapi.swagger.SwaggerEntity;
import org.ssssssss.magicapi.swagger.SwaggerProvider;
import org.ssssssss.magicapi.core.config.MagicAPIProperties;
import org.ssssssss.magicapi.core.config.MagicPluginConfiguration;
import org.ssssssss.magicapi.core.model.Plugin;
import org.ssssssss.magicapi.core.service.MagicResourceService;
import org.ssssssss.magicapi.core.service.impl.RequestMagicDynamicRegistry;
import org.ssssssss.magicapi.swagger.entity.SwaggerEntity;
import org.ssssssss.magicapi.swagger.entity.SwaggerProvider;
import org.ssssssss.magicapi.utils.Mapping;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
@ -24,44 +27,39 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Swagger配置类
*
* @author mxd
*/
@Configuration
@AutoConfigureAfter({MagicAPIAutoConfiguration.class})
@EnableConfigurationProperties(MagicAPIProperties.class)
@EnableConfigurationProperties(SwaggerConfig.class)
@ConditionalOnClass(name = "springfox.documentation.swagger.web.SwaggerResourcesProvider")
public class MagicSwaggerConfiguration {
public class MagicSwaggerConfiguration implements MagicPluginConfiguration {
private final MagicAPIProperties properties;
private final SwaggerConfig swaggerConfig;
private final ApplicationContext applicationContext;
@Autowired
@Lazy
private RequestMappingHandlerMapping requestMappingHandlerMapping;
public MagicSwaggerConfiguration(MagicAPIProperties properties, ApplicationContext applicationContext) {
public MagicSwaggerConfiguration(MagicAPIProperties properties, SwaggerConfig swaggerConfig, ApplicationContext applicationContext) {
this.properties = properties;
this.swaggerConfig = swaggerConfig;
this.applicationContext = applicationContext;
}
@Override
public Plugin plugin() {
return new Plugin("Swagger");
}
@Bean
@Primary
public SwaggerResourcesProvider magicSwaggerResourcesProvider(MappingHandlerMapping handlerMapping, GroupServiceProvider groupServiceProvider, ServletContext servletContext) throws NoSuchMethodException {
SwaggerConfig config = properties.getSwaggerConfig();
public SwaggerResourcesProvider magicSwaggerResourcesProvider(RequestMagicDynamicRegistry requestMagicDynamicRegistry, MagicResourceService magicResourceService, ServletContext servletContext) throws NoSuchMethodException {
Mapping mapping = Mapping.create(requestMappingHandlerMapping);
RequestMappingInfo requestMappingInfo = mapping.paths(config.getLocation()).build();
// 构建文档信息
SwaggerProvider swaggerProvider = new SwaggerProvider();
swaggerProvider.setGroupServiceProvider(groupServiceProvider);
swaggerProvider.setMappingHandlerMapping(handlerMapping);
swaggerProvider.setPersistenceResponseBody(properties.isPersistenceResponseBody());
RequestMappingInfo requestMappingInfo = mapping.paths(swaggerConfig.getLocation()).build();
SwaggerEntity.License license = new SwaggerEntity.License("MIT", "https://gitee.com/ssssssss-team/magic-api/blob/master/LICENSE");
swaggerProvider.setInfo(new SwaggerEntity.Info(config.getDescription(), config.getVersion(), config.getTitle(), license, config.getConcat()));
swaggerProvider.setBasePath(servletContext.getContextPath());
SwaggerEntity.Info info = new SwaggerEntity.Info(swaggerConfig.getDescription(), swaggerConfig.getVersion(), swaggerConfig.getTitle(), license, swaggerConfig.getConcat());
// 构建文档信息
SwaggerProvider swaggerProvider = new SwaggerProvider(requestMagicDynamicRegistry, magicResourceService, servletContext.getContextPath(), info, properties.isPersistenceResponseBody());
// 注册swagger.json
@ -70,7 +68,7 @@ public class MagicSwaggerConfiguration {
return () -> {
List<SwaggerResource> resources = new ArrayList<>();
// 追加Magic Swagger信息
resources.add(swaggerResource(config.getName(), config.getLocation()));
resources.add(swaggerResource(swaggerConfig.getName(), swaggerConfig.getLocation()));
Map<String, SwaggerResourcesProvider> beans = applicationContext.getBeansOfType(SwaggerResourcesProvider.class);
// 获取已定义的文档信息
for (Map.Entry<String, SwaggerResourcesProvider> entry : beans.entrySet()) {

View File

@ -1,13 +1,16 @@
package org.ssssssss.magicapi.spring.boot.starter;
package org.ssssssss.magicapi.swagger;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.ssssssss.magicapi.swagger.SwaggerEntity;
import org.ssssssss.magicapi.swagger.entity.SwaggerEntity;
/**
* Swagger 配置
*
* @author mxd
*/
@ConfigurationProperties(prefix = "magic-api.swagger")
public class SwaggerConfig {
/**

View File

@ -1,4 +1,4 @@
package org.ssssssss.magicapi.swagger;
package org.ssssssss.magicapi.swagger.entity;
import java.util.*;
@ -17,17 +17,17 @@ public class SwaggerEntity {
private Info info;
private Set<Tag> tags = new TreeSet<>(Comparator.comparing(Tag::getName));
private final Set<Tag> tags = new TreeSet<>(Comparator.comparing(Tag::getName));
private Map<String, Object> definitions = new HashMap<>();
private final Map<String, Object> definitions = new HashMap<>();
private Map<String, Map<String, Path>> paths = new HashMap<>();
private final Map<String, Map<String, Path>> paths = new HashMap<>();
private static Map<String, Object> doProcessSchema(Object target) {
Map<String, Object> result = new HashMap<>(3);
result.put("type", getType(target));
if (target instanceof List) {
List targetList = (List) target;
List<?> targetList = (List<?>) target;
if (targetList.size() > 0) {
result.put("items", doProcessSchema(targetList.get(0)));
} else {
@ -244,7 +244,7 @@ public class SwaggerEntity {
private String description;
private String operationId;
private final String operationId;
private List<String> produces = new ArrayList<>();
@ -343,7 +343,6 @@ public class SwaggerEntity {
}
}
@Deprecated
public static class Parameter {
private String name;
@ -367,9 +366,6 @@ public class SwaggerEntity {
this.description = description;
this.required = required;
if ("body".equalsIgnoreCase(in)) {
Map<String, Object> schema = new HashMap<>();
schema.put("type", type);
schema.put("example", example);
this.schema = "";
} else {
this.example = example;

View File

@ -1,21 +1,23 @@
package org.ssssssss.magicapi.swagger;
package org.ssssssss.magicapi.swagger.entity;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.ResponseBody;
import org.ssssssss.magicapi.config.MappingHandlerMapping;
import org.ssssssss.magicapi.model.ApiInfo;
import org.ssssssss.magicapi.model.BaseDefinition;
import org.ssssssss.magicapi.model.DataType;
import org.ssssssss.magicapi.model.Path;
import org.ssssssss.magicapi.provider.GroupServiceProvider;
import org.ssssssss.magicapi.core.config.MagicConfiguration;
import org.ssssssss.magicapi.core.model.ApiInfo;
import org.ssssssss.magicapi.core.model.BaseDefinition;
import org.ssssssss.magicapi.core.model.DataType;
import org.ssssssss.magicapi.core.model.Path;
import org.ssssssss.magicapi.core.service.MagicResourceService;
import org.ssssssss.magicapi.core.service.impl.RequestMagicDynamicRegistry;
import org.ssssssss.magicapi.utils.JsonUtils;
import org.ssssssss.magicapi.utils.PathUtils;
import org.ssssssss.script.parsing.ast.literal.BooleanLiteral;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import static org.ssssssss.magicapi.model.Constants.*;
import static org.ssssssss.magicapi.core.config.Constants.*;
/**
* 生成swagger用的json
@ -32,56 +34,46 @@ public class SwaggerProvider {
* body空对象
*/
private static final String BODY_EMPTY = "{}";
private final Map<String, Object> DEFINITION_MAP = new ConcurrentHashMap<>();
private MappingHandlerMapping mappingHandlerMapping;
private final RequestMagicDynamicRegistry requestMagicDynamicRegistry;
private final MagicResourceService magicResourceService;
/**
* 基础路径
*/
private String basePath;
private GroupServiceProvider groupServiceProvider;
private SwaggerEntity.Info info;
private boolean persistenceResponseBody;
private final String basePath;
private final SwaggerEntity.Info info;
private final boolean persistenceResponseBody;
public void setMappingHandlerMapping(MappingHandlerMapping mappingHandlerMapping) {
this.mappingHandlerMapping = mappingHandlerMapping;
}
public void setGroupServiceProvider(GroupServiceProvider groupServiceProvider) {
this.groupServiceProvider = groupServiceProvider;
}
public void setInfo(SwaggerEntity.Info info) {
this.info = info;
}
public void setBasePath(String basePath) {
public SwaggerProvider(RequestMagicDynamicRegistry requestMagicDynamicRegistry, MagicResourceService magicResourceService, String basePath, SwaggerEntity.Info info, boolean persistenceResponseBody) {
this.requestMagicDynamicRegistry = requestMagicDynamicRegistry;
this.magicResourceService = magicResourceService;
this.basePath = basePath;
}
public void setPersistenceResponseBody(boolean persistenceResponseBody) {
this.info = info;
this.persistenceResponseBody = persistenceResponseBody;
}
@ResponseBody
public SwaggerEntity swaggerJson() {
this.DEFINITION_MAP.clear();
List<ApiInfo> infos = mappingHandlerMapping.getApiInfos();
List<ApiInfo> infos = requestMagicDynamicRegistry.mappings();
SwaggerEntity swaggerEntity = new SwaggerEntity();
swaggerEntity.setInfo(info);
swaggerEntity.setBasePath(this.basePath);
ObjectMapper mapper = new ObjectMapper();
for (ApiInfo info : infos) {
String groupName = groupServiceProvider.getFullName(info.getGroupId()).replace("/", "-");
String requestPath = "/" + mappingHandlerMapping.getRequestPath(info.getGroupId(), info.getPath());
String groupName = magicResourceService.getGroupName(info.getGroupId()).replace("/", "-");
String requestPath = PathUtils.replaceSlash("/" + magicResourceService.getGroupPath(info.getGroupId()) + "/" + info.getPath());
SwaggerEntity.Path path = new SwaggerEntity.Path(info.getId());
path.addTag(groupName);
boolean hasBody = false;
try {
List<Map<String, Object>> parameters = parseParameters(mapper, info);
List<Map<String, Object>> parameters = parseParameters(info);
hasBody = parameters.stream().anyMatch(it -> VAR_NAME_REQUEST_BODY.equals(it.get("in")));
BaseDefinition baseDefinition = info.getRequestBodyDefinition();
if (hasBody && baseDefinition != null) {
doProcessDefinition(baseDefinition, info, "root_", "request", 0);
doProcessDefinition(baseDefinition, info, groupName, "root_", "request", 0);
}
parameters.forEach(path::addParameter);
if (this.persistenceResponseBody) {
@ -90,10 +82,10 @@ public class SwaggerProvider {
Map responseMap = parseResponse(info);
if (!responseMap.isEmpty()) {
path.setResponses(responseMap);
doProcessDefinition(baseDefinition, info, "root_" + baseDefinition.getName(), "response", 0);
doProcessDefinition(baseDefinition, info, groupName, "root_" + baseDefinition.getName(), "response", 0);
}
} else {
path.addResponse("200", mapper.readValue(Objects.toString(info.getResponseBody(), BODY_EMPTY), Object.class));
path.addResponse("200", JsonUtils.readValue(Objects.toString(info.getResponseBody(), BODY_EMPTY), Object.class));
}
}
@ -121,26 +113,23 @@ public class SwaggerProvider {
return swaggerEntity;
}
private List<Map<String, Object>> parseParameters(ObjectMapper mapper, ApiInfo info) {
private List<Map<String, Object>> parseParameters(ApiInfo info) {
List<Map<String, Object>> parameters = new ArrayList<>();
info.getParameters().forEach(it -> parameters.add(SwaggerEntity.createParameter(it.isRequired(), it.getName(), VAR_NAME_QUERY, it.getDataType().getJavascriptType(), it.getDescription(), it.getValue())));
info.getHeaders().forEach(it -> parameters.add(SwaggerEntity.createParameter(it.isRequired(), it.getName(), VAR_NAME_HEADER, it.getDataType().getJavascriptType(), it.getDescription(), it.getValue())));
List<Path> paths = new ArrayList<>(info.getPaths());
MappingHandlerMapping.findGroups(info.getGroupId())
MagicConfiguration.getMagicResourceService().getGroupsByFileId(info.getId())
.stream()
.flatMap(it -> it.getPaths().stream())
.forEach(it -> {
if (!paths.contains(it)) {
paths.add(it);
}
});
.filter(it -> !paths.contains(it))
.forEach(paths::add);
paths.forEach(it -> parameters.add(SwaggerEntity.createParameter(it.isRequired(), it.getName(), VAR_NAME_PATH_VARIABLE, it.getDataType().getJavascriptType(), it.getDescription(), it.getValue())));
try {
BaseDefinition baseDefinition = info.getRequestBodyDefinition();
if (baseDefinition != null && !CollectionUtils.isEmpty(baseDefinition.getChildren())) {
Map<String, Object> parameter = SwaggerEntity.createParameter(baseDefinition.isRequired(), StringUtils.isNotBlank(baseDefinition.getName()) ? baseDefinition.getName() : VAR_NAME_REQUEST_BODY, VAR_NAME_REQUEST_BODY, baseDefinition.getDataType().getJavascriptType(), baseDefinition.getDescription(), baseDefinition);
Map<String, Object> schema = new HashMap<>(2);
String groupName = groupServiceProvider.getFullName(info.getGroupId()).replace("/", "-");
String groupName = magicResourceService.getGroupName(info.getGroupId()).replace("/", "-");
String voName = groupName + "«" + info.getPath().replaceFirst("/", "").replaceAll("/", "_") + "«request«";
if (DataType.Array == baseDefinition.getDataType()) {
voName += "root_" + (StringUtils.isNotBlank(baseDefinition.getName()) ? baseDefinition.getName() + "_" : "_") + "»»»";
@ -158,7 +147,7 @@ public class SwaggerProvider {
parameter.put("schema", schema);
parameters.add(parameter);
} else {
Object object = mapper.readValue(info.getRequestBody(), Object.class);
Object object = JsonUtils.readValue(info.getRequestBody(), Object.class);
boolean isListOrMap = (object instanceof List || object instanceof Map);
if (isListOrMap && BooleanLiteral.isTrue(object)) {
parameters.add(SwaggerEntity.createParameter(false, VAR_NAME_REQUEST_BODY, VAR_NAME_REQUEST_BODY, object instanceof List ? VAR_NAME_REQUEST_BODY_VALUE_TYPE_ARRAY : VAR_NAME_REQUEST_BODY_VALUE_TYPE_OBJECT, null, object));
@ -175,7 +164,7 @@ public class SwaggerProvider {
BaseDefinition baseDefinition = info.getResponseBodyDefinition();
if (!CollectionUtils.isEmpty(baseDefinition.getChildren())) {
String groupName = groupServiceProvider.getFullName(info.getGroupId()).replace("/", "-");
String groupName = magicResourceService.getGroupName(info.getGroupId()).replace("/", "-");
String voName = groupName + "«" + info.getPath().replaceFirst("/", "").replaceAll("/", "_") + "«response«";
voName += "root_" + baseDefinition.getName() + "»»»";
@ -192,25 +181,24 @@ public class SwaggerProvider {
return result;
}
private Map<String, Object> doProcessDefinition(BaseDefinition target, ApiInfo info, String parentName, String definitionType, int level) {
private Map<String, Object> doProcessDefinition(BaseDefinition target, ApiInfo info, String groupName, String parentName, String definitionType, int level) {
Map<String, Object> result = new HashMap<>(4);
result.put("description", target.getDescription());
if (DataType.Array == target.getDataType()) {
if (!CollectionUtils.isEmpty(target.getChildren())) {
result.put("items", doProcessDefinition(target.getChildren().get(0), info, parentName + target.getName() + "_", definitionType, level + 1));
result.put("items", doProcessDefinition(target.getChildren().get(0), info, groupName, parentName + target.getName() + "_", definitionType, level + 1));
} else {
result.put("items", Collections.emptyList());
}
result.put("type", target.getDataType().getJavascriptType());
} else if (DataType.Object == target.getDataType()) {
String groupName = groupServiceProvider.getFullName(info.getGroupId()).replace("/", "-");
String voName = groupName + "«" + info.getPath().replaceFirst("/", "").replaceAll("/", "_") + (StringUtils.equals("response", definitionType) ? "«response«" : "«request«") + parentName + target.getName() + "»»»";
Map<String, Object> definition = new HashMap<>(4);
Map<String, Map<String, Object>> properties = new HashMap<>(target.getChildren().size());
Set<String> requiredSet = new HashSet<>(target.getChildren().size());
for (BaseDefinition obj : target.getChildren()) {
properties.put(obj.getName(), doProcessDefinition(obj, info, parentName + target.getName() + "_", definitionType, level + 1));
properties.put(obj.getName(), doProcessDefinition(obj, info, groupName, parentName + target.getName() + "_", definitionType, level + 1));
if (obj.isRequired()) {
requiredSet.add(obj.getName());
}

View File

@ -0,0 +1 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=org.ssssssss.magicapi.swagger.MagicSwaggerConfiguration

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-beta.1</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,42 @@
export default function (bus, constants, $i, Message, request) {
return {
// svg text
getIcon: item => ['TASK', '#9012FE'],
// 任务名称
name: $i('task.name'),
language: 'magicscript',
// 执行测试的逻辑
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,90 @@
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();
if (taskScheduler != null) {
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()));
logger.debug("注册定时任务:[{},{}]", MagicConfiguration.getMagicResourceService().getScriptName(info), info.getCron());
}
return true;
}
@Override
protected void unregister(MappingNode<TaskInfo> mappingNode) {
if (taskScheduler == null) {
return;
}
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,58 @@
package org.ssssssss.magicapi.task.starter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
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
@EnableConfigurationProperties(MagicTaskConfig.class)
public class MagicAPITaskConfiguration implements MagicPluginConfiguration {
private final MagicTaskConfig config;
public MagicAPITaskConfiguration(MagicTaskConfig config) {
this.config = config;
}
@Bean
@ConditionalOnMissingBean
public TaskInfoMagicResourceStorage taskInfoMagicResourceStorage() {
return new TaskInfoMagicResourceStorage();
}
@Bean
@ConditionalOnMissingBean
public TaskMagicDynamicRegistry taskMagicDynamicRegistry(TaskInfoMagicResourceStorage taskInfoMagicResourceStorage) {
MagicTaskConfig.Shutdown shutdown = config.getShutdown();
ThreadPoolTaskScheduler poolTaskScheduler = null;
if(config.isEnable()){
poolTaskScheduler = new ThreadPoolTaskScheduler();
poolTaskScheduler.setPoolSize(config.getPool().getSize());
poolTaskScheduler.setWaitForTasksToCompleteOnShutdown(shutdown.isAwaitTermination());
if(shutdown.getAwaitTerminationPeriod() != null){
poolTaskScheduler.setAwaitTerminationSeconds((int) shutdown.getAwaitTerminationPeriod().getSeconds());
}
poolTaskScheduler.setThreadNamePrefix(config.getThreadNamePrefix());
poolTaskScheduler.initialize();
}
return new TaskMagicDynamicRegistry(taskInfoMagicResourceStorage, poolTaskScheduler);
}
@Override
public Plugin plugin() {
return new Plugin("定时任务", "MagicTask", "magic-task.1.0.0.iife.js");
}
@Override
public MagicControllerRegister controllerRegister() {
return (mapping, configuration) -> mapping.registerController(new MagicTaskController(configuration));
}
}

View File

@ -0,0 +1,101 @@
package org.ssssssss.magicapi.task.starter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
@ConfigurationProperties("magic-api.task")
public class MagicTaskConfig {
/**
* 是否启用定时任务
*/
private boolean enable = true;
/**
* 线程池相关配置
*/
private final Pool pool = new Pool();
/**
* 关闭时相关配置
*/
private final Shutdown shutdown = new Shutdown();
/**
* 线程池前缀
*/
private String threadNamePrefix = "magic-task-";
public Pool getPool() {
return this.pool;
}
public Shutdown getShutdown() {
return this.shutdown;
}
public String getThreadNamePrefix() {
return this.threadNamePrefix;
}
public void setThreadNamePrefix(String threadNamePrefix) {
this.threadNamePrefix = threadNamePrefix;
}
public boolean isEnable() {
return enable;
}
public void setEnable(boolean enable) {
this.enable = enable;
}
public static class Pool {
/**
* 线程池大小
*/
private int size = Runtime.getRuntime().availableProcessors();
public int getSize() {
return this.size;
}
public void setSize(int size) {
this.size = size;
}
}
public static class Shutdown {
/**
* 关闭时是否等待任务执行完毕默认为false
*/
private boolean awaitTermination;
/**
* 关闭时最多等待任务执行完毕的时间
*/
private Duration awaitTerminationPeriod;
public boolean isAwaitTermination() {
return this.awaitTermination;
}
public void setAwaitTermination(boolean awaitTermination) {
this.awaitTermination = awaitTermination;
}
public Duration getAwaitTerminationPeriod() {
return this.awaitTerminationPeriod;
}
public void setAwaitTerminationPeriod(Duration awaitTerminationPeriod) {
this.awaitTerminationPeriod = awaitTerminationPeriod;
}
}
}

View File

@ -0,0 +1,43 @@
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.config.WebSocketSessionManager;
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());
String sessionAndScriptId = debugRequest.getRequestedClientId() + debugRequest.getRequestedScriptId();
try {
MagicScriptDebugContext magicScriptContext = debugRequest.createMagicScriptContext(configuration.getDebugTimeout());
WebSocketSessionManager.addMagicScriptContext(sessionAndScriptId, magicScriptContext);
magicScriptContext.setScriptName(MagicConfiguration.getMagicResourceService().getScriptName(entity));
return new JsonBean<>(ScriptManager.executeScript(script, magicScriptContext));
} finally {
WebSocketSessionManager.removeMagicScriptContext(sessionAndScriptId);
MagicLoggerContext.SESSION.remove();
}
}
}

View File

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

41
magic-api-plugins/pom.xml Normal file
View File

@ -0,0 +1,41 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.ssssssss</groupId>
<artifactId>magic-api-parent</artifactId>
<version>2.0.0-beta.1</version>
</parent>
<artifactId>magic-api-plugins</artifactId>
<version>2.0.0-beta.1</version>
<packaging>pom</packaging>
<name>magic-api-plugins</name>
<description>auto generate http api</description>
<modules>
<module>magic-api-plugin-task</module>
<module>magic-api-plugin-swagger</module>
<module>magic-api-plugin-redis</module>
<module>magic-api-plugin-mongo</module>
<module>magic-api-plugin-elasticsearch</module>
<module>magic-api-plugin-cluster</module>
</modules>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.ssssssss</groupId>
<artifactId>magic-api</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.ssssssss</groupId>
<artifactId>magic-script</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>org.ssssssss</groupId>
<artifactId>magic-api-parent</artifactId>
<version>1.7.5</version>
<version>2.0.0-beta.1</version>
</parent>
<artifactId>magic-api-spring-boot-starter</artifactId>
<packaging>jar</packaging>
@ -18,16 +18,6 @@
<artifactId>spring-boot-starter</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
@ -55,11 +45,6 @@
<artifactId>fastjson</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>

View File

@ -7,6 +7,7 @@ import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.ssssssss.magicapi.core.config.MagicAPIProperties;
import org.ssssssss.magicapi.utils.PathUtils;
import java.net.InetAddress;
@ -25,7 +26,7 @@ import java.util.Objects;
@Order
public class ApplicationUriPrinter implements CommandLineRunner {
@Value("${server.port:9999}")
@Value("${server.port:8080}")
private int port;
@Value("${server.servlet.context-path:}")

View File

@ -1,51 +0,0 @@
package org.ssssssss.magicapi.spring.boot.starter;
import java.util.UUID;
/**
* 集群配置
*
* @author mxd
* @since 1.2.0
*/
public class ClusterConfig {
/**
* 是否启用默认不启用
*/
private boolean enable = false;
/**
* 实例ID集群环境下要保证每台机器不同默认启动后随机生成uuid
*/
private String instanceId = UUID.randomUUID().toString();
/**
* redis 通道
*/
private String channel = "magic-api:notify:channel";
public String getInstanceId() {
return instanceId;
}
public void setInstanceId(String instanceId) {
this.instanceId = instanceId;
}
public boolean isEnable() {
return enable;
}
public void setEnable(boolean enable) {
this.enable = enable;
}
public String getChannel() {
return channel;
}
public void setChannel(String channel) {
this.channel = channel;
}
}

View File

@ -5,7 +5,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -17,46 +17,52 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.Ordered;
import org.springframework.core.env.Environment;
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.web.bind.annotation.ResponseBody;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistration;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.ssssssss.magicapi.adapter.ColumnMapperAdapter;
import org.ssssssss.magicapi.adapter.DialectAdapter;
import org.ssssssss.magicapi.adapter.Resource;
import org.ssssssss.magicapi.adapter.ResourceAdapter;
import org.ssssssss.magicapi.adapter.resource.DatabaseResource;
import org.ssssssss.magicapi.cache.DefaultSqlCache;
import org.ssssssss.magicapi.cache.SqlCache;
import org.ssssssss.magicapi.config.*;
import org.ssssssss.magicapi.controller.*;
import org.ssssssss.magicapi.dialect.Dialect;
import org.ssssssss.magicapi.exception.MagicAPIException;
import org.ssssssss.magicapi.interceptor.*;
import org.ssssssss.magicapi.logging.LoggerManager;
import org.ssssssss.magicapi.model.Constants;
import org.ssssssss.magicapi.model.DataType;
import org.ssssssss.magicapi.model.Options;
import org.ssssssss.magicapi.modules.*;
import org.ssssssss.magicapi.provider.*;
import org.ssssssss.magicapi.provider.impl.*;
import org.ssssssss.magicapi.utils.ClassScanner;
import org.ssssssss.magicapi.backup.service.MagicBackupService;
import org.ssssssss.magicapi.backup.service.MagicDatabaseBackupService;
import org.ssssssss.magicapi.backup.web.MagicBackupController;
import org.ssssssss.magicapi.core.annotation.MagicModule;
import org.ssssssss.magicapi.core.config.*;
import org.ssssssss.magicapi.core.exception.MagicAPIException;
import org.ssssssss.magicapi.core.handler.MagicCoordinationHandler;
import org.ssssssss.magicapi.core.handler.MagicDebugHandler;
import org.ssssssss.magicapi.core.handler.MagicWebSocketDispatcher;
import org.ssssssss.magicapi.core.handler.MagicWorkbenchHandler;
import org.ssssssss.magicapi.core.interceptor.*;
import org.ssssssss.magicapi.core.logging.LoggerManager;
import org.ssssssss.magicapi.core.model.DataType;
import org.ssssssss.magicapi.core.model.MagicEntity;
import org.ssssssss.magicapi.core.model.Plugin;
import org.ssssssss.magicapi.core.resource.DatabaseResource;
import org.ssssssss.magicapi.core.resource.ResourceAdapter;
import org.ssssssss.magicapi.core.service.*;
import org.ssssssss.magicapi.core.service.impl.DefaultMagicAPIService;
import org.ssssssss.magicapi.core.service.impl.DefaultMagicResourceService;
import org.ssssssss.magicapi.core.service.impl.RequestMagicDynamicRegistry;
import org.ssssssss.magicapi.core.web.MagicResourceController;
import org.ssssssss.magicapi.core.web.MagicWorkbenchController;
import org.ssssssss.magicapi.core.web.RequestHandler;
import org.ssssssss.magicapi.datasource.model.MagicDynamicDataSource;
import org.ssssssss.magicapi.datasource.service.DataSourceEncryptProvider;
import org.ssssssss.magicapi.datasource.web.MagicDataSourceController;
import org.ssssssss.magicapi.function.service.FunctionMagicDynamicRegistry;
import org.ssssssss.magicapi.jsr223.LanguageProvider;
import org.ssssssss.magicapi.modules.servlet.RequestModule;
import org.ssssssss.magicapi.modules.servlet.ResponseModule;
import org.ssssssss.magicapi.modules.spring.EnvModule;
import org.ssssssss.magicapi.utils.Mapping;
import org.ssssssss.magicapi.utils.PathUtils;
import org.ssssssss.script.MagicResourceLoader;
import org.ssssssss.script.MagicScript;
import org.ssssssss.script.MagicScriptEngine;
@ -66,16 +72,12 @@ import org.ssssssss.script.functions.ExtensionMethod;
import org.ssssssss.script.parsing.ast.statement.AsyncCall;
import org.ssssssss.script.reflection.JavaReflection;
import javax.servlet.http.HttpServletRequest;
import javax.sql.DataSource;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
/**
* magic-api自动配置类
@ -85,8 +87,9 @@ import java.util.function.BiFunction;
@Configuration
@ConditionalOnClass({RequestMappingHandlerMapping.class})
@EnableConfigurationProperties(MagicAPIProperties.class)
@Import({MagicRedisAutoConfiguration.class, MagicMongoAutoConfiguration.class, MagicSwaggerConfiguration.class, MagicJsonAutoConfiguration.class, ApplicationUriPrinter.class})
@Import({MagicJsonAutoConfiguration.class, ApplicationUriPrinter.class, MagicModuleConfiguration.class, MagicDynamicRegistryConfiguration.class})
@EnableWebSocket
@AutoConfigureAfter(MagicPluginConfiguration.class)
public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketConfigurer {
private static final Logger logger = LoggerFactory.getLogger(MagicAPIAutoConfiguration.class);
@ -96,15 +99,6 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
*/
private final ObjectProvider<List<RequestInterceptor>> requestInterceptorsProvider;
/**
* SQL拦截器
*/
private final ObjectProvider<List<SQLInterceptor>> sqlInterceptorsProvider;
/**
* 单表API拦截器
*/
private final ObjectProvider<List<NamedTableInterceptor>> namedTableInterceptorsProvider;
/**
* 自定义的类型扩展
@ -116,16 +110,6 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
*/
private final ObjectProvider<List<HttpMessageConverter<?>>> httpMessageConvertersProvider;
/**
* 自定义的方言
*/
private final ObjectProvider<List<Dialect>> dialectsProvider;
/**
* 自定义的列名转换
*/
private final ObjectProvider<List<ColumnMapperProvider>> columnMapperProvidersProvider;
private final ObjectProvider<AuthorizationInterceptor> authorizationInterceptorProvider;
@ -134,11 +118,15 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
*/
private final ObjectProvider<List<MagicFunction>> magicFunctionsProvider;
private final ObjectProvider<List<MagicPluginConfiguration>> magicPluginsProvider;
private final ObjectProvider<MagicNotifyService> magicNotifyServiceProvider;
private final ObjectProvider<DataSourceEncryptProvider> dataSourceEncryptProvider;
private final ObjectProvider<List<MagicDynamicRegistry<? extends MagicEntity>>> magicDynamicRegistriesProvider;
private final Environment environment;
private final ObjectProvider<List<MagicResourceStorage<? extends MagicEntity>>> magicResourceStoragesProvider;
private final ObjectProvider<DataSourceEncryptProvider> dataSourceEncryptProvider;
private final MagicCorsFilter magicCorsFilter = new MagicCorsFilter();
@ -156,94 +144,40 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
@Lazy
private RequestMappingHandlerMapping requestMappingHandlerMapping;
@Autowired(required = false)
private MultipartResolver multipartResolver;
private String allClassTxt;
private DefaultAuthorizationInterceptor defaultAuthorizationInterceptor;
public MagicAPIAutoConfiguration(MagicAPIProperties properties,
ObjectProvider<List<Dialect>> dialectsProvider,
ObjectProvider<List<RequestInterceptor>> requestInterceptorsProvider,
ObjectProvider<List<SQLInterceptor>> sqlInterceptorsProvider,
ObjectProvider<List<ExtensionMethod>> extensionMethodsProvider,
ObjectProvider<List<HttpMessageConverter<?>>> httpMessageConvertersProvider,
ObjectProvider<List<ColumnMapperProvider>> columnMapperProvidersProvider,
ObjectProvider<List<MagicFunction>> magicFunctionsProvider,
ObjectProvider<List<MagicPluginConfiguration>> magicPluginsProvider,
ObjectProvider<MagicNotifyService> magicNotifyServiceProvider,
ObjectProvider<AuthorizationInterceptor> authorizationInterceptorProvider,
ObjectProvider<List<NamedTableInterceptor>> namedTableInterceptorsProvider,
ObjectProvider<DataSourceEncryptProvider> dataSourceEncryptProvider,
Environment environment,
ObjectProvider<List<MagicDynamicRegistry<? extends MagicEntity>>> magicDynamicRegistriesProvider,
ObjectProvider<List<MagicResourceStorage<? extends MagicEntity>>> magicResourceStoragesProvider,
ApplicationContext applicationContext
) {
this.properties = properties;
this.dialectsProvider = dialectsProvider;
this.requestInterceptorsProvider = requestInterceptorsProvider;
this.sqlInterceptorsProvider = sqlInterceptorsProvider;
this.extensionMethodsProvider = extensionMethodsProvider;
this.httpMessageConvertersProvider = httpMessageConvertersProvider;
this.columnMapperProvidersProvider = columnMapperProvidersProvider;
this.magicFunctionsProvider = magicFunctionsProvider;
this.magicPluginsProvider = magicPluginsProvider;
this.magicNotifyServiceProvider = magicNotifyServiceProvider;
this.authorizationInterceptorProvider = authorizationInterceptorProvider;
this.namedTableInterceptorsProvider = namedTableInterceptorsProvider;
this.dataSourceEncryptProvider = dataSourceEncryptProvider;
this.environment = environment;
this.magicDynamicRegistriesProvider = magicDynamicRegistriesProvider;
this.magicResourceStoragesProvider = magicResourceStoragesProvider;
this.applicationContext = applicationContext;
}
private String redirectIndex(HttpServletRequest request) {
if (request.getRequestURI().endsWith("/")) {
return "redirect:./index.html";
}
return "redirect:" + properties.getWeb() + "/index.html";
}
@ResponseBody
private MagicAPIProperties readConfig() {
return properties;
}
@ResponseBody
private String readClass() {
if (allClassTxt == null) {
try {
allClassTxt = ClassScanner.compress(ClassScanner.scan());
} catch (Throwable t) {
logger.warn("扫描Class失败", t);
allClassTxt = "";
}
}
return allClassTxt;
}
@Bean
@ConditionalOnMissingBean(HttpModule.class)
public HttpModule magicHttpModule() {
return new HttpModule(createRestTemplate());
}
/**
* 注入动态数据源
*/
@Bean
@ConditionalOnMissingBean(MagicDynamicDataSource.class)
public MagicDynamicDataSource magicDynamicDataSource(@Autowired(required = false) DataSource dataSource) {
MagicDynamicDataSource dynamicDataSource = new MagicDynamicDataSource();
if (dataSource != null) {
dynamicDataSource.put(dataSource);
} else {
logger.warn("当前数据源未配置");
}
return dynamicDataSource;
}
@Bean
@ConditionalOnMissingBean(Resource.class)
@ConditionalOnMissingBean(org.ssssssss.magicapi.core.resource.Resource.class)
@ConditionalOnProperty(prefix = "magic-api", name = "resource.type", havingValue = "database")
public Resource magicDatabaseResource(MagicDynamicDataSource magicDynamicDataSource) {
ResourceConfig resourceConfig = properties.getResource();
public org.ssssssss.magicapi.core.resource.Resource magicDatabaseResource(MagicDynamicDataSource magicDynamicDataSource) {
Resource resourceConfig = properties.getResource();
if (magicDynamicDataSource.isEmpty()) {
throw new MagicAPIException("当前未配置数据源,如已配置,请引入 spring-boot-starter-jdbc 后在试!");
}
@ -252,18 +186,18 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
}
@Bean
@ConditionalOnMissingBean(Resource.class)
@ConditionalOnMissingBean(org.ssssssss.magicapi.core.resource.Resource.class)
@ConditionalOnProperty(prefix = "magic-api", name = "resource.type", havingValue = "file", matchIfMissing = true)
public Resource magicResource() throws IOException {
ResourceConfig resourceConfig = properties.getResource();
public org.ssssssss.magicapi.core.resource.Resource magicResource() throws IOException {
Resource resourceConfig = properties.getResource();
return ResourceAdapter.getResource(resourceConfig.getLocation(), resourceConfig.isReadonly());
}
@Bean
@ConditionalOnMissingBean(MagicBackupService.class)
@ConditionalOnProperty(prefix = "magic-api", name = "backup-config.resource-type", havingValue = "database")
@ConditionalOnProperty(prefix = "magic-api", name = "backup.enable", havingValue = "true")
public MagicBackupService magicDatabaseBackupService(MagicDynamicDataSource magicDynamicDataSource) {
BackupConfig backupConfig = properties.getBackupConfig();
Backup backupConfig = properties.getBackup();
MagicDynamicDataSource.DataSourceNode dataSourceNode = magicDynamicDataSource.getDataSource(backupConfig.getDatasource());
return new MagicDatabaseBackupService(new JdbcTemplate(dataSourceNode.getDataSource()), backupConfig.getTableName());
}
@ -277,16 +211,6 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
LoggerManager.createMagicAppender();
// 配置静态资源路径
registry.addResourceHandler(web + "/**").addResourceLocations("classpath:/magic-editor/");
try {
Mapping mapping = Mapping.create(requestMappingHandlerMapping);
// 默认首页设置
mapping.register(mapping.paths(web).build(), this, MagicAPIAutoConfiguration.class.getDeclaredMethod("redirectIndex", HttpServletRequest.class))
// 读取配置
.register(mapping.paths(web + "/config.json").build(), this, MagicAPIAutoConfiguration.class.getDeclaredMethod("readConfig"))
// 读取配置
.register(mapping.paths(web + "/classes.txt").produces("text/plain").build(), this, MagicAPIAutoConfiguration.class.getDeclaredMethod("readClass"));
} catch (NoSuchMethodException ignored) {
}
}
}
@ -310,148 +234,33 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
}
@Bean
@ConditionalOnMissingBean(PageProvider.class)
public PageProvider pageProvider() {
PageConfig pageConfig = properties.getPageConfig();
logger.info("未找到分页实现,采用默认分页实现,分页配置:(页码={},页大小={},默认首页={},默认页大小={})", pageConfig.getPage(), pageConfig.getSize(), pageConfig.getDefaultPage(), pageConfig.getDefaultSize());
return new DefaultPageProvider(pageConfig.getPage(), pageConfig.getSize(), pageConfig.getDefaultPage(), pageConfig.getDefaultSize());
@ConditionalOnMissingBean
public MagicResourceService magicResourceService(org.ssssssss.magicapi.core.resource.Resource workspace) {
return new DefaultMagicResourceService(workspace, magicResourceStoragesProvider.getObject(), applicationContext);
}
/**
* 注入结果构建方法
*/
@Bean
@ConditionalOnMissingBean(ResultProvider.class)
public ResultProvider resultProvider() {
return new DefaultResultProvider(properties.getResponse());
}
/**
* 注入SQL缓存实现
*/
@Bean
@ConditionalOnMissingBean(SqlCache.class)
public SqlCache sqlCache() {
CacheConfig cacheConfig = properties.getCacheConfig();
logger.info("未找到SQL缓存实现采用默认缓存实现(LRU+TTL),缓存配置:(容量={},TTL={})", cacheConfig.getCapacity(), cacheConfig.getTtl());
return new DefaultSqlCache(cacheConfig.getCapacity(), cacheConfig.getTtl());
}
/**
* 注入接口映射
*/
@Bean
public MappingHandlerMapping mappingHandlerMapping() throws NoSuchMethodException {
String prefix = StringUtils.isNotBlank(properties.getPrefix()) ? PathUtils.replaceSlash("/" + properties.getPrefix() + "/") : null;
return new MappingHandlerMapping(prefix, properties.isAllowOverride());
}
@Bean
@ConditionalOnMissingBean(FunctionServiceProvider.class)
public FunctionServiceProvider functionServiceProvider(GroupServiceProvider groupServiceProvider, Resource magicResource) {
return new DefaultFunctionServiceProvider(groupServiceProvider, magicResource);
}
/**
* 注入分组存储service
*/
@Bean
@ConditionalOnMissingBean(GroupServiceProvider.class)
public GroupServiceProvider groupServiceProvider(Resource magicResource) {
return new DefaultGroupServiceProvider(magicResource);
}
/**
* 注入接口存储service
*/
@Bean
@ConditionalOnMissingBean(ApiServiceProvider.class)
public ApiServiceProvider apiServiceProvider(GroupServiceProvider groupServiceProvider, Resource magicResource) {
return new DefaultApiServiceProvider(groupServiceProvider, magicResource);
}
@Bean
@ConditionalOnMissingBean(MagicNotifyService.class)
public MagicNotifyService magicNotifyService() {
logger.info("未配置集群通知服务,本实例不会推送通知,集群环境下可能会有问题,如需开启,请配置magic-api.cluster-config.enable=true若开启后本提示还在请检查 spring-boot-starter-data-redis 是否引入");
logger.info("未配置集群通知服务本实例不会推送通知集群环境下可能会有问题如需开启请引用magic-api-plugin-cluster插件");
return magicNotify -> {
};
}
@Bean
@ConditionalOnMissingBean(MagicBackupService.class)
@ConditionalOnProperty(prefix = "magic-api", name = "backup-config.resource-type", havingValue = "file", matchIfMissing = true)
public MagicBackupService magicFileBackupService() {
return new MagicFileBackupService(new File(properties.getBackupConfig().getLocation()));
}
@Bean
public MagicFunctionManager magicFunctionManager(GroupServiceProvider groupServiceProvider, FunctionServiceProvider functionServiceProvider) {
return new MagicFunctionManager(groupServiceProvider, functionServiceProvider);
}
/**
* 注入API调用Service
*/
@Bean
@ConditionalOnMissingBean
public MagicAPIService magicAPIService(MappingHandlerMapping mappingHandlerMapping,
ApiServiceProvider apiServiceProvider,
FunctionServiceProvider functionServiceProvider,
GroupServiceProvider groupServiceProvider,
ResultProvider resultProvider,
MagicDynamicDataSource magicDynamicDataSource,
MagicFunctionManager magicFunctionManager,
Resource workspace,
MagicBackupService magicBackupService) {
return new DefaultMagicAPIService(mappingHandlerMapping, apiServiceProvider, functionServiceProvider, groupServiceProvider, resultProvider, magicDynamicDataSource, magicFunctionManager, magicNotifyServiceProvider.getObject(), properties.getClusterConfig().getInstanceId(), workspace, magicBackupService, dataSourceEncryptProvider.getIfAvailable() , properties.isThrowException());
}
/**
* 注入数据库查询模块
*/
@Bean
@ConditionalOnBean({MagicDynamicDataSource.class})
public SQLModule magicSqlModule(MagicDynamicDataSource dynamicDataSource,
ResultProvider resultProvider,
PageProvider pageProvider,
SqlCache sqlCache) {
SQLModule sqlModule = new SQLModule(dynamicDataSource);
if (!dynamicDataSource.isEmpty()) {
sqlModule.setDataSourceNode(dynamicDataSource.getDataSource());
}
sqlModule.setResultProvider(resultProvider);
sqlModule.setPageProvider(pageProvider);
List<SQLInterceptor> sqlInterceptors = sqlInterceptorsProvider.getIfAvailable(ArrayList::new);
if (properties.isShowSql()) {
sqlInterceptors.add(new DefaultSqlInterceptor());
}
sqlModule.setSqlInterceptors(sqlInterceptors);
sqlModule.setNamedTableInterceptors(namedTableInterceptorsProvider.getIfAvailable(Collections::emptyList));
ColumnMapperAdapter columnMapperAdapter = new ColumnMapperAdapter();
this.columnMapperProvidersProvider.getIfAvailable(Collections::emptyList).stream().filter(mapperProvider -> !"default".equals(mapperProvider.name())).forEach(columnMapperAdapter::add);
columnMapperAdapter.setDefault(properties.getSqlColumnCase());
sqlModule.setColumnMapperProvider(columnMapperAdapter);
sqlModule.setColumnMapRowMapper(columnMapperAdapter.getDefaultColumnMapRowMapper());
sqlModule.setRowMapColumnMapper(columnMapperAdapter.getDefaultRowMapColumnMapper());
sqlModule.setSqlCache(sqlCache);
DialectAdapter dialectAdapter = new DialectAdapter();
dialectsProvider.getIfAvailable(Collections::emptyList).forEach(dialectAdapter::add);
sqlModule.setDialectAdapter(dialectAdapter);
sqlModule.setLogicDeleteColumn(properties.getCrudConfig().getLogicDeleteColumn());
sqlModule.setLogicDeleteValue(properties.getCrudConfig().getLogicDeleteValue());
return sqlModule;
public MagicAPIService magicAPIService(ResultProvider resultProvider, MagicResourceService magicResourceService, RequestMagicDynamicRegistry requestMagicDynamicRegistry, FunctionMagicDynamicRegistry functionMagicDynamicRegistry) {
return new DefaultMagicAPIService(resultProvider, properties.getInstanceId(), magicResourceService, requestMagicDynamicRegistry, functionMagicDynamicRegistry, properties.isThrowException(), applicationContext);
}
/**
* 注册模块类型扩展
*/
private void setupMagicModules(MagicDynamicDataSource dynamicDataSource,
SQLModule sqlModule,
ResultProvider resultProvider,
List<MagicModule> magicModules,
List<ExtensionMethod> extensionMethods,
List<LanguageProvider> languageProviders) {
private void setupMagicModules(List<ExtensionMethod> extensionMethods, List<LanguageProvider> languageProviders) {
// 设置脚本import时 class加载策略
MagicResourceLoader.setClassLoader((className) -> {
try {
@ -480,27 +289,13 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
}).orElse(null)
);
logger.info("注册模块:{} -> {}", "log", Logger.class);
MagicResourceLoader.addModule("log", new DynamicModuleImport(Logger.class, context -> LoggerFactory.getLogger(Objects.toString(context.getScriptName(),"Unknown"))));
MagicResourceLoader.addModule("log", new DynamicModuleImport(Logger.class, context -> LoggerFactory.getLogger(Objects.toString(context.getScriptName(), "Unknown"))));
List<String> importModules = properties.getAutoImportModuleList();
logger.info("注册模块:{} -> {}", "env", EnvModule.class);
MagicResourceLoader.addModule("env", new EnvModule(environment));
logger.info("注册模块:{} -> {}", "request", RequestModule.class);
MagicResourceLoader.addModule("request", new RequestModule(multipartResolver));
logger.info("注册模块:{} -> {}", "response", ResponseModule.class);
MagicResourceLoader.addModule("response", new ResponseModule(resultProvider));
logger.info("注册模块:{} -> {}", "assert", AssertModule.class);
MagicResourceLoader.addModule("assert", new AssertModule());
magicModules.forEach(module -> {
logger.info("注册模块:{} -> {}", module.getModuleName(), module.getClass());
MagicResourceLoader.addModule(module.getModuleName(), module);
applicationContext.getBeansWithAnnotation(MagicModule.class).values().forEach(module -> {
String moduleName = module.getClass().getAnnotation(MagicModule.class).value();
logger.info("注册模块:{} -> {}", moduleName, module.getClass());
MagicResourceLoader.addModule(moduleName, module);
});
MagicResourceLoader.addModule(sqlModule.getModuleName(), new DynamicModuleImport(SQLModule.class, context -> {
String dataSourceKey = context.getString(Options.DEFAULT_DATA_SOURCE.getValue());
if(StringUtils.isEmpty(dataSourceKey)) return sqlModule;
SQLModule newSqlModule = sqlModule.cloneSQLModule();
newSqlModule.setDataSourceNode(dynamicDataSource.getDataSource(dataSourceKey));
return newSqlModule;
}));
MagicResourceLoader.getModuleNames().stream().filter(importModules::contains).forEach(moduleName -> {
logger.info("自动导入模块:{}", moduleName);
MagicScriptEngine.addDefaultImport(moduleName, MagicResourceLoader.loadModule(moduleName));
@ -516,47 +311,34 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
}
@Bean
public JSR223LanguageProvider jsr223LanguageProvider() {
return new JSR223LanguageProvider();
}
@Bean
public MagicConfiguration magicConfiguration(MagicDynamicDataSource dynamicDataSource,
SQLModule sqlModule,
List<MagicModule> magicModules,
List<LanguageProvider> languageProviders,
Resource magicResource,
public MagicConfiguration magicConfiguration(List<LanguageProvider> languageProviders,
org.ssssssss.magicapi.core.resource.Resource magicResource,
ResultProvider resultProvider,
MagicResourceService magicResourceService,
MagicAPIService magicAPIService,
ApiServiceProvider apiServiceProvider,
GroupServiceProvider groupServiceProvider,
MappingHandlerMapping mappingHandlerMapping,
FunctionServiceProvider functionServiceProvider,
MagicNotifyService magicNotifyService,
MagicFunctionManager magicFunctionManager,
MagicBackupService magicBackupService) throws NoSuchMethodException {
RequestMagicDynamicRegistry requestMagicDynamicRegistry,
@Autowired(required = false) MagicBackupService magicBackupService) throws NoSuchMethodException {
logger.info("magic-api工作目录:{}", magicResource);
AsyncCall.setThreadPoolExecutorSize(properties.getThreadPoolExecutorSize());
DataType.DATE_PATTERNS = properties.getDatePattern();
MagicScript.setCompileCache(properties.getCompileCacheSize());
// 设置响应结果的code值
ResponseCodeConfig responseCodeConfig = properties.getResponseCodeConfig();
ResponseCode responseCodeConfig = properties.getResponseCode();
Constants.RESPONSE_CODE_SUCCESS = responseCodeConfig.getSuccess();
Constants.RESPONSE_CODE_INVALID = responseCodeConfig.getInvalid();
Constants.RESPONSE_CODE_EXCEPTION = responseCodeConfig.getException();
// 设置模块和扩展方法
setupMagicModules(dynamicDataSource, sqlModule, resultProvider, magicModules, extensionMethodsProvider.getIfAvailable(Collections::emptyList), languageProviders);
setupMagicModules(extensionMethodsProvider.getIfAvailable(Collections::emptyList), languageProviders);
MagicConfiguration configuration = new MagicConfiguration();
configuration.setMagicAPIService(magicAPIService);
configuration.setMagicNotifyService(magicNotifyService);
configuration.setInstanceId(properties.getClusterConfig().getInstanceId());
configuration.setApiServiceProvider(apiServiceProvider);
configuration.setGroupServiceProvider(groupServiceProvider);
configuration.setMappingHandlerMapping(mappingHandlerMapping);
configuration.setFunctionServiceProvider(functionServiceProvider);
configuration.setInstanceId(properties.getInstanceId());
configuration.setMagicResourceService(magicResourceService);
configuration.setMagicDynamicRegistries(magicDynamicRegistriesProvider.getObject());
configuration.setMagicBackupService(magicBackupService);
SecurityConfig securityConfig = properties.getSecurityConfig();
configuration.setDebugTimeout(properties.getDebugConfig().getTimeout());
Security securityConfig = properties.getSecurityConfig();
configuration.setDebugTimeout(properties.getDebug().getTimeout());
configuration.setHttpMessageConverters(httpMessageConvertersProvider.getIfAvailable(Collections::emptyList));
configuration.setResultProvider(resultProvider);
configuration.setThrowException(properties.isThrowException());
@ -568,32 +350,25 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
// 向页面传递配置信息时不传递用户名密码增强安全性
securityConfig.setUsername(null);
securityConfig.setPassword(null);
requestMagicDynamicRegistry.setHandler(new RequestHandler(configuration, requestMagicDynamicRegistry));
List<MagicPluginConfiguration> pluginConfigurations = magicPluginsProvider.getIfAvailable(Collections::emptyList);
List<Plugin> plugins = pluginConfigurations.stream().map(MagicPluginConfiguration::plugin).collect(Collectors.toList());
// 构建UI请求处理器
String base = properties.getWeb();
mappingHandlerMapping.setRequestMappingHandlerMapping(requestMappingHandlerMapping);
MagicDataSourceController dataSourceController = new MagicDataSourceController(configuration);
MagicWorkbenchController magicWorkbenchController = new MagicWorkbenchController(configuration, properties.getSecretKey());
Mapping mapping = Mapping.create(requestMappingHandlerMapping, base, properties.getPrefix());
MagicWorkbenchController magicWorkbenchController = new MagicWorkbenchController(configuration, properties, plugins);
if (base != null) {
configuration.setEnableWeb(true);
List<MagicController> controllers = new ArrayList<>(Arrays.asList(
new MagicAPIController(configuration),
dataSourceController,
magicWorkbenchController,
new MagicGroupController(configuration),
new MagicFunctionController(configuration)
));
controllers.forEach(item -> mappingHandlerMapping.registerController(item, base));
mapping.registerController(magicWorkbenchController)
.registerController(new MagicResourceController(configuration))
.registerController(new MagicDataSourceController(configuration))
.registerController(new MagicBackupController(configuration));
pluginConfigurations.forEach(it -> it.controllerRegister().register(mapping, configuration));
}
// 注册接收推送的接口
if (StringUtils.isNotBlank(properties.getSecretKey())) {
Mapping mapping = Mapping.create(requestMappingHandlerMapping);
RequestMappingInfo requestMappingInfo = mapping.paths(properties.getPushPath()).build();
Method method = MagicWorkbenchController.class.getDeclaredMethod("receivePush", MultipartFile.class, String.class, Long.class, String.class);
mapping.register(requestMappingInfo, magicWorkbenchController, method);
mapping.register(mapping.paths(properties.getPushPath()).methods(RequestMethod.POST).build(), magicWorkbenchController, MagicWorkbenchController.class.getDeclaredMethod("receivePush", MultipartFile.class, String.class, Long.class, String.class));
}
// 注册数据源
magicAPIService.registerAllDataSource();
// 设置拦截器信息
this.requestInterceptorsProvider.getIfAvailable(Collections::emptyList).forEach(interceptor -> {
logger.info("注册请求拦截器:{}", interceptor.getClass());
@ -601,21 +376,14 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
});
// 打印banner
if (this.properties.isBanner()) {
configuration.printBanner();
configuration.printBanner(plugins.stream().map(Plugin::getName).collect(Collectors.toList()));
}
if (magicBackupService == null) {
logger.error("当前备份设置未配置,强烈建议配置备份设置,以免代码丢失。");
}
configuration.setMagicFunctionManager(magicFunctionManager);
// 注册函数加载器
magicFunctionManager.registerFunctionLoader();
// 注册所有函数
magicFunctionManager.registerAllFunction();
mappingHandlerMapping.setHandler(new RequestHandler(configuration));
mappingHandlerMapping.setMagicApiService(apiServiceProvider);
mappingHandlerMapping.setGroupServiceProvider(groupServiceProvider);
// 注册所有映射
mappingHandlerMapping.registerAllMapping();
// 备份清理
if (properties.getBackupConfig().getMaxHistory() > 0) {
long interval = properties.getBackupConfig().getMaxHistory() * 86400000L;
if (properties.getBackup().isEnable() && properties.getBackup().getMaxHistory() > 0 && magicBackupService != null) {
long interval = properties.getBackup().getMaxHistory() * 86400000L;
// 1小时执行1次
new ScheduledThreadPoolExecutor(1, r -> new Thread(r, "magic-api-clean-task")).scheduleAtFixedRate(() -> {
try {
@ -635,26 +403,11 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
if (defaultAuthorizationInterceptor != null) {
return defaultAuthorizationInterceptor;
}
SecurityConfig securityConfig = properties.getSecurityConfig();
Security securityConfig = properties.getSecurityConfig();
defaultAuthorizationInterceptor = new DefaultAuthorizationInterceptor(securityConfig.getUsername(), securityConfig.getPassword());
return defaultAuthorizationInterceptor;
}
private RestTemplate createRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(StandardCharsets.UTF_8) {
{
setSupportedMediaTypes(Collections.singletonList(MediaType.ALL));
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
});
return restTemplate;
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
String web = properties.getWeb();
@ -662,8 +415,9 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer, WebSocketCon
WebSocketSessionManager.setMagicNotifyService(magicNotifyService);
if (web != null && !registerWebsocket) {
registerWebsocket = true;
MagicWebSocketDispatcher dispatcher = new MagicWebSocketDispatcher(properties.getClusterConfig().getInstanceId(), magicNotifyService, Arrays.asList(
MagicWebSocketDispatcher dispatcher = new MagicWebSocketDispatcher(properties.getInstanceId(), magicNotifyService, Arrays.asList(
new MagicDebugHandler(),
new MagicCoordinationHandler(),
new MagicWorkbenchHandler(authorizationInterceptorProvider.getIfAvailable(this::createAuthorizationInterceptor))
));
WebSocketHandlerRegistration registration = webSocketHandlerRegistry.addHandler(dispatcher, web + "/console");

View File

@ -0,0 +1,74 @@
package org.ssssssss.magicapi.spring.boot.starter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.ssssssss.magicapi.core.config.MagicAPIProperties;
import org.ssssssss.magicapi.core.interceptor.DefaultResultProvider;
import org.ssssssss.magicapi.core.interceptor.ResultProvider;
import org.ssssssss.magicapi.core.service.impl.ApiInfoMagicResourceStorage;
import org.ssssssss.magicapi.core.service.impl.RequestMagicDynamicRegistry;
import org.ssssssss.magicapi.datasource.model.MagicDynamicDataSource;
import org.ssssssss.magicapi.datasource.service.DataSourceInfoMagicResourceStorage;
import org.ssssssss.magicapi.datasource.service.DataSourceMagicDynamicRegistry;
import org.ssssssss.magicapi.function.service.FunctionInfoMagicResourceStorage;
import org.ssssssss.magicapi.function.service.FunctionMagicDynamicRegistry;
import org.ssssssss.magicapi.utils.Mapping;
@Configuration
@AutoConfigureAfter(MagicModuleConfiguration.class)
public class MagicDynamicRegistryConfiguration {
private final MagicAPIProperties properties;
@Autowired
@Lazy
private RequestMappingHandlerMapping requestMappingHandlerMapping;
public MagicDynamicRegistryConfiguration(MagicAPIProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean
public ApiInfoMagicResourceStorage apiInfoMagicResourceStorage() {
return new ApiInfoMagicResourceStorage();
}
@Bean
@ConditionalOnMissingBean
public RequestMagicDynamicRegistry magicRequestMagicDynamicRegistry(ApiInfoMagicResourceStorage apiInfoMagicResourceStorage) throws NoSuchMethodException {
return new RequestMagicDynamicRegistry(apiInfoMagicResourceStorage, Mapping.create(requestMappingHandlerMapping, properties.getWeb(), properties.getPrefix()), properties.isAllowOverride());
}
@Bean
@ConditionalOnMissingBean
public FunctionInfoMagicResourceStorage functionInfoMagicResourceStorage() {
return new FunctionInfoMagicResourceStorage();
}
@Bean
@ConditionalOnMissingBean
public FunctionMagicDynamicRegistry functionMagicDynamicRegistry(FunctionInfoMagicResourceStorage functionInfoMagicResourceStorage) {
return new FunctionMagicDynamicRegistry(functionInfoMagicResourceStorage);
}
@Bean
@ConditionalOnMissingBean
public DataSourceInfoMagicResourceStorage dataSourceInfoMagicResourceStorage() {
return new DataSourceInfoMagicResourceStorage();
}
@Bean
@ConditionalOnMissingBean
public DataSourceMagicDynamicRegistry dataSourceMagicDynamicRegistry(DataSourceInfoMagicResourceStorage dataSourceInfoMagicResourceStorage, MagicDynamicDataSource magicDynamicDataSource) {
return new DataSourceMagicDynamicRegistry(dataSourceInfoMagicResourceStorage, magicDynamicDataSource);
}
}

View File

@ -0,0 +1,226 @@
package org.ssssssss.magicapi.spring.boot.starter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartResolver;
import org.ssssssss.magicapi.core.config.Cache;
import org.ssssssss.magicapi.core.config.MagicAPIProperties;
import org.ssssssss.magicapi.core.config.Page;
import org.ssssssss.magicapi.core.interceptor.DefaultResultProvider;
import org.ssssssss.magicapi.core.interceptor.ResultProvider;
import org.ssssssss.magicapi.core.model.Options;
import org.ssssssss.magicapi.datasource.model.MagicDynamicDataSource;
import org.ssssssss.magicapi.jsr223.JSR223LanguageProvider;
import org.ssssssss.magicapi.modules.db.ColumnMapperAdapter;
import org.ssssssss.magicapi.modules.db.SQLModule;
import org.ssssssss.magicapi.modules.db.cache.DefaultSqlCache;
import org.ssssssss.magicapi.modules.db.cache.SqlCache;
import org.ssssssss.magicapi.modules.db.dialect.Dialect;
import org.ssssssss.magicapi.modules.db.dialect.DialectAdapter;
import org.ssssssss.magicapi.modules.db.inteceptor.DefaultSqlInterceptor;
import org.ssssssss.magicapi.modules.db.inteceptor.NamedTableInterceptor;
import org.ssssssss.magicapi.modules.db.inteceptor.SQLInterceptor;
import org.ssssssss.magicapi.modules.db.provider.ColumnMapperProvider;
import org.ssssssss.magicapi.modules.db.provider.DefaultPageProvider;
import org.ssssssss.magicapi.modules.db.provider.PageProvider;
import org.ssssssss.magicapi.modules.http.HttpModule;
import org.ssssssss.magicapi.modules.servlet.RequestModule;
import org.ssssssss.magicapi.modules.servlet.ResponseModule;
import org.ssssssss.magicapi.modules.spring.EnvModule;
import org.ssssssss.script.MagicResourceLoader;
import org.ssssssss.script.functions.DynamicModuleImport;
import javax.sql.DataSource;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class MagicModuleConfiguration {
private static final Logger logger = LoggerFactory.getLogger(MagicModuleConfiguration.class);
private final MagicAPIProperties properties;
/**
* SQL拦截器
*/
private final ObjectProvider<List<SQLInterceptor>> sqlInterceptorsProvider;
/**
* 单表API拦截器
*/
private final ObjectProvider<List<NamedTableInterceptor>> namedTableInterceptorsProvider;
/**
* 自定义的方言
*/
private final ObjectProvider<List<Dialect>> dialectsProvider;
/**
* 自定义的列名转换
*/
private final ObjectProvider<List<ColumnMapperProvider>> columnMapperProvidersProvider;
private final Environment environment;
@Autowired(required = false)
private MultipartResolver multipartResolver;
public MagicModuleConfiguration(MagicAPIProperties properties,
ObjectProvider<List<SQLInterceptor>> sqlInterceptorsProvider,
ObjectProvider<List<NamedTableInterceptor>> namedTableInterceptorsProvider,
ObjectProvider<List<Dialect>> dialectsProvider,
ObjectProvider<List<ColumnMapperProvider>> columnMapperProvidersProvider,
Environment environment) {
this.properties = properties;
this.sqlInterceptorsProvider = sqlInterceptorsProvider;
this.namedTableInterceptorsProvider = namedTableInterceptorsProvider;
this.dialectsProvider = dialectsProvider;
this.columnMapperProvidersProvider = columnMapperProvidersProvider;
this.environment = environment;
}
/**
* 注入动态数据源
*/
@Bean
@ConditionalOnMissingBean(MagicDynamicDataSource.class)
public MagicDynamicDataSource magicDynamicDataSource(@Autowired(required = false) DataSource dataSource) {
MagicDynamicDataSource dynamicDataSource = new MagicDynamicDataSource();
if (dataSource != null) {
dynamicDataSource.put(dataSource);
} else {
logger.warn("当前数据源未配置");
}
return dynamicDataSource;
}
@Bean
@ConditionalOnMissingBean(PageProvider.class)
public PageProvider pageProvider() {
Page pageConfig = properties.getPage();
logger.info("未找到分页实现,采用默认分页实现,分页配置:(页码={},页大小={},默认首页={},默认页大小={})", pageConfig.getPage(), pageConfig.getSize(), pageConfig.getDefaultPage(), pageConfig.getDefaultSize());
return new DefaultPageProvider(pageConfig.getPage(), pageConfig.getSize(), pageConfig.getDefaultPage(), pageConfig.getDefaultSize());
}
/**
* 注入SQL缓存实现
*/
@Bean
@ConditionalOnMissingBean(SqlCache.class)
public SqlCache sqlCache() {
Cache cacheConfig = properties.getCache();
logger.info("未找到SQL缓存实现采用默认缓存实现(LRU+TTL),缓存配置:(容量={},TTL={})", cacheConfig.getCapacity(), cacheConfig.getTtl());
return new DefaultSqlCache(cacheConfig.getCapacity(), cacheConfig.getTtl());
}
/**
* 注入数据库查询模块
*/
@Bean
@ConditionalOnBean({MagicDynamicDataSource.class})
public SQLModule magicSqlModule(MagicDynamicDataSource dynamicDataSource,
ResultProvider resultProvider,
PageProvider pageProvider,
SqlCache sqlCache) {
SQLModule sqlModule = new SQLModule(dynamicDataSource);
if (!dynamicDataSource.isEmpty()) {
sqlModule.setDataSourceNode(dynamicDataSource.getDataSource());
}
sqlModule.setResultProvider(resultProvider);
sqlModule.setPageProvider(pageProvider);
List<SQLInterceptor> sqlInterceptors = sqlInterceptorsProvider.getIfAvailable(ArrayList::new);
if (properties.isShowSql()) {
sqlInterceptors.add(new DefaultSqlInterceptor());
}
sqlModule.setSqlInterceptors(sqlInterceptors);
sqlModule.setNamedTableInterceptors(namedTableInterceptorsProvider.getIfAvailable(Collections::emptyList));
ColumnMapperAdapter columnMapperAdapter = new ColumnMapperAdapter();
this.columnMapperProvidersProvider.getIfAvailable(Collections::emptyList).stream().filter(mapperProvider -> !"default".equals(mapperProvider.name())).forEach(columnMapperAdapter::add);
columnMapperAdapter.setDefault(properties.getSqlColumnCase());
sqlModule.setColumnMapperProvider(columnMapperAdapter);
sqlModule.setColumnMapRowMapper(columnMapperAdapter.getDefaultColumnMapRowMapper());
sqlModule.setRowMapColumnMapper(columnMapperAdapter.getDefaultRowMapColumnMapper());
sqlModule.setSqlCache(sqlCache);
DialectAdapter dialectAdapter = new DialectAdapter();
dialectsProvider.getIfAvailable(Collections::emptyList).forEach(dialectAdapter::add);
sqlModule.setDialectAdapter(dialectAdapter);
sqlModule.setLogicDeleteColumn(properties.getCrud().getLogicDeleteColumn());
sqlModule.setLogicDeleteValue(properties.getCrud().getLogicDeleteValue());
MagicResourceLoader.addModule("db", new DynamicModuleImport(SQLModule.class, context -> {
String dataSourceKey = context.getString(Options.DEFAULT_DATA_SOURCE.getValue());
if (StringUtils.isEmpty(dataSourceKey)) return sqlModule;
SQLModule newSqlModule = sqlModule.cloneSQLModule();
newSqlModule.setDataSourceNode(dynamicDataSource.getDataSource(dataSourceKey));
return newSqlModule;
}));
return sqlModule;
}
@Bean
public JSR223LanguageProvider jsr223LanguageProvider() {
return new JSR223LanguageProvider();
}
@Bean
@ConditionalOnMissingBean(HttpModule.class)
public HttpModule magicHttpModule() {
return new HttpModule(createRestTemplate());
}
@Bean
@ConditionalOnMissingBean
public EnvModule magicEnvModule(){
return new EnvModule(environment);
}
@Bean
@ConditionalOnMissingBean
public RequestModule magicRequestModule(){
return new RequestModule(multipartResolver);
}
/**
* 注入结果构建方法
*/
@Bean
@ConditionalOnMissingBean(ResultProvider.class)
public ResultProvider resultProvider() {
return new DefaultResultProvider(properties.getResponse());
}
@Bean
@ConditionalOnMissingBean
public ResponseModule magicResponseModule(ResultProvider resultProvider){
return new ResponseModule(resultProvider);
}
private RestTemplate createRestTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().add(new StringHttpMessageConverter(StandardCharsets.UTF_8) {
{
setSupportedMediaTypes(Collections.singletonList(MediaType.ALL));
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
});
return restTemplate;
}
}

View File

@ -1,33 +0,0 @@
{
"groups": [
{
"sourceType": "org.ssssssss.magicapi.spring.boot.starter.MagicAPIProperties",
"name": "magic-api",
"type": "org.ssssssss.magicapi.spring.boot.starter.MagicAPIProperties"
},
{
"sourceType": "org.ssssssss.magicapi.spring.boot.starter.MagicAPIProperties",
"name": "page-config",
"sourceMethod": "getPageConfig()",
"type": "org.ssssssss.magicapi.spring.boot.starter.PageConfig"
},
{
"sourceType": "org.ssssssss.magicapi.spring.boot.starter.MagicAPIProperties",
"name": "cache-config",
"sourceMethod": "getCacheConfig()",
"type": "org.ssssssss.magicapi.spring.boot.starter.CacheConfig"
},
{
"sourceType": "org.ssssssss.magicapi.spring.boot.starter.MagicAPIProperties",
"name": "debug-config",
"sourceMethod": "getDebugConfig()",
"type": "org.ssssssss.magicapi.spring.boot.starter.DebugConfig"
},
{
"sourceType": "org.ssssssss.magicapi.spring.boot.starter.MagicAPIProperties",
"name": "crud-config",
"sourceMethod": "getCrudConfig()",
"type": "org.ssssssss.magicapi.spring.boot.starter.CrudConfig"
}
]
}

View File

@ -6,7 +6,7 @@
<parent>
<groupId>org.ssssssss</groupId>
<artifactId>magic-api-parent</artifactId>
<version>1.7.5</version>
<version>2.0.0-beta.1</version>
</parent>
<artifactId>magic-api</artifactId>
<packaging>jar</packaging>
@ -26,20 +26,9 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
@ -71,5 +60,10 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@ -1,4 +1,4 @@
package org.ssssssss.magicapi.model;
package org.ssssssss.magicapi.backup.model;
/**
* 备份记录
@ -34,7 +34,7 @@ public class Backup {
/**
* 备份内容
*/
private String content;
private byte[] content;
/**
* 操作人取用户名空为系统记录
@ -45,7 +45,7 @@ public class Backup {
public Backup() {
}
public Backup(String id, String type, String name, String content) {
public Backup(String id, String type, String name, byte[] content) {
this.id = id;
this.type = type;
this.name = name;
@ -84,11 +84,11 @@ public class Backup {
this.id = id;
}
public String getContent() {
public byte[] getContent() {
return content;
}
public void setContent(String content) {
public void setContent(byte[] content) {
this.content = content;
}

View File

@ -1,8 +1,8 @@
package org.ssssssss.magicapi.provider;
package org.ssssssss.magicapi.backup.service;
import org.ssssssss.magicapi.model.*;
import org.ssssssss.magicapi.utils.JsonUtils;
import org.ssssssss.magicapi.backup.model.Backup;
import java.io.IOException;
import java.util.List;
/**
@ -14,33 +14,6 @@ public interface MagicBackupService {
int FETCH_SIZE = 100;
/**
* 备份接口
*
* @param apiInfo 接口信息
*/
default void backup(ApiInfo apiInfo) {
doBackup(new Backup(apiInfo.getId(), Constants.PATH_API, apiInfo.getName(), JsonUtils.toJsonString(apiInfo)));
}
/**
* 备份函数
*
* @param functionInfo 函数信息
*/
default void backup(FunctionInfo functionInfo) {
doBackup(new Backup(functionInfo.getId(), Constants.PATH_FUNCTION, functionInfo.getName(), JsonUtils.toJsonString(functionInfo)));
}
/**
* 备份数据源
*
* @param dataSourceInfo 数据源信息
*/
default void backup(DataSourceInfo dataSourceInfo) {
doBackup(new Backup(dataSourceInfo.getId(), Constants.PATH_DATASOURCE, dataSourceInfo.get("name"), JsonUtils.toJsonString(dataSourceInfo)));
}
/**
* 执行备份动作
*
@ -48,6 +21,8 @@ public interface MagicBackupService {
*/
void doBackup(Backup backup);
void doBackupAll(String name, String createBy) throws IOException;
/**
* 根据时间戳查询最近的 FETCH_SIZE 条记录
*
@ -85,14 +60,6 @@ public interface MagicBackupService {
*/
long removeBackup(String id);
/**
* 删除一组备份信息
*
* @param idList 对象ID集合
* @return 返回删除的记录数
*/
long removeBackup(List<String> idList);
/**
* 根据13位时间戳删除备份记录清除小于该值的记录
*

View File

@ -0,0 +1,161 @@
package org.ssssssss.magicapi.backup.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.event.EventListener;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
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.backup.model.Backup;
import org.ssssssss.magicapi.core.model.Group;
import org.ssssssss.magicapi.core.model.MagicEntity;
import org.ssssssss.magicapi.utils.JsonUtils;
import org.ssssssss.magicapi.utils.WebUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
/**
* 数据库备份实现
*
* @author mxd
*/
public class MagicDatabaseBackupService implements MagicBackupService {
private final static String DEFAULT_COLUMNS = "id,create_date,tag,type,name,create_by";
private final JdbcTemplate template;
private final String INSERT_SQL;
private final String FIND_BY_ID;
private final String FIND_BY_TAG;
private final String FIND_BY_TIMESTAMP;
private final String FIND_BY_ID_AND_TIMESTAMP;
private final String DELETE_BY_ID;
private final String DELETE_BY_TIMESTAMP;
private final BeanPropertyRowMapper<Backup> rowMapper = new BeanPropertyRowMapper<>(Backup.class);
private static final Logger logger = LoggerFactory.getLogger(MagicDatabaseBackupService.class);
public MagicDatabaseBackupService(JdbcTemplate template, String tableName) {
this.template = template;
this.template.setMaxRows(FETCH_SIZE);
this.INSERT_SQL = String.format("insert into %s(%s,content) values(?,?,?,?,?,?,?)", tableName, DEFAULT_COLUMNS);
this.FIND_BY_ID = String.format("select %s from %s where id = ? order by create_date desc", DEFAULT_COLUMNS, tableName);
this.DELETE_BY_ID = String.format("delete from %s where id = ?", tableName);
this.FIND_BY_TAG = String.format("select %s from %s where tag = ? order by create_date desc", DEFAULT_COLUMNS, tableName);
this.FIND_BY_TIMESTAMP = String.format("select %s from %s where create_date < ? order by create_date desc", DEFAULT_COLUMNS, tableName);
this.DELETE_BY_TIMESTAMP = String.format("delete from %s where create_date < ?", tableName);
this.FIND_BY_ID_AND_TIMESTAMP = String.format("select * from %s where id = ? and create_date = ?", tableName);
}
@Override
public void doBackupAll(String name, String createBy) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
MagicConfiguration.getMagicResourceService().export(null, null, baos);
Backup backup = new Backup();
backup.setId("full");
backup.setType("full");
backup.setName(name);
backup.setCreateBy(createBy);
backup.setContent(baos.toByteArray());
doBackup(backup);
}
@Override
public void doBackup(Backup backup) {
try {
if (backup.getCreateDate() == 0) {
backup.setCreateDate(System.currentTimeMillis());
}
if (backup.getCreateBy() == null) {
backup.setCreateBy(WebUtils.currentUserName());
}
template.update(INSERT_SQL, backup.getId(), backup.getCreateDate(), backup.getTag(), backup.getType(), backup.getName(), backup.getCreateBy(), backup.getContent());
} catch (Exception e) {
logger.warn("备份失败", e);
}
}
@Override
public List<Backup> backupList(long timestamp) {
return template.query(FIND_BY_TIMESTAMP, rowMapper, timestamp);
}
@Override
public List<Backup> backupById(String id) {
return template.query(FIND_BY_ID, rowMapper, id);
}
@Override
public Backup backupInfo(String id, long timestamp) {
return template.queryForObject(FIND_BY_ID_AND_TIMESTAMP, rowMapper, id, timestamp);
}
@Override
public List<Backup> backupByTag(String tag) {
return template.query(FIND_BY_TAG, rowMapper, tag);
}
@Override
public long removeBackup(String id) {
return template.update(DELETE_BY_ID, id);
}
@Override
public long removeBackupByTimestamp(long timestamp) {
try {
return template.update(DELETE_BY_TIMESTAMP, timestamp);
} catch (Exception e) {
logger.warn("删除备份失败", e);
return -1;
}
}
@EventListener(condition = "#event.source != T(org.ssssssss.magicapi.core.config.Constants).EVENT_SOURCE_NOTIFY")
public void onFileEvent(FileEvent event) {
switch (event.getAction()) {
case SAVE:
case CREATE:
case MOVE:
break;
default:
return;
}
MagicEntity entity = event.getEntity();
doBackup(entity.getId(), JsonUtils.toJsonBytes(entity), entity.getName(), event.getType());
}
@EventListener(condition = "#event.source != T(org.ssssssss.magicapi.core.config.Constants).EVENT_SOURCE_NOTIFY")
public void onFolderEvent(GroupEvent event) {
switch (event.getAction()) {
case SAVE:
case CREATE:
case MOVE:
break;
default:
return;
}
Group group = event.getGroup();
doBackup(group.getId(), JsonUtils.toJsonBytes(group), group.getName(), group.getType() + "-group");
}
private void doBackup(String id, byte[] content, String name, String type) {
Backup backup = new Backup();
backup.setName(name);
backup.setId(id);
backup.setContent(content);
backup.setType(type);
doBackup(backup);
}
}

View File

@ -0,0 +1,84 @@
package org.ssssssss.magicapi.backup.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.ssssssss.magicapi.backup.model.Backup;
import org.ssssssss.magicapi.core.config.Constants;
import org.ssssssss.magicapi.core.web.MagicController;
import org.ssssssss.magicapi.core.web.MagicExceptionHandler;
import org.ssssssss.magicapi.core.config.MagicConfiguration;
import org.ssssssss.magicapi.core.model.*;
import org.ssssssss.magicapi.backup.service.MagicBackupService;
import org.ssssssss.magicapi.core.service.MagicDynamicRegistry;
import org.ssssssss.magicapi.utils.JsonUtils;
import org.ssssssss.magicapi.utils.WebUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
public class MagicBackupController extends MagicController implements MagicExceptionHandler {
private final MagicBackupService service;
public MagicBackupController(MagicConfiguration configuration) {
super(configuration);
this.service = configuration.getMagicBackupService();
}
@GetMapping("/backups")
@ResponseBody
public JsonBean<List<Backup>> backups(Long timestamp) {
if(service == null){
return new JsonBean<>(Collections.emptyList());
}
return new JsonBean<>(service.backupList(timestamp == null ? System.currentTimeMillis() : timestamp));
}
@GetMapping("/backup/rollback")
@ResponseBody
public JsonBean<Boolean> rollback(String id, Long timestamp) throws IOException {
notNull(service, BACKUP_NOT_ENABLED);
Backup backup = service.backupInfo(id, timestamp);
if("full".equals(id)){
service.doBackupAll("还原全量备份前,系统自动全量备份", WebUtils.currentUserName());
configuration.getMagicAPIService().upload(new ByteArrayInputStream(backup.getContent()), Constants.UPLOAD_MODE_FULL);
return new JsonBean<>(true);
}
if(backup.getType().endsWith("-group")){
Group group = JsonUtils.readValue(backup.getContent(), Group.class);
return new JsonBean<>(MagicConfiguration.getMagicResourceService().saveGroup(group));
}
MagicEntity entity = configuration.getMagicDynamicRegistries().stream()
.map(MagicDynamicRegistry::getMagicResourceStorage)
.filter(it -> it.folder().equals(backup.getType()))
.map(it -> it.read(backup.getContent()))
.findFirst()
.orElse(null);
if(entity != null){
return new JsonBean<>(MagicConfiguration.getMagicResourceService().saveFile(entity));
}
return new JsonBean<>(false);
}
@GetMapping("/backup")
@ResponseBody
public JsonBean<String> backup(Long timestamp, String id) {
notNull(service, BACKUP_NOT_ENABLED);
notBlank(id, PARAMETER_INVALID);
notNull(timestamp, PARAMETER_INVALID);
Backup backup = service.backupInfo(id, timestamp);
MagicEntity entity = JsonUtils.readValue(backup.getContent(), MagicEntity.class);
return new JsonBean<>(entity == null ? null : entity.getScript());
}
@PostMapping("/backup/full")
@ResponseBody
public JsonBean<Boolean> doBackup() throws IOException {
notNull(service, BACKUP_NOT_ENABLED);
service.doBackupAll("主动全量备份", WebUtils.currentUserName());
return new JsonBean<>(true);
}
}

View File

@ -1,222 +0,0 @@
package org.ssssssss.magicapi.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.ssssssss.magicapi.model.FunctionInfo;
import org.ssssssss.magicapi.model.Group;
import org.ssssssss.magicapi.model.Parameter;
import org.ssssssss.magicapi.model.TreeNode;
import org.ssssssss.magicapi.provider.FunctionServiceProvider;
import org.ssssssss.magicapi.provider.GroupServiceProvider;
import org.ssssssss.magicapi.script.ScriptManager;
import org.ssssssss.magicapi.utils.PathUtils;
import org.ssssssss.script.MagicResourceLoader;
import org.ssssssss.script.MagicScriptContext;
import org.ssssssss.script.exception.MagicExitException;
import org.ssssssss.script.runtime.ExitValue;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 函数映射管理
*
* @author mxd
*/
public class MagicFunctionManager {
private static final Logger logger = LoggerFactory.getLogger(MagicFunctionManager.class);
private static final Map<String, FunctionInfo> MAPPINGS = new ConcurrentHashMap<>();
private final GroupServiceProvider groupServiceProvider;
private final FunctionServiceProvider functionServiceProvider;
private TreeNode<Group> groups;
public MagicFunctionManager(GroupServiceProvider groupServiceProvider, FunctionServiceProvider functionServiceProvider) {
this.groupServiceProvider = groupServiceProvider;
this.functionServiceProvider = functionServiceProvider;
}
public void registerFunctionLoader() {
MagicResourceLoader.addFunctionLoader((context, path) -> {
FunctionInfo info = MAPPINGS.get(path);
if (info != null) {
String scriptName = groupServiceProvider.getScriptName(info.getGroupId(), info.getName(), info.getPath());
List<Parameter> parameters = info.getParameters();
return (Function<Object[], Object>) objects -> {
MagicScriptContext functionContext = new MagicScriptContext(context.getRootVariables());
functionContext.setScriptName(scriptName);
if (objects != null) {
for (int i = 0, len = objects.length, size = parameters.size(); i < len && i < size; i++) {
functionContext.set(parameters.get(i).getName(), objects[i]);
}
}
Object value = ScriptManager.executeScript(info.getScript(), functionContext);
if (value instanceof ExitValue) {
throw new MagicExitException((ExitValue) value);
}
return value;
};
}
return null;
});
}
/**
* 加载所有分组
*/
public synchronized void loadGroup() {
groups = groupServiceProvider.functionGroupTree();
}
public void registerAllFunction() {
loadGroup();
functionServiceProvider.listWithScript().stream()
.filter(it -> groupServiceProvider.getFullPath(it.getGroupId()) != null)
.forEach(this::register);
}
public boolean hasRegister(FunctionInfo info) {
String path = PathUtils.replaceSlash(Objects.toString(groupServiceProvider.getFullPath(info.getGroupId()), "") + "/" + info.getPath());
FunctionInfo functionInfo = MAPPINGS.get(path);
return functionInfo != null && !Objects.equals(info.getId(), functionInfo.getId());
}
public boolean hasRegister(Set<String> paths) {
return paths.stream().anyMatch(MAPPINGS::containsKey);
}
/**
* 函数移动
*/
public boolean move(String id, String groupId) {
FunctionInfo info = MAPPINGS.get(id);
if (info == null) {
return false;
}
String path = Objects.toString(groupServiceProvider.getFullPath(groupId), "");
FunctionInfo functionInfo = MAPPINGS.get(PathUtils.replaceSlash(path + "/" + info.getPath()));
if (functionInfo != null && !Objects.equals(functionInfo.getId(), id)) {
return false;
}
unregister(id);
info.setGroupId(groupId);
register(info);
return true;
}
public void register(FunctionInfo functionInfo) {
if (functionInfo == null) {
return;
}
FunctionInfo oldFunctionInfo = MAPPINGS.get(functionInfo.getId());
if (oldFunctionInfo != null) {
// 完全一致时不用注册
if (functionInfo.equals(oldFunctionInfo)) {
return;
}
// 如果路径不一致则需要取消注册
if (!Objects.equals(functionInfo.getPath(), oldFunctionInfo.getPath())) {
unregister(functionInfo.getId());
}
}
String path = Objects.toString(groupServiceProvider.getFullPath(functionInfo.getGroupId()), "");
MAPPINGS.put(functionInfo.getId(), functionInfo);
path = PathUtils.replaceSlash(path + "/" + functionInfo.getPath());
functionInfo.setMappingPath(path);
MAPPINGS.put(path, functionInfo);
logger.info("注册函数:[{}:{}]", functionInfo.getName(), path);
}
public List<FunctionInfo> getFunctionInfos() {
return MAPPINGS.values().stream().distinct().collect(Collectors.toList());
}
public FunctionInfo getFunctionInfo(String path) {
return MAPPINGS.get(path);
}
private boolean hasConflict(TreeNode<Group> group, String newPath) {
// 获取要移动的接口
List<FunctionInfo> infos = MAPPINGS.values().stream()
.filter(info -> Objects.equals(info.getGroupId(), group.getNode().getId()))
.distinct()
.collect(Collectors.toList());
// 判断是否有冲突
for (FunctionInfo info : infos) {
if (MAPPINGS.containsKey(PathUtils.replaceSlash(newPath + "/" + info.getPath()))) {
return true;
}
}
for (TreeNode<Group> child : group.getChildren()) {
if (hasConflict(child, newPath + "/" + Objects.toString(child.getNode().getPath(), ""))) {
return true;
}
}
return false;
}
public TreeNode<Group> findGroupTree(String groupId) {
return groups.findTreeNode(it -> it.getId().equals(groupId));
}
public boolean checkGroup(Group group) {
TreeNode<Group> oldTree = groups.findTreeNode((item) -> item.getId().equals(group.getId()));
// 如果只改了名字则不做任何操作
if (Objects.equals(oldTree.getNode().getParentId(), group.getParentId()) &&
Objects.equals(oldTree.getNode().getPath(), group.getPath())) {
return true;
}
// 新的接口分组路径
String newPath = Objects.toString(groupServiceProvider.getFullPath(group.getParentId()), "");
// 检测冲突
return !hasConflict(oldTree, newPath + "/" + Objects.toString(group.getPath(), ""));
}
private void recurseUpdateGroup(TreeNode<Group> node, boolean updateGroupId) {
MAPPINGS.values().stream()
.filter(info -> Objects.equals(info.getGroupId(), node.getNode().getId()))
.distinct()
.collect(Collectors.toList())
.forEach(info -> {
unregister(info.getId());
if (updateGroupId) {
info.setGroupId(node.getNode().getId());
}
register(info);
});
for (TreeNode<Group> child : node.getChildren()) {
recurseUpdateGroup(child, false);
}
}
public boolean updateGroup(String groupId) {
loadGroup(); // 重新加载分组
TreeNode<Group> groupTreeNode = groups.findTreeNode((item) -> item.getId().equals(groupId));
recurseUpdateGroup(groupTreeNode, true);
return functionServiceProvider.reload(groupId);
}
public void deleteGroup(List<String> groupIds) {
MAPPINGS.values().stream()
.filter(info -> groupIds.contains(info.getGroupId()))
.distinct()
.collect(Collectors.toList())
.forEach(info -> unregister(info.getId()));
// 刷新分组缓存
loadGroup();
}
public void unregister(String id) {
FunctionInfo functionInfo = MAPPINGS.remove(id);
if (functionInfo != null) {
MAPPINGS.remove(functionInfo.getMappingPath());
logger.info("取消注册函数:[{},{}]", functionInfo.getName(), functionInfo.getMappingPath());
}
}
}

View File

@ -1,19 +0,0 @@
package org.ssssssss.magicapi.config;
import org.ssssssss.script.annotation.UnableCall;
/**
* 模块主要用于import指令import时根据模块名获取当前类如<code>import assert</code>;
*
* @author mxd
*/
public interface MagicModule {
/**
* 获取模块名
*
* @return 返回模块名称
*/
@UnableCall
String getModuleName();
}

View File

@ -1,528 +0,0 @@
package org.ssssssss.magicapi.config;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.ssssssss.magicapi.controller.RequestHandler;
import org.ssssssss.magicapi.model.ApiInfo;
import org.ssssssss.magicapi.model.Constants;
import org.ssssssss.magicapi.model.Group;
import org.ssssssss.magicapi.model.TreeNode;
import org.ssssssss.magicapi.provider.ApiServiceProvider;
import org.ssssssss.magicapi.provider.GroupServiceProvider;
import org.ssssssss.magicapi.utils.Mapping;
import org.ssssssss.magicapi.utils.PathUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 请求映射
*
* @author mxd
*/
public class MappingHandlerMapping {
/**
* 已缓存的映射信息
*/
private static final Map<String, MappingNode> MAPPINGS = new ConcurrentHashMap<>();
private static final Logger logger = LoggerFactory.getLogger(MappingHandlerMapping.class);
/**
* 接口分组
*/
private static TreeNode<Group> groups;
/**
* 请求到达时处理的方法
*/
private final Method method = RequestHandler.class.getDeclaredMethod("invoke", HttpServletRequest.class, HttpServletResponse.class, Map.class, Map.class, Map.class);
/**
* 统一接口前缀
*/
private final String prefix;
/**
* 是否覆盖应用接口
*/
private final boolean allowOverride;
/**
* 缓存已映射的接口信息
*/
private final List<ApiInfo> apiInfos = Collections.synchronizedList(new ArrayList<>());
private Mapping mappingHelper;
/**
* 请求处理器
*/
private Object handler;
/**
* 接口信息读取
*/
private ApiServiceProvider magicApiService;
/**
* 分组信息读取
*/
private GroupServiceProvider groupServiceProvider;
public MappingHandlerMapping(String prefix, boolean allowOverride) throws NoSuchMethodException {
this.prefix = prefix;
this.allowOverride = allowOverride;
}
/**
* 根据request获取对应的接口信息
*/
public static ApiInfo getMappingApiInfo(HttpServletRequest request) {
NativeWebRequest webRequest = new ServletWebRequest(request);
// 找到注册的路径
String requestMapping = (String) webRequest.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST);
// 根据请求方法和路径获取接口信息
return getMappingApiInfo(buildMappingKey(request.getMethod(), requestMapping));
}
/**
* 根据绑定的key获取接口信息
*/
private static ApiInfo getMappingApiInfo(String key) {
return MAPPINGS.get(key).getInfo();
}
/**
* 构建缓存map的key
*
* @param requestMethod 请求方法
* @param requestMapping 请求路径
*/
private static String buildMappingKey(String requestMethod, String requestMapping) {
if (StringUtils.isNotBlank(requestMapping) && !requestMapping.startsWith("/")) {
requestMapping = "/" + requestMapping;
}
return Objects.toString(requestMethod, "GET").toUpperCase() + ":" + requestMapping;
}
public static Group findGroup(String groupId) {
TreeNode<Group> node = groups.findTreeNode(it -> it.getId().equals(groupId));
return node != null ? node.getNode() : null;
}
public static List<Group> findGroups(String groupId) {
List<Group> groups = new ArrayList<>();
Group group;
while (!Constants.ROOT_ID.equals(groupId) && (group = MappingHandlerMapping.findGroup(groupId)) != null) {
groups.add(group);
groupId = group.getParentId();
}
return groups;
}
public TreeNode<Group> findGroupTree(String groupId) {
return groups.findTreeNode(it -> it.getId().equals(groupId));
}
public void setRequestMappingHandlerMapping(RequestMappingHandlerMapping requestMappingHandlerMapping) {
this.mappingHelper = Mapping.create(requestMappingHandlerMapping);
}
public void setHandler(Object handler) {
this.handler = handler;
}
public void setMagicApiService(ApiServiceProvider magicApiService) {
this.magicApiService = magicApiService;
}
public void setGroupServiceProvider(GroupServiceProvider groupServiceProvider) {
this.groupServiceProvider = groupServiceProvider;
}
public List<ApiInfo> getApiInfos() {
return apiInfos;
}
/**
* 加载所有分组
*/
public synchronized void loadGroup() {
groups = groupServiceProvider.apiGroupTree();
}
/**
* 注册请求
*/
public void registerAllMapping() {
try {
loadGroup();
List<ApiInfo> list = magicApiService.listWithScript();
if (list != null) {
list = list.stream().filter(it -> groupServiceProvider.getFullPath(it.getGroupId()) != null).collect(Collectors.toList());
for (ApiInfo info : list) {
try {
// 当接口存在时刷新缓存
registerMapping(info, true);
} catch (Exception e) {
logger.error("接口:{}注册失败", info.getName(), e);
}
}
List<String> resistedList = list.stream().map(ApiInfo::getId).collect(Collectors.toList());
Iterator<ApiInfo> iterator = apiInfos.iterator();
while (iterator.hasNext()) {
String oldId = iterator.next().getId();
// 当接口不存在时取消注册接口
if (!resistedList.contains(oldId)) {
unregisterMapping(oldId, false);
iterator.remove();
}
}
}
} catch (Exception e) {
logger.info("注册接口映射失败", e);
}
}
/**
* 根据请求方法和路径获取接口信息
*
* @param method 请求方法
* @param requestMapping 请求路径
*/
public ApiInfo getApiInfo(String method, String requestMapping) {
MappingNode mappingNode = MAPPINGS.get(buildMappingKey(method, concatPath("", requestMapping)));
return mappingNode == null ? null : mappingNode.getInfo();
}
private boolean hasConflict(TreeNode<Group> group, String newPath) {
// 获取要移动的接口
List<ApiInfo> infos = apiInfos.stream().filter(info -> Objects.equals(info.getGroupId(), group.getNode().getId())).collect(Collectors.toList());
// 判断是否有冲突
for (ApiInfo info : infos) {
String path = concatPath(newPath, "/" + info.getPath());
String mappingKey = buildMappingKey(info.getMethod(), path);
MappingNode mappingNode = MAPPINGS.get(mappingKey);
if (mappingNode != null) {
if (mappingNode.getInfo().equals(info)) {
continue;
}
return true;
}
if (!allowOverride) {
Map<RequestMappingInfo, HandlerMethod> handlerMethods = this.mappingHelper.getHandlerMethods();
if (handlerMethods.get(getRequestMapping(info.getMethod(), path)) != null) {
return true;
}
}
}
for (TreeNode<Group> child : group.getChildren()) {
if (hasConflict(child, newPath + "/" + Objects.toString(child.getNode().getPath(), ""))) {
return true;
}
}
return false;
}
/**
* 检测是否允许修改
*/
public boolean checkGroup(Group group) {
TreeNode<Group> oldTree = groups.findTreeNode((item) -> item.getId().equals(group.getId()));
// 如果没移动目录且没改路径则只需要判断名字是否冲突
boolean parentIdEquals = Objects.equals(oldTree.getNode().getParentId(), group.getParentId());
boolean nameEquals = Objects.equals(oldTree.getNode().getName(), group.getName());
if (parentIdEquals && Objects.equals(oldTree.getNode().getPath(), group.getPath())) {
return nameEquals || !groupServiceProvider.exists(group);
}
// 检测名字是否冲突
boolean requiredChecked = (!parentIdEquals || !nameEquals);
if (requiredChecked && groupServiceProvider.exists(group)) {
return false;
}
// 新的接口分组路径
String newPath = groupServiceProvider.getFullPath(group.getParentId());
// 检测冲突
return !hasConflict(oldTree, newPath + "/" + Objects.toString(group.getPath(), ""));
}
public boolean hasRegister(Set<String> paths) {
return paths.stream().anyMatch(MAPPINGS::containsKey);
}
/**
* 删除分组
*/
public void deleteGroup(List<String> groupIds) {
// 找到对应的所有接口
List<ApiInfo> deleteInfos = apiInfos.stream().filter(info -> groupIds.contains(info.getGroupId())).collect(Collectors.toList());
for (ApiInfo info : deleteInfos) {
unregisterMapping(info.getId(), true);
}
// 全部删除
apiInfos.removeAll(deleteInfos);
// 刷新分组缓存
loadGroup();
}
/**
* 修改分组
*/
public boolean updateGroup(String groupId) {
loadGroup(); // 重新加载分组
TreeNode<Group> groupTreeNode = groups.findTreeNode((item) -> item.getId().equals(groupId));
recurseUpdateGroup(groupTreeNode, true);
return magicApiService.reload(groupId);
}
private void recurseUpdateGroup(TreeNode<Group> node, boolean updateGroupId) {
apiInfos.stream().filter(info -> Objects.equals(info.getGroupId(), node.getNode().getId())).forEach(info -> {
unregisterMapping(info.getId(), false);
if (updateGroupId) {
info.setGroupId(node.getNode().getId());
}
registerMapping(info, false);
});
for (TreeNode<Group> child : node.getChildren()) {
recurseUpdateGroup(child, false);
}
}
/**
* 判断是否已注册
*/
public boolean hasRegisterMapping(ApiInfo info) {
if (info.getId() != null) {
MappingNode mappingNode = MAPPINGS.get(info.getId());
ApiInfo oldInfo = mappingNode == null ? null : mappingNode.getInfo();
if (oldInfo != null
&& Objects.equals(oldInfo.getGroupId(), info.getGroupId())
&& Objects.equals(oldInfo.getMethod(), info.getMethod())
&& Objects.equals(oldInfo.getPath(), info.getPath())) {
return false;
}
}
String mappingKey = getMappingKey(info);
if (MAPPINGS.containsKey(mappingKey)) {
return !MAPPINGS.get(mappingKey).getInfo().getId().equals(info.getId());
}
if (!allowOverride) {
Map<RequestMappingInfo, HandlerMethod> handlerMethods = this.mappingHelper.getHandlerMethods();
return handlerMethods.get(getRequestMapping(info)) != null;
}
return false;
}
/**
* 接口移动
*/
public boolean move(String id, String groupId) {
MappingNode mappingNode = MAPPINGS.get(id);
if (mappingNode == null) {
return false;
}
ApiInfo copy = mappingNode.getInfo().copy();
copy.setGroupId(groupId);
if (hasRegisterMapping(copy)) {
return false;
}
unregisterMapping(id, true);
registerMapping(copy, true);
return true;
}
/**
* 注册请求映射
*/
public void registerMapping(ApiInfo info, boolean delete) {
if (info == null) {
return;
}
// 先判断是否已注册如果已注册则先取消注册在进行注册
MappingNode mappingNode = MAPPINGS.get(info.getId());
String newMappingKey = getMappingKey(info);
if (mappingNode != null) {
ApiInfo oldInfo = mappingNode.getInfo();
String oldMappingKey = mappingNode.getMappingKey();
// URL 路径一致时刷新脚本内容即可
if (Objects.equals(oldMappingKey, newMappingKey)) {
if (!info.equals(oldInfo)) {
mappingNode.setInfo(info);
MAPPINGS.get(newMappingKey).setInfo(info);
if (delete) {
refreshCache(info);
}
logger.info("刷新接口:{},{}", info.getName(), newMappingKey);
}
return;
}
// URL不一致时需要取消注册旧接口重新注册新接口
logger.info("取消注册接口:{},{}", oldInfo.getName(), oldMappingKey);
// 取消注册
MAPPINGS.remove(oldMappingKey);
mappingHelper.unregister(getRequestMapping(oldInfo));
}
mappingNode = new MappingNode(info);
mappingNode.setMappingKey(newMappingKey);
// 注册
RequestMappingInfo requestMapping = getRequestMapping(info);
mappingNode.setRequestMappingInfo(requestMapping);
mappingNode.setInfo(info);
// 如果与应用冲突
if (!overrideApplicationMapping(requestMapping)) {
logger.error("接口{},{}与应用冲突,无法注册", info.getName(), newMappingKey);
return;
}
logger.info("注册接口:{},{}", info.getName(), newMappingKey);
MAPPINGS.put(info.getId(), mappingNode);
MAPPINGS.put(newMappingKey, mappingNode);
registerMapping(requestMapping, handler, method);
if (delete) {
// 刷新缓存
refreshCache(info);
}
}
private void refreshCache(ApiInfo info) {
apiInfos.removeIf(i -> i.getId().equalsIgnoreCase(info.getId()));
apiInfos.add(info);
}
private void registerMapping(RequestMappingInfo requestMapping, Object handler, Method method) {
mappingHelper.register(requestMapping, handler, method);
}
/**
* 取消注册请求映射
*/
public void unregisterMapping(String id, boolean delete) {
MappingNode mappingNode = MAPPINGS.remove(id);
if (mappingNode != null) {
ApiInfo info = mappingNode.getInfo();
logger.info("取消注册接口:{}", info.getName());
MAPPINGS.remove(mappingNode.getMappingKey());
mappingHelper.unregister(mappingNode.getRequestMappingInfo());
if (delete) {
// 刷新缓存
apiInfos.removeIf(i -> i.getId().equalsIgnoreCase(info.getId()));
}
}
}
/**
* 根据接口信息获取绑定map的key
*/
private String getMappingKey(ApiInfo info) {
return buildMappingKey(info.getMethod(), getRequestPath(info.getGroupId(), info.getPath()));
}
/**
* 处理前缀
*
* @param groupId 分组ID
* @param path 请求路径
*/
public String getRequestPath(String groupId, String path) {
return concatPath(groupServiceProvider.getFullPath(groupId), path);
}
public void registerController(Object target, String base) {
Method[] methods = target.getClass().getDeclaredMethods();
for (Method method : methods) {
RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
if (requestMapping != null) {
String[] paths = Stream.of(requestMapping.value()).map(value -> base + value).toArray(String[]::new);
mappingHelper.register(mappingHelper.paths(paths).build(), target, method);
}
}
}
private String concatPath(String groupPath, String path) {
path = groupPath + "/" + path;
if (prefix != null) {
path = prefix + "/" + path;
}
path = PathUtils.replaceSlash(path);
if (path.startsWith("/")) {
return path.substring(1);
}
return path;
}
/**
* 覆盖应用接口
*/
private boolean overrideApplicationMapping(RequestMappingInfo requestMapping) {
if (mappingHelper.getHandlerMethods().containsKey(requestMapping)) {
if (!allowOverride) {
// 不允许覆盖
return false;
}
logger.warn("取消注册应用接口:{}", requestMapping);
// 取消注册原接口
mappingHelper.unregister(requestMapping);
}
return true;
}
/**
* 根据接口信息构建 RequestMappingInfo
*/
private RequestMappingInfo getRequestMapping(ApiInfo info) {
return mappingHelper.paths(getRequestPath(info.getGroupId(), info.getPath())).methods(RequestMethod.valueOf(info.getMethod().toUpperCase())).build();
}
/**
* 根据接口信息构建 RequestMappingInfo
*/
private RequestMappingInfo getRequestMapping(String method, String path) {
return mappingHelper.paths(path).methods(RequestMethod.valueOf(method.toUpperCase())).build();
}
static class MappingNode {
private ApiInfo info;
private String mappingKey;
private RequestMappingInfo requestMappingInfo;
public MappingNode(ApiInfo info) {
this.info = info;
}
public ApiInfo getInfo() {
return info;
}
public void setInfo(ApiInfo info) {
this.info = info;
}
public String getMappingKey() {
return mappingKey;
}
public void setMappingKey(String mappingKey) {
this.mappingKey = mappingKey;
}
public RequestMappingInfo getRequestMappingInfo() {
return requestMappingInfo;
}
public void setRequestMappingInfo(RequestMappingInfo requestMappingInfo) {
this.requestMappingInfo = requestMappingInfo;
}
}
}

View File

@ -1,26 +0,0 @@
package org.ssssssss.magicapi.config;
/**
* WebSocket 消息类型
*
* @author mxd
*/
public enum MessageType {
/* S -> C message */
/* 日志消息 */
LOG,
/* 进入断点 */
BREAKPOINT,
/* 请求接口发生异常 */
EXCEPTION,
/* C -> S message */
/* 设置断点 */
SET_BREAKPOINT,
/* 恢复断点 */
RESUME_BREAKPOINT,
/* 设置 Session ID */
SET_SESSION_ID,
/* 登录 */
LOGIN
}

View File

@ -1,133 +0,0 @@
package org.ssssssss.magicapi.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.TextMessage;
import org.ssssssss.magicapi.model.Constants;
import org.ssssssss.magicapi.model.MagicConsoleSession;
import org.ssssssss.magicapi.model.MagicNotify;
import org.ssssssss.magicapi.provider.MagicNotifyService;
import org.ssssssss.magicapi.utils.JsonUtils;
import org.ssssssss.script.MagicScriptDebugContext;
import java.io.IOException;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
/**
* WebSocket Session 管理
*
* @author mxd
*/
public class WebSocketSessionManager {
private static final Logger logger = LoggerFactory.getLogger(WebSocketSessionManager.class);
private static final Map<String, MagicConsoleSession> SESSION = new ConcurrentHashMap<>();
private static MagicNotifyService magicNotifyService;
private static String instanceId;
public static void add(MagicConsoleSession session) {
SESSION.put(session.getId(), session);
}
public static void remove(MagicConsoleSession session) {
if (session.getId() != null) {
remove(session.getId());
}
}
public static void remove(String sessionId) {
SESSION.remove(sessionId);
}
public static void sendToAll(MessageType messageType, Object... values) {
String content = buildMessage(messageType, values);
sendToAll(content);
}
private static void sendToAll(String content) {
SESSION.values().stream().filter(MagicConsoleSession::writeable).forEach(session -> sendBySession(session, content));
sendToOther(null, content);
}
public static void sendBySessionId(String sessionId, MessageType messageType, Object... values) {
MagicConsoleSession session = findSession(sessionId);
String content = buildMessage(messageType, values);
if (session != null && session.writeable()) {
sendBySession(session, content);
} else {
sendToOther(sessionId, content);
}
}
private static void sendToOther(String sessionId, String content) {
if (magicNotifyService != null) {
// 通知其他机器去发送消息
magicNotifyService.sendNotify(new MagicNotify(instanceId, Constants.NOTIFY_WS_S_C, sessionId, content));
}
}
private static String buildMessage(MessageType messageType, Object... values) {
StringBuilder builder = new StringBuilder(messageType.name().toLowerCase());
if (values != null) {
for (int i = 0, len = values.length; i < len; i++) {
builder.append(",");
Object value = values[i];
if (i + 1 < len || value instanceof CharSequence || value instanceof Number) {
builder.append(value);
} else {
builder.append(JsonUtils.toJsonString(value));
}
}
}
return builder.toString();
}
public static void sendBySessionId(String sessionId, String content) {
if (sessionId == null) {
sendToAll(content);
} else {
MagicConsoleSession session = findSession(sessionId);
if (session != null) {
sendBySession(session, content);
}
}
}
public static void sendBySession(MagicConsoleSession session, String content) {
try {
session.getWebSocketSession().sendMessage(new TextMessage(content));
} catch (IOException e) {
logger.error("发送WebSocket消息失败", e);
}
}
public static MagicConsoleSession findSession(String sessionId) {
return SESSION.values().stream()
.filter(it -> Objects.equals(sessionId, it.getSessionId()))
.findFirst()
.orElse(null);
}
public static void setMagicNotifyService(MagicNotifyService magicNotifyService) {
WebSocketSessionManager.magicNotifyService = magicNotifyService;
}
public static void setInstanceId(String instanceId) {
WebSocketSessionManager.instanceId = instanceId;
}
public static void createSession(String sessionId, MagicScriptDebugContext debugContext) {
MagicConsoleSession consoleSession = findSession(sessionId);
if (consoleSession == null) {
consoleSession = new MagicConsoleSession(sessionId, debugContext);
SESSION.put(sessionId, consoleSession);
} else {
consoleSession.setMagicScriptDebugContext(debugContext);
}
}
}

View File

@ -1,154 +0,0 @@
package org.ssssssss.magicapi.controller;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.ssssssss.magicapi.config.MagicConfiguration;
import org.ssssssss.magicapi.config.Valid;
import org.ssssssss.magicapi.interceptor.Authorization;
import org.ssssssss.magicapi.model.ApiInfo;
import org.ssssssss.magicapi.model.Backup;
import org.ssssssss.magicapi.model.Constants;
import org.ssssssss.magicapi.model.JsonBean;
import javax.servlet.http.HttpServletRequest;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
* 接口相关操作
*
* @author mxd
*/
public class MagicAPIController extends MagicController implements MagicExceptionHandler {
public MagicAPIController(MagicConfiguration configuration) {
super(configuration);
}
/**
* 删除接口
*
* @param id 接口ID
*/
@RequestMapping("/delete")
@ResponseBody
@Valid(readonly = false)
public JsonBean<Boolean> delete(HttpServletRequest request, String id) {
ApiInfo apiInfo = getApiInfo(id);
isTrue(allowVisit(request, Authorization.DELETE, apiInfo), PERMISSION_INVALID);
isTrue(!Constants.LOCK.equals(apiInfo.getLock()), RESOURCE_LOCKED);
return new JsonBean<>(magicAPIService.deleteApi(id));
}
/**
* 查询所有接口
*/
@RequestMapping("/list")
@ResponseBody
public JsonBean<List<ApiInfo>> list(HttpServletRequest request) {
return new JsonBean<>(magicAPIService.apiList()
.stream()
.filter(it -> allowVisit(request, Authorization.VIEW, it))
.map(ApiInfo::simple)
.collect(Collectors.toList())
);
}
/**
* 查询接口详情
*
* @param id 接口ID
*/
@RequestMapping("/get")
@ResponseBody
public JsonBean<ApiInfo> get(HttpServletRequest request, String id) {
isTrue(allowVisit(request, Authorization.VIEW, getApiInfo(id)), PERMISSION_INVALID);
return new JsonBean<>(magicAPIService.getApiInfo(id));
}
/**
* 查询历史记录
*
* @param id 接口ID
*/
@RequestMapping("/backups")
@ResponseBody
public JsonBean<List<Backup>> backupList(HttpServletRequest request, String id) {
isTrue(allowVisit(request, Authorization.VIEW, getApiInfo(id)), PERMISSION_INVALID);
return new JsonBean<>(magicBackupService.backupById(id));
}
/**
* 获取历史记录
*
* @param id 接口ID
* @param timestamp 时间点
*/
@RequestMapping("/backup/get")
@ResponseBody
public JsonBean<Backup> backups(HttpServletRequest request, String id, Long timestamp) {
isTrue(allowVisit(request, Authorization.VIEW, getApiInfo(id)), PERMISSION_INVALID);
return new JsonBean<>(magicBackupService.backupInfo(id, timestamp));
}
/**
* 移动接口
*/
@RequestMapping("/api/move")
@ResponseBody
@Valid(readonly = false)
public JsonBean<Boolean> apiMove(HttpServletRequest request, String id, String groupId) {
ApiInfo apiInfo = getApiInfo(id).copy();
// 新的分组ID
apiInfo.setGroupId(groupId);
isTrue(allowVisit(request, Authorization.SAVE, apiInfo), PERMISSION_INVALID);
isTrue(!Constants.LOCK.equals(apiInfo.getLock()), RESOURCE_LOCKED);
return new JsonBean<>(magicAPIService.moveApi(id, groupId));
}
/**
* 保存接口
*/
@RequestMapping("/save")
@ResponseBody
@Valid(readonly = false)
public JsonBean<String> save(HttpServletRequest request, @RequestBody ApiInfo info) {
isTrue(allowVisit(request, Authorization.SAVE, info), PERMISSION_INVALID);
if (StringUtils.isNotBlank(info.getId())) {
ApiInfo oldInfo = getApiInfo(info.getId());
isTrue(!Constants.LOCK.equals(oldInfo.getLock()), RESOURCE_LOCKED);
}
return new JsonBean<>(magicAPIService.saveApi(info));
}
/**
* 锁定接口
*/
@RequestMapping("/lock")
@ResponseBody
@Valid(readonly = false)
public JsonBean<Boolean> lock(HttpServletRequest request, String id) {
isTrue(allowVisit(request, Authorization.LOCK, getApiInfo(id)), PERMISSION_INVALID);
return new JsonBean<>(magicAPIService.lockApi(id));
}
/**
* 解锁接口
*/
@RequestMapping("/unlock")
@ResponseBody
@Valid(readonly = false)
public JsonBean<Boolean> unlock(HttpServletRequest request, String id) {
isTrue(allowVisit(request, Authorization.UNLOCK, getApiInfo(id)), PERMISSION_INVALID);
return new JsonBean<>(magicAPIService.unlockApi(id));
}
private ApiInfo getApiInfo(String id) {
ApiInfo apiInfo = magicAPIService.getApiInfo(id);
notNull(apiInfo, API_NOT_FOUND);
return apiInfo;
}
}

View File

@ -1,86 +0,0 @@
package org.ssssssss.magicapi.controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.ssssssss.magicapi.config.MagicConfiguration;
import org.ssssssss.magicapi.config.Valid;
import org.ssssssss.magicapi.interceptor.Authorization;
import org.ssssssss.magicapi.model.DataSourceInfo;
import org.ssssssss.magicapi.model.JsonBean;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.stream.Collectors;
/**
* 数据源相关操作
*
* @author mxd
*/
public class MagicDataSourceController extends MagicController implements MagicExceptionHandler {
public MagicDataSourceController(MagicConfiguration configuration) {
super(configuration);
}
/**
* 查询数据源列表
*/
@RequestMapping("/datasource/list")
@ResponseBody
public JsonBean<List<DataSourceInfo>> list(HttpServletRequest request) {
return new JsonBean<>(magicAPIService.datasourceList()
.stream()
.filter(it -> allowVisit(request, Authorization.VIEW, it))
.collect(Collectors.toList())
);
}
@RequestMapping("/datasource/test")
@ResponseBody
public JsonBean<String> test(@RequestBody DataSourceInfo properties) {
return new JsonBean<>(magicAPIService.testDataSource(properties));
}
/**
* 保存数据源
*
* @param properties 数据源配置信息
*/
@RequestMapping("/datasource/save")
@Valid(readonly = false)
@ResponseBody
public JsonBean<String> save(HttpServletRequest request, @RequestBody DataSourceInfo properties) {
isTrue(allowVisit(request, Authorization.SAVE, properties), PERMISSION_INVALID);
return new JsonBean<>(magicAPIService.saveDataSource(properties));
}
/**
* 删除数据源
*
* @param id 数据源ID
*/
@RequestMapping("/datasource/delete")
@Valid(readonly = false)
@ResponseBody
public JsonBean<Boolean> delete(HttpServletRequest request, String id) {
DataSourceInfo dataSource = getDataSourceInfo(id);
isTrue(allowVisit(request, Authorization.DELETE, dataSource), PERMISSION_INVALID);
return new JsonBean<>(magicAPIService.deleteDataSource(id));
}
@RequestMapping("/datasource/detail")
@ResponseBody
public JsonBean<DataSourceInfo> detail(HttpServletRequest request, String id) {
DataSourceInfo dataSource = getDataSourceInfo(id);
isTrue(allowVisit(request, Authorization.VIEW, dataSource), PERMISSION_INVALID);
return new JsonBean<>(dataSource);
}
private DataSourceInfo getDataSourceInfo(String id) {
DataSourceInfo dataSource = magicAPIService.getDataSource(id);
notNull(dataSource, DATASOURCE_NOT_FOUND);
return dataSource;
}
}

View File

@ -1,127 +0,0 @@
package org.ssssssss.magicapi.controller;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.ssssssss.magicapi.config.MagicConfiguration;
import org.ssssssss.magicapi.config.Valid;
import org.ssssssss.magicapi.interceptor.Authorization;
import org.ssssssss.magicapi.model.Backup;
import org.ssssssss.magicapi.model.Constants;
import org.ssssssss.magicapi.model.FunctionInfo;
import org.ssssssss.magicapi.model.JsonBean;
import javax.servlet.http.HttpServletRequest;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
* 函数相关操作
*
* @author mxd
*/
public class MagicFunctionController extends MagicController implements MagicExceptionHandler {
public MagicFunctionController(MagicConfiguration configuration) {
super(configuration);
}
@RequestMapping("/function/list")
@ResponseBody
public JsonBean<List<FunctionInfo>> list(HttpServletRequest request) {
return new JsonBean<>(magicAPIService.functionList()
.stream()
.filter(it -> allowVisit(request, Authorization.VIEW, it))
.collect(Collectors.toList())
);
}
@RequestMapping("/function/get")
@ResponseBody
public JsonBean<FunctionInfo> get(HttpServletRequest request, String id) {
isTrue(allowVisit(request, Authorization.VIEW, getFunctionInfo(id)), PERMISSION_INVALID);
return new JsonBean<>(magicAPIService.getFunctionInfo(id));
}
@RequestMapping("/function/backup/get")
@ResponseBody
public JsonBean<Backup> backups(HttpServletRequest request, String id, Long timestamp) {
isTrue(allowVisit(request, Authorization.VIEW, getFunctionInfo(id)), PERMISSION_INVALID);
return new JsonBean<>(magicBackupService.backupInfo(id, timestamp));
}
@RequestMapping("/function/backups")
@ResponseBody
public JsonBean<List<Backup>> backupList(HttpServletRequest request, String id) {
isTrue(allowVisit(request, Authorization.VIEW, getFunctionInfo(id)), PERMISSION_INVALID);
return new JsonBean<>(magicBackupService.backupById(id)
.stream()
.sorted(Comparator.comparing(Backup::getCreateDate).reversed())
.collect(Collectors.toList()));
}
@RequestMapping("/function/move")
@ResponseBody
@Valid(readonly = false)
public JsonBean<Boolean> move(HttpServletRequest request, String id, String groupId) {
FunctionInfo functionInfo = getFunctionInfo(id);
functionInfo.setGroupId(groupId);
isTrue(allowVisit(request, Authorization.SAVE, functionInfo), PERMISSION_INVALID);
isTrue(!Constants.LOCK.equals(functionInfo.getLock()), RESOURCE_LOCKED);
return new JsonBean<>(magicAPIService.moveFunction(id, groupId));
}
@RequestMapping("/function/save")
@ResponseBody
@Valid(readonly = false)
public JsonBean<String> save(HttpServletRequest request, @RequestBody FunctionInfo functionInfo) {
isTrue(allowVisit(request, Authorization.SAVE, functionInfo), PERMISSION_INVALID);
if (StringUtils.isNotBlank(functionInfo.getId())) {
FunctionInfo oldInfo = getFunctionInfo(functionInfo.getId());
isTrue(!Constants.LOCK.equals(oldInfo.getLock()), RESOURCE_LOCKED);
}
return new JsonBean<>(magicAPIService.saveFunction(functionInfo));
}
@RequestMapping("/function/delete")
@ResponseBody
@Valid(readonly = false)
public JsonBean<Boolean> delete(HttpServletRequest request, String id) {
FunctionInfo info = getFunctionInfo(id);
isTrue(allowVisit(request, Authorization.DELETE, info), PERMISSION_INVALID);
isTrue(!Constants.LOCK.equals(info.getLock()), RESOURCE_LOCKED);
return new JsonBean<>(magicAPIService.deleteFunction(id));
}
/**
* 锁定函数
*/
@RequestMapping("/function/lock")
@ResponseBody
@Valid(readonly = false)
public JsonBean<Boolean> lock(HttpServletRequest request, String id) {
isTrue(allowVisit(request, Authorization.LOCK, getFunctionInfo(id)), PERMISSION_INVALID);
return new JsonBean<>(magicAPIService.lockFunction(id));
}
/**
* 解锁函数
*/
@RequestMapping("/function/unlock")
@ResponseBody
@Valid(readonly = false)
public JsonBean<Boolean> unlock(HttpServletRequest request, String id) {
isTrue(allowVisit(request, Authorization.UNLOCK, getFunctionInfo(id)), PERMISSION_INVALID);
return new JsonBean<>(magicAPIService.unlockFunction(id));
}
public FunctionInfo getFunctionInfo(String id) {
FunctionInfo functionInfo = magicAPIService.getFunctionInfo(id);
notNull(functionInfo, FUNCTION_NOT_FOUND);
return functionInfo;
}
}

View File

@ -1,102 +0,0 @@
package org.ssssssss.magicapi.controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.ssssssss.magicapi.adapter.Resource;
import org.ssssssss.magicapi.adapter.resource.FileResource;
import org.ssssssss.magicapi.config.MagicConfiguration;
import org.ssssssss.magicapi.config.Valid;
import org.ssssssss.magicapi.interceptor.Authorization;
import org.ssssssss.magicapi.model.Constants;
import org.ssssssss.magicapi.model.Group;
import org.ssssssss.magicapi.model.JsonBean;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.stream.Collectors;
/**
* 分组相关操作
*
* @author mxd
*/
public class MagicGroupController extends MagicController implements MagicExceptionHandler {
public MagicGroupController(MagicConfiguration configuration) {
super(configuration);
}
/**
* 删除分组
*/
@RequestMapping("/group/delete")
@ResponseBody
@Valid(readonly = false)
public JsonBean<Boolean> deleteGroup(HttpServletRequest request, String groupId) {
Group group = magicAPIService.getGroup(groupId);
notNull(group, GROUP_NOT_FOUND);
isTrue(allowVisit(request, Authorization.DELETE, group), PERMISSION_INVALID);
return new JsonBean<>(magicAPIService.deleteGroup(groupId));
}
/**
* 修改分组
*/
@RequestMapping("/group/update")
@ResponseBody
@Valid(readonly = false)
public synchronized JsonBean<Boolean> groupUpdate(HttpServletRequest request, @RequestBody Group group) {
isTrue(allowVisit(request, Authorization.SAVE, group), PERMISSION_INVALID);
if (magicAPIService.updateGroup(group)) {
return new JsonBean<>(true);
}
return new JsonBean<>(GROUP_CONFLICT);
}
/**
* 查询所有分组
*/
@RequestMapping("/group/list")
@ResponseBody
public JsonBean<List<Group>> groupList(HttpServletRequest request, String type) {
return new JsonBean<>(magicAPIService.groupList(type)
.stream()
.filter(it -> allowVisit(request, Authorization.VIEW, it))
.collect(Collectors.toList())
);
}
/**
* 创建分组
*/
@RequestMapping("/group/create")
@ResponseBody
@Valid(readonly = false)
public JsonBean<String> createGroup(HttpServletRequest request, @RequestBody Group group) {
isTrue(allowVisit(request, Authorization.SAVE, group), PERMISSION_INVALID);
Resource resource = configuration.getWorkspace();
if(resource instanceof FileResource){
isTrue(resource.exists(), FILE_PATH_NOT_EXISTS);
}
return new JsonBean<>(magicAPIService.createGroup(group));
}
/**
* 复制分组
*/
@RequestMapping("/group/copy")
@ResponseBody
@Valid(readonly = false)
public JsonBean<String> copyGroup(HttpServletRequest request, String src, String target) {
Group group = magicAPIService.getGroup(src);
notNull(group, GROUP_NOT_FOUND);
if (!Constants.ROOT_ID.equals(target)) {
Group targetGroup = magicAPIService.getGroup(target);
notNull(targetGroup, GROUP_NOT_FOUND);
isTrue(allowVisit(request, Authorization.SAVE, targetGroup), PERMISSION_INVALID);
}
isTrue(allowVisit(request, Authorization.VIEW, group), PERMISSION_INVALID);
return new JsonBean<>(magicAPIService.copyGroup(src, target));
}
}

View File

@ -1,33 +0,0 @@
package org.ssssssss.magicapi.controller;
import org.ssssssss.magicapi.config.Message;
import org.ssssssss.magicapi.config.MessageType;
import org.ssssssss.magicapi.config.WebSocketSessionManager;
import org.ssssssss.magicapi.exception.MagicLoginException;
import org.ssssssss.magicapi.interceptor.AuthorizationInterceptor;
import org.ssssssss.magicapi.model.MagicConsoleSession;
/**
* UI上其它操作处理
*
* @author mxd
*/
public class MagicWorkbenchHandler {
private final AuthorizationInterceptor authorizationInterceptor;
public MagicWorkbenchHandler(AuthorizationInterceptor authorizationInterceptor) {
this.authorizationInterceptor = authorizationInterceptor;
}
@Message(MessageType.LOGIN)
public void onLogin(MagicConsoleSession session, String token) {
try {
if (!authorizationInterceptor.requireLogin() || authorizationInterceptor.getUserByToken(token) != null) {
WebSocketSessionManager.add(session);
}
} catch (MagicLoginException ignored) {
}
}
}

View File

@ -0,0 +1,19 @@
package org.ssssssss.magicapi.core.annotation;
import java.lang.annotation.*;
/**
* 模块主要用于import指令import时根据模块名获取当前类如<code>import assert</code>;
*
* @author mxd
*/
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MagicModule {
/**
* 模块名
*/
String value();
}

View File

@ -1,4 +1,6 @@
package org.ssssssss.magicapi.config;
package org.ssssssss.magicapi.core.annotation;
import org.ssssssss.magicapi.core.config.MessageType;
import java.lang.annotation.*;

View File

@ -1,6 +1,6 @@
package org.ssssssss.magicapi.config;
package org.ssssssss.magicapi.core.annotation;
import org.ssssssss.magicapi.interceptor.Authorization;
import org.ssssssss.magicapi.core.interceptor.Authorization;
import java.lang.annotation.*;

View File

@ -1,22 +1,17 @@
package org.ssssssss.magicapi.spring.boot.starter;
package org.ssssssss.magicapi.core.config;
/**
* 备份配置
*
* @author mxd
* @since 1.3.5
* @since 2.0.0
*/
public class BackupConfig {
public class Backup {
/**
* 存储类型可选 file database
* 是否启用备份配置默认不启用
*/
private String resourceType = "file";
/**
* 存储位置选择存储为文件时专用
*/
private String location = "/data/magic-api/backup";
private boolean enable = false;
/**
* 保留天数<=0 为不限制
@ -33,22 +28,6 @@ public class BackupConfig {
*/
private String datasource;
public String getResourceType() {
return resourceType;
}
public void setResourceType(String resourceType) {
this.resourceType = resourceType;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public int getMaxHistory() {
return maxHistory;
}
@ -72,4 +51,12 @@ public class BackupConfig {
public void setDatasource(String datasource) {
this.datasource = datasource;
}
public boolean isEnable() {
return enable;
}
public void setEnable(boolean enable) {
this.enable = enable;
}
}

View File

@ -1,11 +1,11 @@
package org.ssssssss.magicapi.spring.boot.starter;
package org.ssssssss.magicapi.core.config;
/**
* 缓存配置
*
* @author mxd
*/
public class CacheConfig {
public class Cache {
/**
* 是否启用缓存

View File

@ -1,10 +1,8 @@
package org.ssssssss.magicapi.model;
package org.ssssssss.magicapi.core.config;
import java.util.Arrays;
import java.util.List;
/**
* magic-api中使用的常量信息
*
* @author mxd
*/
public class Constants {
/**
@ -13,46 +11,11 @@ public class Constants {
public static final String CONST_STRING_TRUE = "true";
/**
* 分组类型: 接口
*/
public static final String GROUP_TYPE_API = "1";
/**
* 分组类型: 函数
*/
public static final String GROUP_TYPE_FUNCTION = "2";
/**
* 接口文件夹名
*/
public static final String PATH_API = "api";
/**
* 函数文件夹名
*/
public static final String PATH_FUNCTION = "function";
/**
* 数据源文件夹名
*/
public static final String PATH_DATASOURCE = "datasource";
/**
* 备份文件夹名
*/
public static final String PATH_BACKUPS = "backups";
/**
* 空值
*/
public static final String EMPTY = "";
/**
* 根节点ID
*/
public static final String ROOT_ID = "0";
/**
* 表达式验证
*/
@ -87,25 +50,31 @@ public class Constants {
* 脚本中header的变量名
*/
public static final String VAR_NAME_HEADER = "header";
/**
* 脚本中query的变量名
*/
public static final String VAR_NAME_QUERY = "query";
/**
/**
* 脚本中RequestBody的变量名
*/
public static final String VAR_NAME_REQUEST_BODY = "body";
/**
* 脚本中RequestBody的变量值字段类型
*/
public static final String VAR_NAME_REQUEST_BODY_VALUE_TYPE_OBJECT = "object";
/**
* 脚本中RequestBody的变量名字段类型
*/
public static final String VAR_NAME_REQUEST_BODY_VALUE_TYPE_ARRAY = "array";
public static final String HEADER_REQUEST_SESSION = "Magic-Request-Session";
public static final String HEADER_REQUEST_SCRIPT_ID = "Magic-Request-Script-Id";
public static final String HEADER_REQUEST_CLIENT_ID = "Magic-Request-Client-Id";
public static final String HEADER_REQUEST_BREAKPOINTS = "Magic-Request-Breakpoints";
@ -115,72 +84,52 @@ public class Constants {
public static final String GROUP_METABASE = "group.json";
public static final String JSON_SUFFIX = ".json";
public static final String UPLOAD_MODE_FULL = "full";
public static final String LOCK = "1";
public static final String UNLOCK = "0";
/**
* 执行成功的message值
*/
public static final String RESPONSE_MESSAGE_SUCCESS = "success";
/**
* 通知新增
*/
public static final int NOTIFY_ACTION_ADD = 1;
/**
* 通知修改
*/
public static final int NOTIFY_ACTION_UPDATE = 2;
/**
* 通知删除
*/
public static final int NOTIFY_ACTION_DELETE = 3;
/**
* 通知更新全部
*/
public static final int NOTIFY_ACTION_ALL = 0;
/**
* 通知接口刷新
*/
public static final int NOTIFY_ACTION_API = 1;
/**
* 通知分组刷新
*/
public static final int NOTIFY_ACTION_GROUP = 2;
/**
* 通知函数刷新
*/
public static final int NOTIFY_ACTION_FUNCTION = 3;
/**
* 通知数据源刷新
*/
public static final int NOTIFY_ACTION_DATASOURCE = 4;
/**
* 通知 C -> S 的WebSocket消息
*/
public static final int NOTIFY_WS_C_S = 100;
/**
* 通知 S -> C 的WebSocket消息
*/
public static final int NOTIFY_WS_S_C = 200;
/**
* 空数组
*/
public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
public static final String ROOT_ID = "0";
public static final String EVENT_TYPE_FILE = "file";
public static final String EVENT_SOURCE_NOTIFY = "notify";
public static final String WEBSOCKET_ATTRIBUTE_FILE_ID = "fileId";
public static final String WEBSOCKET_ATTRIBUTE_USER_ID = "id";
public static final String WEBSOCKET_ATTRIBUTE_USER_NAME = "username";
public static final String WEBSOCKET_ATTRIBUTE_USER_IP = "ip";
public static final String WEBSOCKET_ATTRIBUTE_CLIENT_ID = "cid";
/**
* 执行成功的code值
*/
public static int RESPONSE_CODE_SUCCESS = 1;
/**
* 执行成功的message值
*/
public static final String RESPONSE_MESSAGE_SUCCESS = "success";
/**
* 执行出现异常的code值
*/
public static int RESPONSE_CODE_EXCEPTION = -1;
/**
* 参数验证未通过的code值
*/
public static int RESPONSE_CODE_INVALID = 0;
/**
* 空数组
*/
public static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];
}

View File

@ -1,4 +1,4 @@
package org.ssssssss.magicapi.spring.boot.starter;
package org.ssssssss.magicapi.core.config;
/**
* CRUD 配置
@ -7,7 +7,7 @@ package org.ssssssss.magicapi.spring.boot.starter;
* @date 2021-7-15 09:26:17
* @since 1.3.4
*/
public class CrudConfig {
public class Crud {
/**
* 逻辑删除列
*/

View File

@ -1,11 +1,11 @@
package org.ssssssss.magicapi.spring.boot.starter;
package org.ssssssss.magicapi.core.config;
/**
* Debug配置
*
* @author mxd
*/
public class DebugConfig {
public class Debug {
/**
* 断点超时时间

View File

@ -0,0 +1,100 @@
package org.ssssssss.magicapi.core.config;
import org.apache.commons.lang3.StringUtils;
import org.ssssssss.magicapi.core.exception.InvalidArgumentException;
import org.ssssssss.magicapi.core.model.JsonCode;
public interface JsonCodeConstants {
JsonCode SUCCESS = new JsonCode(1, Constants.RESPONSE_MESSAGE_SUCCESS);
JsonCode IS_READ_ONLY = new JsonCode(-2, "当前为只读模式,无法操作");
JsonCode PERMISSION_INVALID = new JsonCode(-10, "无权限操作.");
JsonCode GROUP_NOT_FOUND = new JsonCode(1001, "找不到分组信息");
JsonCode NOT_SUPPORTED_GROUP_TYPE = new JsonCode(1002, "不支持该分组类型");
JsonCode TARGET_IS_REQUIRED = new JsonCode(1003, "目标网址不能为空");
JsonCode SECRET_KEY_IS_REQUIRED = new JsonCode(1004, "secretKey不能为空");
JsonCode MOVE_NAME_CONFLICT = new JsonCode(1005, "移动后名称会重复,请修改名称后在试。");
JsonCode SRC_GROUP_CONFLICT = new JsonCode(1006, "源对象和分组不能一致");
JsonCode FILE_NOT_FOUND = new JsonCode(1007, "找不到对应文件或分组");
JsonCode RESOURCE_LOCKED = new JsonCode(1008, "当前资源已被锁定,请解锁后在操作。");
JsonCode PATH_CONFLICT = new JsonCode(1009, "该路径已被使用,请换一个路径在试");
JsonCode RESOURCE_PATH_CONFLICT = new JsonCode(1010, "资源中[%s]有冲突,请检查");
JsonCode MOVE_PATH_CONFLICT = new JsonCode(1011, "移动后路径会冲突,请换一个路径在试");
JsonCode REQUEST_METHOD_REQUIRED = new JsonCode(1012, "请求方法不能为空");
JsonCode REQUEST_PATH_REQUIRED = new JsonCode(1013, "请求路径不能为空");
JsonCode FUNCTION_PATH_REQUIRED = new JsonCode(1014, "函数路径不能为空");
JsonCode FILE_PATH_NOT_EXISTS = new JsonCode(1015, "配置的文件路径不存在,请检查");
JsonCode REQUEST_PATH_CONFLICT = new JsonCode(1016, "接口[%s(%s)]与应用冲突,无法注册");
JsonCode SCRIPT_REQUIRED = new JsonCode(1017, "脚本内容不能为空");
JsonCode NAME_REQUIRED = new JsonCode(1018, "名称不能为空");
JsonCode PATH_REQUIRED = new JsonCode(1019, "路径不能为空");
JsonCode DS_URL_REQUIRED = new JsonCode(1020, "jdbcURL不能为空");
JsonCode DS_KEY_REQUIRED = new JsonCode(1021, "key不能为空");
JsonCode DS_KEY_CONFLICT = new JsonCode(1022, "数据源key已被使用请更换后在试");
JsonCode GROUP_ID_REQUIRED = new JsonCode(1023, "请选择分组");
JsonCode CRON_ID_REQUIRED = new JsonCode(1024, "cron表达式不能为空");
JsonCode NAME_INVALID = new JsonCode(1025, "名称不能包含特殊字符,只允许中文、数字、字母以及+_-.()的组合且不能.开头");
JsonCode DATASOURCE_KEY_INVALID = new JsonCode(1026, "数据源Key不能包含特殊字符只允许中文、数字、字母以及_组合");
JsonCode FILE_SAVE_FAILURE = new JsonCode(1027, "保存失败,同一组下分组名称不能重复且不能包含特殊字符。");
JsonCode PARAMETER_INVALID = new JsonCode(1028, "参数验证失败");
JsonCode HEADER_INVALID = new JsonCode(1029, "header验证失败");
JsonCode PATH_VARIABLE_INVALID = new JsonCode(1030, "路径变量验证失败");
JsonCode BODY_INVALID = new JsonCode(1031, "body验证失败");
JsonCode FILE_IS_REQUIRED = new JsonCode(1032, "请上传文件");
JsonCode SIGN_IS_INVALID = new JsonCode(1033, "签名验证失败,请检查秘钥是否正确");
JsonCode BACKUP_NOT_ENABLED = new JsonCode(1034, "未启用备份,无法操作");
JsonCode API_NOT_FOUND = new JsonCode(1035, "找不到接口");
default void notNull(Object value, JsonCode jsonCode) {
if (value == null) {
throw new InvalidArgumentException(jsonCode);
}
}
default void isTrue(boolean value, JsonCode jsonCode) {
if (!value) {
throw new InvalidArgumentException(jsonCode);
}
}
default void notBlank(String value, JsonCode jsonCode) {
isTrue(StringUtils.isNotBlank(value), jsonCode);
}
}

View File

@ -1,13 +1,14 @@
package org.ssssssss.magicapi.spring.boot.starter;
package org.ssssssss.magicapi.core.config;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import org.ssssssss.magicapi.controller.RequestHandler;
import org.ssssssss.magicapi.core.web.RequestHandler;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
/**
* magic-api配置信息
@ -135,51 +136,34 @@ public class MagicAPIProperties {
*/
private boolean persistenceResponseBody = true;
@NestedConfigurationProperty
private SecurityConfig securityConfig = new SecurityConfig();
/**
* 实例ID集群环境下要保证每台机器不同默认启动后随机生成uuid
*/
private String instanceId = UUID.randomUUID().toString();
@NestedConfigurationProperty
private PageConfig pageConfig = new PageConfig();
private Security securityConfig = new Security();
@NestedConfigurationProperty
private CacheConfig cacheConfig = new CacheConfig();
private Page page = new Page();
@NestedConfigurationProperty
private DebugConfig debugConfig = new DebugConfig();
private Cache cache = new Cache();
@NestedConfigurationProperty
private SwaggerConfig swaggerConfig = new SwaggerConfig();
private Debug debug = new Debug();
@NestedConfigurationProperty
private ResourceConfig resource = new ResourceConfig();
private Resource resource = new Resource();
@NestedConfigurationProperty
private ResponseCodeConfig responseCodeConfig = new ResponseCodeConfig();
private ResponseCode responseCode = new ResponseCode();
@NestedConfigurationProperty
private ClusterConfig clusterConfig = new ClusterConfig();
private Crud crud = new Crud();
@NestedConfigurationProperty
private CrudConfig crudConfig = new CrudConfig();
@NestedConfigurationProperty
private BackupConfig backupConfig = new BackupConfig();
public CrudConfig getCrudConfig() {
return crudConfig;
}
public void setCrudConfig(CrudConfig crudConfig) {
this.crudConfig = crudConfig;
}
public String getEditorConfig() {
return editorConfig;
}
public void setEditorConfig(String editorConfig) {
this.editorConfig = editorConfig;
}
private Backup backup = new Backup();
public String getWeb() {
if (StringUtils.isBlank(web)) {
@ -217,44 +201,20 @@ public class MagicAPIProperties {
this.banner = banner;
}
public PageConfig getPageConfig() {
return pageConfig;
public List<String> getAutoImportModuleList() {
return Arrays.asList(autoImportModule.replaceAll("\\s", "").split(","));
}
public void setPageConfig(PageConfig pageConfig) {
this.pageConfig = pageConfig;
public List<String> getAutoImportPackageList() {
if (autoImportPackage == null) {
return Collections.emptyList();
}
return Arrays.asList(autoImportPackage.replaceAll("\\s", "").split(","));
}
public boolean isThrowException() {
return throwException;
}
public void setThrowException(boolean throwException) {
this.throwException = throwException;
}
public CacheConfig getCacheConfig() {
return cacheConfig;
}
public void setCacheConfig(CacheConfig cacheConfig) {
this.cacheConfig = cacheConfig;
}
public DebugConfig getDebugConfig() {
return debugConfig;
}
public void setDebugConfig(DebugConfig debugConfig) {
this.debugConfig = debugConfig;
}
public SecurityConfig getSecurityConfig() {
return securityConfig;
}
public void setSecurityConfig(SecurityConfig securityConfig) {
this.securityConfig = securityConfig;
public String getVersion() {
return version;
}
public String getPrefix() {
@ -265,32 +225,20 @@ public class MagicAPIProperties {
this.prefix = prefix;
}
public SwaggerConfig getSwaggerConfig() {
return swaggerConfig;
public boolean isThrowException() {
return throwException;
}
public void setSwaggerConfig(SwaggerConfig swaggerConfig) {
this.swaggerConfig = swaggerConfig;
public void setThrowException(boolean throwException) {
this.throwException = throwException;
}
public String getAutoImportModule() {
return autoImportModule;
}
public void setAutoImportModule(String autoImport) {
this.autoImportModule = autoImport;
}
public List<String> getAutoImportModuleList() {
return Arrays.asList(autoImportModule.replaceAll("\\s", "").split(","));
}
public boolean isAllowOverride() {
return allowOverride;
}
public void setAllowOverride(boolean allowOverride) {
this.allowOverride = allowOverride;
public void setAutoImportModule(String autoImportModule) {
this.autoImportModule = autoImportModule;
}
public String getAutoImportPackage() {
@ -301,11 +249,12 @@ public class MagicAPIProperties {
this.autoImportPackage = autoImportPackage;
}
public List<String> getAutoImportPackageList() {
if (autoImportPackage == null) {
return Collections.emptyList();
}
return Arrays.asList(autoImportPackage.replaceAll("\\s", "").split(","));
public boolean isAllowOverride() {
return allowOverride;
}
public void setAllowOverride(boolean allowOverride) {
this.allowOverride = allowOverride;
}
public int getThreadPoolExecutorSize() {
@ -316,17 +265,12 @@ public class MagicAPIProperties {
this.threadPoolExecutorSize = threadPoolExecutorSize;
}
public String getVersion() {
return version;
public String getEditorConfig() {
return editorConfig;
}
public ResourceConfig getResource() {
return resource;
}
public void setResource(ResourceConfig resource) {
this.resource = resource;
public void setEditorConfig(String editorConfig) {
this.editorConfig = editorConfig;
}
public boolean isSupportCrossDomain() {
@ -345,22 +289,6 @@ public class MagicAPIProperties {
this.response = response;
}
public ResponseCodeConfig getResponseCodeConfig() {
return responseCodeConfig;
}
public void setResponseCodeConfig(ResponseCodeConfig responseCodeConfig) {
this.responseCodeConfig = responseCodeConfig;
}
public ClusterConfig getClusterConfig() {
return clusterConfig;
}
public void setClusterConfig(ClusterConfig clusterConfig) {
this.clusterConfig = clusterConfig;
}
public String getSecretKey() {
return secretKey;
}
@ -385,14 +313,6 @@ public class MagicAPIProperties {
this.showUrl = showUrl;
}
public BackupConfig getBackupConfig() {
return backupConfig;
}
public void setBackupConfig(BackupConfig backupConfig) {
this.backupConfig = backupConfig;
}
public boolean isShowSql() {
return showSql;
}
@ -424,4 +344,76 @@ public class MagicAPIProperties {
public void setPersistenceResponseBody(boolean persistenceResponseBody) {
this.persistenceResponseBody = persistenceResponseBody;
}
public String getInstanceId() {
return instanceId;
}
public void setInstanceId(String instanceId) {
this.instanceId = instanceId;
}
public Security getSecurityConfig() {
return securityConfig;
}
public void setSecurityConfig(Security securityConfig) {
this.securityConfig = securityConfig;
}
public Page getPage() {
return page;
}
public void setPage(Page page) {
this.page = page;
}
public Cache getCache() {
return cache;
}
public void setCache(Cache cache) {
this.cache = cache;
}
public Debug getDebug() {
return debug;
}
public void setDebug(Debug debug) {
this.debug = debug;
}
public Resource getResource() {
return resource;
}
public void setResource(Resource resource) {
this.resource = resource;
}
public ResponseCode getResponseCode() {
return responseCode;
}
public void setResponseCode(ResponseCode responseCode) {
this.responseCode = responseCode;
}
public Crud getCrud() {
return crud;
}
public void setCrud(Crud crud) {
this.crud = crud;
}
public Backup getBackup() {
return backup;
}
public void setBackup(Backup backup) {
this.backup = backup;
}
}

View File

@ -1,55 +1,38 @@
package org.ssssssss.magicapi.config;
package org.ssssssss.magicapi.core.config;
import org.springframework.http.converter.HttpMessageConverter;
import org.ssssssss.magicapi.adapter.Resource;
import org.ssssssss.magicapi.controller.RequestHandler;
import org.ssssssss.magicapi.interceptor.AuthorizationInterceptor;
import org.ssssssss.magicapi.interceptor.RequestInterceptor;
import org.ssssssss.magicapi.provider.*;
import org.ssssssss.magicapi.core.resource.Resource;
import org.ssssssss.magicapi.core.web.RequestHandler;
import org.ssssssss.magicapi.core.model.MagicEntity;
import org.ssssssss.magicapi.core.service.MagicDynamicRegistry;
import org.ssssssss.magicapi.core.service.MagicResourceService;
import org.ssssssss.magicapi.core.interceptor.AuthorizationInterceptor;
import org.ssssssss.magicapi.core.interceptor.RequestInterceptor;
import org.ssssssss.magicapi.core.service.MagicAPIService;
import org.ssssssss.magicapi.backup.service.MagicBackupService;
import org.ssssssss.magicapi.core.service.MagicNotifyService;
import org.ssssssss.magicapi.core.interceptor.ResultProvider;
import org.ssssssss.magicapi.datasource.model.MagicDynamicDataSource;
import java.util.ArrayList;
import java.util.List;
/**
* 配置信息
*
* @author mxd
*/
public class MagicConfiguration {
/**
* 拦截器
*/
private final List<RequestInterceptor> requestInterceptors = new ArrayList<>();
/**
* 接口映射
*/
private MappingHandlerMapping mappingHandlerMapping;
/**
* 函数管理
*/
private MagicFunctionManager magicFunctionManager;
/**
* 编辑器配置文件
*/
private String editorConfig;
/**
* 接口查询Service
*/
private ApiServiceProvider apiServiceProvider;
/**
* 分组查询Service
*/
private GroupServiceProvider groupServiceProvider;
/**
* 函数查询Service
*/
private FunctionServiceProvider functionServiceProvider;
private MagicAPIService magicAPIService;
private MagicDynamicDataSource magicDynamicDataSource;
/**
* 请求出错时是否抛出异常
*/
@ -72,6 +55,10 @@ public class MagicConfiguration {
private MagicBackupService magicBackupService;
private static MagicResourceService magicResourceService;
private List<MagicDynamicRegistry<? extends MagicEntity>> magicDynamicRegistries;
/**
* debug 超时时间
*/
@ -83,14 +70,6 @@ public class MagicConfiguration {
this.requestInterceptors.add(requestInterceptor);
}
public MappingHandlerMapping getMappingHandlerMapping() {
return mappingHandlerMapping;
}
public void setMappingHandlerMapping(MappingHandlerMapping mappingHandlerMapping) {
this.mappingHandlerMapping = mappingHandlerMapping;
}
public AuthorizationInterceptor getAuthorizationInterceptor() {
return authorizationInterceptor;
}
@ -103,22 +82,6 @@ public class MagicConfiguration {
return requestInterceptors;
}
public ApiServiceProvider getApiServiceProvider() {
return apiServiceProvider;
}
public void setApiServiceProvider(ApiServiceProvider apiServiceProvider) {
this.apiServiceProvider = apiServiceProvider;
}
public GroupServiceProvider getGroupServiceProvider() {
return groupServiceProvider;
}
public void setGroupServiceProvider(GroupServiceProvider groupServiceProvider) {
this.groupServiceProvider = groupServiceProvider;
}
public boolean isThrowException() {
return throwException;
}
@ -159,22 +122,6 @@ public class MagicConfiguration {
this.enableWeb = enableWeb;
}
public FunctionServiceProvider getFunctionServiceProvider() {
return functionServiceProvider;
}
public void setFunctionServiceProvider(FunctionServiceProvider functionServiceProvider) {
this.functionServiceProvider = functionServiceProvider;
}
public MagicFunctionManager getMagicFunctionManager() {
return magicFunctionManager;
}
public void setMagicFunctionManager(MagicFunctionManager magicFunctionManager) {
this.magicFunctionManager = magicFunctionManager;
}
public String getEditorConfig() {
return editorConfig;
}
@ -223,15 +170,43 @@ public class MagicConfiguration {
this.magicBackupService = magicBackupService;
}
public MagicDynamicDataSource getMagicDynamicDataSource() {
return magicDynamicDataSource;
}
public void setMagicDynamicDataSource(MagicDynamicDataSource magicDynamicDataSource) {
this.magicDynamicDataSource = magicDynamicDataSource;
}
public static MagicResourceService getMagicResourceService() {
return MagicConfiguration.magicResourceService;
}
public void setMagicResourceService(MagicResourceService magicResourceService) {
MagicConfiguration.magicResourceService = magicResourceService;
}
public List<MagicDynamicRegistry<? extends MagicEntity>> getMagicDynamicRegistries() {
return magicDynamicRegistries;
}
public void setMagicDynamicRegistries(List<MagicDynamicRegistry<? extends MagicEntity>> magicDynamicRegistries) {
this.magicDynamicRegistries = magicDynamicRegistries;
}
/**
* 打印banner
*/
public void printBanner() {
public void printBanner(List<String> plugins) {
System.out.println(" __ __ _ _ ____ ___ ");
System.out.println(" | \\/ | __ _ __ _ (_) ___ / \\ | _ \\|_ _|");
System.out.println(" | |\\/| | / _` | / _` || | / __| / _ \\ | |_) || | ");
System.out.println(" | | | || (_| || (_| || || (__ / ___ \\ | __/ | | ");
System.out.println(" |_| |_| \\__,_| \\__, ||_| \\___|/_/ \\_\\|_| |___|");
System.out.println(" |___/ " + RequestHandler.class.getPackage().getImplementationVersion());
if(!plugins.isEmpty()){
System.out.println("集成插件:");
plugins.stream().peek(it -> System.out.print("- ")).forEach(System.out::println);
}
}
}

View File

@ -1,19 +1,13 @@
package org.ssssssss.magicapi.config;
package org.ssssssss.magicapi.core.config;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpHeaders;
import org.ssssssss.magicapi.model.Constants;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 接口跨域处理
*
* @author mxd
*/
public class MagicCorsFilter implements Filter {
@Override
@ -41,7 +35,7 @@ public class MagicCorsFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
if (StringUtils.isNotBlank(Constants.HEADER_REQUEST_SESSION)) {
if (StringUtils.isNotBlank(Constants.HEADER_REQUEST_CLIENT_ID)) {
process(request, (HttpServletResponse) resp);
}
chain.doFilter(req, resp);

View File

@ -1,4 +1,4 @@
package org.ssssssss.magicapi.config;
package org.ssssssss.magicapi.core.config;
/**
* 函数主要用于脚本中直接可使用的函数 now();

View File

@ -0,0 +1,17 @@
package org.ssssssss.magicapi.core.config;
import org.ssssssss.magicapi.core.model.Plugin;
import org.ssssssss.magicapi.core.web.MagicControllerRegister;
public interface MagicPluginConfiguration {
Plugin plugin();
/**
* 注册Controller
*/
default MagicControllerRegister controllerRegister(){
return (mapping, configuration) -> { };
}
}

View File

@ -0,0 +1,45 @@
package org.ssssssss.magicapi.core.config;
/**
* 消息类型
*/
public enum MessageType {
/* S -> C message */
/* 日志消息 */
LOG,
/* 多个日志消息 */
LOGS,
/* 进入断点 */
BREAKPOINT,
/* 请求接口发生异常 */
EXCEPTION,
/* 登录结果 */
LOGIN_RESPONSE,
/* 通知客户端,有用户上线 */
USER_LOGIN,
/* 通知客户端,有用户下线 */
USER_LOGOUT,
/* 通知客户端,当前机器在线人数 */
ONLINE_USERS,
/* 通知客户端,他人进入文件*/
INTO_FILE_ID,
/* PONG */
PONG,
/* C -> S message */
/* 设置断点 */
SET_BREAKPOINT,
/* 恢复断点 */
RESUME_BREAKPOINT,
/* 登录 */
LOGIN,
/* 设置当前所在文件 */
SET_FILE_ID,
/* ping */
PING,
/* S <-> S -> C message*/
/* 获取当前在线用户 */
SEND_ONLINE
}

View File

@ -1,11 +1,11 @@
package org.ssssssss.magicapi.spring.boot.starter;
package org.ssssssss.magicapi.core.config;
/**
* 分页配置
*
* @author mxd
*/
public class PageConfig {
public class Page {
/**
* 默认page表达式

View File

@ -1,11 +1,11 @@
package org.ssssssss.magicapi.spring.boot.starter;
package org.ssssssss.magicapi.core.config;
/**
* 接口存储配置
*
* @author mxd
*/
public class ResourceConfig {
public class Resource {
/**
* 存储类型默认是文件

View File

@ -1,4 +1,4 @@
package org.ssssssss.magicapi.spring.boot.starter;
package org.ssssssss.magicapi.core.config;
/**
* json结果code配置
@ -6,7 +6,7 @@ package org.ssssssss.magicapi.spring.boot.starter;
* @author mxd
* @since 1.1.2
*/
public class ResponseCodeConfig {
public class ResponseCode {
/**
* 执行成功的code值

View File

@ -1,4 +1,4 @@
package org.ssssssss.magicapi.spring.boot.starter;
package org.ssssssss.magicapi.core.config;
/**
* 安全配置
@ -6,7 +6,7 @@ package org.ssssssss.magicapi.spring.boot.starter;
* @author mxd
* @since 0.4.0
*/
public class SecurityConfig {
public class Security {
/**
* 登录用的用户名

View File

@ -0,0 +1,210 @@
package org.ssssssss.magicapi.core.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.socket.TextMessage;
import org.ssssssss.magicapi.core.event.EventAction;
import org.ssssssss.magicapi.core.context.MagicConsoleSession;
import org.ssssssss.magicapi.core.model.MagicNotify;
import org.ssssssss.magicapi.core.model.Pair;
import org.ssssssss.magicapi.core.service.MagicNotifyService;
import org.ssssssss.magicapi.utils.JsonUtils;
import org.ssssssss.script.MagicScriptDebugContext;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public class WebSocketSessionManager {
private static final Logger logger = LoggerFactory.getLogger(WebSocketSessionManager.class);
private static final Map<String, MagicConsoleSession> SESSIONS = new ConcurrentHashMap<>();
private static MagicNotifyService magicNotifyService;
private static final Map<String, MagicScriptDebugContext> CONTEXTS = new ConcurrentHashMap<>();
private static String instanceId;
private static final List<Pair<String, String>> MESSAGE_CACHE = new ArrayList<>(200);
public static void add(MagicConsoleSession session) {
SESSIONS.put(session.getClientId(), session);
}
public static MagicConsoleSession getConsoleSession(String clientId) {
return SESSIONS.get(clientId);
}
static {
// 1秒1次发送日志
new ScheduledThreadPoolExecutor(1, r -> new Thread(r, "magic-api-send-log-task")).scheduleAtFixedRate(WebSocketSessionManager::flushLog, 1, 1, TimeUnit.SECONDS);
// 60秒检测一次是否在线
new ScheduledThreadPoolExecutor(1, r -> new Thread(r, "magic-api-websocket-clean-task")).scheduleAtFixedRate(WebSocketSessionManager::checkSession, 60, 60, TimeUnit.SECONDS);
}
public static Collection<MagicConsoleSession> getSessions() {
return SESSIONS.values();
}
public static void remove(MagicConsoleSession session) {
if (session.getClientId() != null) {
remove(session.getClientId());
}
}
public static void remove(String sessionId) {
SESSIONS.remove(sessionId);
}
public static void sendToAll(MessageType messageType, Object... values) {
String content = buildMessage(messageType, values);
sendToAll(content);
}
private static void sendToAll(String content) {
getSessions().stream().filter(MagicConsoleSession::writeable).forEach(session -> sendBySession(session, content));
sendToMachineByClientId(null, content);
}
public static void sendLogs(String sessionId, String message) {
synchronized (MESSAGE_CACHE) {
MESSAGE_CACHE.add(Pair.of(sessionId, message));
if (MESSAGE_CACHE.size() >= 100) {
flushLog();
}
}
}
public static void flushLog() {
try {
Map<String, List<String>> messages;
synchronized (MESSAGE_CACHE) {
messages = MESSAGE_CACHE.stream().collect(Collectors.groupingBy(Pair::getFirst, Collectors.mapping(Pair::getSecond, Collectors.toList())));
MESSAGE_CACHE.clear();
}
messages.forEach((clientId, logs) -> sendByClientId(clientId, logs.size() > 1 ? MessageType.LOGS : MessageType.LOG, logs));
} catch (Exception e) {
logger.warn("发送日志失败", e);
}
}
public static void sendByClientId(String clientId, MessageType messageType, Object... values) {
MagicConsoleSession session = findSession(clientId);
String content = buildMessage(messageType, values);
if (session != null && session.writeable()) {
sendBySession(session, content);
} else {
sendToMachineByClientId(clientId, content);
}
}
public static void sendToOther(String excludeClientId, MessageType messageType, Object... values) {
String content = buildMessage(messageType, values);
getSessions().stream()
.filter(MagicConsoleSession::writeable)
.filter(it -> !it.getClientId().equals(excludeClientId))
.forEach(session -> sendBySession(session, content));
sendToMachineByClientId(null, content);
}
public static void sendToMachineByClientId(String clientId, String content) {
if (magicNotifyService != null) {
// 通知其他机器去发送消息
magicNotifyService.sendNotify(new MagicNotify(instanceId, EventAction.WS_S_C, clientId, content));
}
}
public static void sendToMachine(MessageType messageType, Object... args) {
if (magicNotifyService != null) {
// 通知其他机器去发送消息
magicNotifyService.sendNotify(new MagicNotify(instanceId, EventAction.WS_S_S, null, buildMessage(messageType, args)));
}
}
public static String buildMessage(MessageType messageType, Object... values) {
StringBuilder builder = new StringBuilder(messageType.name().toLowerCase());
if (values != null) {
for (int i = 0, len = values.length; i < len; i++) {
builder.append(",");
Object value = values[i];
if (i + 1 < len || value instanceof CharSequence || value instanceof Number) {
builder.append(value);
} else {
builder.append(JsonUtils.toJsonString(value));
}
}
}
return builder.toString();
}
public static void sendByClientId(String clientId, String content) {
if (clientId == null) {
getSessions().stream().filter(MagicConsoleSession::writeable).forEach(session -> sendBySession(session, content));
} else {
MagicConsoleSession session = findSession(clientId);
if (session != null) {
sendBySession(session, content);
}
}
}
public static void sendBySession(MagicConsoleSession session, String content) {
try {
if (session != null) {
synchronized (session.getClientId()) {
session.getWebSocketSession().sendMessage(new TextMessage(content));
}
}
} catch (Exception e) {
logger.warn("发送WebSocket消息失败: {}", e.getMessage());
}
}
public static MagicConsoleSession findSession(String clientId) {
return getSessions().stream()
.filter(it -> Objects.equals(clientId, it.getClientId()))
.findFirst()
.orElse(null);
}
public static void setMagicNotifyService(MagicNotifyService magicNotifyService) {
WebSocketSessionManager.magicNotifyService = magicNotifyService;
}
public static void setInstanceId(String instanceId) {
WebSocketSessionManager.instanceId = instanceId;
}
public static void addMagicScriptContext(String sessionAndScriptId, MagicScriptDebugContext context) {
CONTEXTS.put(sessionAndScriptId, context);
}
public static MagicScriptDebugContext findMagicScriptContext(String sessionAndScriptId) {
return CONTEXTS.get(sessionAndScriptId);
}
public static void removeMagicScriptContext(String sessionAndScriptId) {
CONTEXTS.remove(sessionAndScriptId);
}
private static void checkSession() {
try {
long activateTime = System.currentTimeMillis() - 20 * 1000;
SESSIONS.entrySet().stream()
.filter(it -> it.getValue().getActivateTime() < activateTime)
.collect(Collectors.toList())
.forEach(entry -> {
MagicConsoleSession session = entry.getValue();
SESSIONS.remove(entry.getKey());
session.close();
sendToAll(MessageType.USER_LOGOUT, session.getAttributes());
});
} catch (Exception ignored) {
}
}
}

View File

@ -1,4 +1,4 @@
package org.ssssssss.magicapi.context;
package org.ssssssss.magicapi.core.context;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;

View File

@ -0,0 +1,88 @@
package org.ssssssss.magicapi.core.context;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.WebSocketSession;
import org.ssssssss.magicapi.core.config.Constants;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class MagicConsoleSession {
private static final Map<String, MagicConsoleSession> cached = new ConcurrentHashMap<>();
private String clientId;
private WebSocketSession webSocketSession;
private final Map<String, Object> attributes = new HashMap<>();
private long activateTime = System.currentTimeMillis();
public MagicConsoleSession(WebSocketSession webSocketSession) {
this.webSocketSession = webSocketSession;
}
public String getClientId() {
return clientId;
}
public WebSocketSession getWebSocketSession() {
return webSocketSession;
}
public boolean writeable() {
return webSocketSession != null && webSocketSession.isOpen();
}
public static MagicConsoleSession from(WebSocketSession session) {
MagicConsoleSession magicConsoleSession = cached.get(session.getId());
if (magicConsoleSession == null) {
magicConsoleSession = new MagicConsoleSession(session);
cached.put(session.getId(), magicConsoleSession);
}
return magicConsoleSession;
}
public static void remove(WebSocketSession session) {
cached.remove(session.getId());
}
public Object getAttribute(String key){
return attributes.get(key);
}
public void setAttribute(String key, Object value){
attributes.put(key, value);
}
public Map<String, Object> getAttributes(){
return attributes;
}
public void setClientId(String clientId) {
this.clientId = clientId;
setAttribute(Constants.WEBSOCKET_ATTRIBUTE_CLIENT_ID, clientId);
}
public long getActivateTime() {
return activateTime;
}
public void setActivateTime(long activateTime) {
this.activateTime = activateTime;
}
public void close(){
if(this.webSocketSession != null){
remove(this.webSocketSession);
try {
this.webSocketSession.close(CloseStatus.SESSION_NOT_RELIABLE);
} catch (Exception ignored) {
}
this.webSocketSession = null;
}
}
}

View File

@ -1,4 +1,4 @@
package org.ssssssss.magicapi.interceptor;
package org.ssssssss.magicapi.core.context;
/**
* magic 用户对象

View File

@ -1,6 +1,4 @@
package org.ssssssss.magicapi.context;
import org.ssssssss.magicapi.model.RequestEntity;
package org.ssssssss.magicapi.core.context;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

View File

@ -0,0 +1,141 @@
package org.ssssssss.magicapi.core.context;
import org.ssssssss.magicapi.core.model.ApiInfo;
import org.ssssssss.magicapi.core.model.DebugRequest;
import org.ssssssss.script.MagicScriptContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.UUID;
/**
* 请求信息
*
* @author mxd
*/
public class RequestEntity {
private final Long requestTime = System.currentTimeMillis();
private final String requestId = UUID.randomUUID().toString().replace("-", "");
private ApiInfo apiInfo;
private HttpServletRequest request;
private HttpServletResponse response;
private boolean requestedFromTest;
private Map<String, Object> parameters;
private Map<String, Object> pathVariables;
private MagicScriptContext magicScriptContext;
private Object requestBody;
private DebugRequest debugRequest;
private Map<String, Object> headers;
private RequestEntity() {
}
public static RequestEntity create() {
return new RequestEntity();
}
public ApiInfo getApiInfo() {
return apiInfo;
}
public RequestEntity info(ApiInfo apiInfo) {
this.apiInfo = apiInfo;
return this;
}
public HttpServletRequest getRequest() {
return request;
}
public RequestEntity request(HttpServletRequest request) {
this.request = request;
this.debugRequest = DebugRequest.create(request);
return this;
}
public HttpServletResponse getResponse() {
return response;
}
public RequestEntity response(HttpServletResponse response) {
this.response = response;
return this;
}
public boolean isRequestedFromTest() {
return requestedFromTest;
}
public RequestEntity requestedFromTest(boolean requestedFromTest) {
this.requestedFromTest = requestedFromTest;
return this;
}
public boolean isRequestedFromDebug() {
return requestedFromTest && !this.debugRequest.getRequestedBreakpoints().isEmpty();
}
public Map<String, Object> getParameters() {
return parameters;
}
public RequestEntity parameters(Map<String, Object> parameters) {
this.parameters = parameters;
return this;
}
public Map<String, Object> getPathVariables() {
return pathVariables;
}
public RequestEntity pathVariables(Map<String, Object> pathVariables) {
this.pathVariables = pathVariables;
return this;
}
public Long getRequestTime() {
return requestTime;
}
public MagicScriptContext getMagicScriptContext() {
return magicScriptContext;
}
public RequestEntity setMagicScriptContext(MagicScriptContext magicScriptContext) {
this.magicScriptContext = magicScriptContext;
return this;
}
public Map<String, Object> getHeaders() {
return headers;
}
public RequestEntity setHeaders(Map<String, Object> headers) {
this.headers = headers;
return this;
}
public String getRequestId() {
return requestId;
}
/**
* 获取 RequestBody
*/
public Object getRequestBody() {
return this.requestBody;
}
public RequestEntity setRequestBody(Object requestBody) {
this.requestBody = requestBody;
return this;
}
public DebugRequest getDebugRequest() {
return debugRequest;
}
}

View File

@ -1,4 +1,4 @@
package org.ssssssss.magicapi.context;
package org.ssssssss.magicapi.core.context;
import javax.servlet.http.HttpSession;
import java.util.HashMap;

Some files were not shown because too many files have changed in this diff Show More