فهرست منبع

集群通知配置、内置实现,删除@Deprecated方法

mxd 4 سال پیش
والد
کامیت
d3d500aef4
17فایلهای تغییر یافته به همراه546 افزوده شده و 336 حذف شده
  1. 30 0
      magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/ClusterConfig.java
  2. 11 14
      magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/MagicAPIAutoConfiguration.java
  3. 1 15
      magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/MagicAPIProperties.java
  4. 41 1
      magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/MagicRedisAutoConfiguration.java
  5. 10 0
      magic-api/src/main/java/org/ssssssss/magicapi/config/MagicConfiguration.java
  6. 0 8
      magic-api/src/main/java/org/ssssssss/magicapi/config/MagicFunctionManager.java
  7. 2 11
      magic-api/src/main/java/org/ssssssss/magicapi/config/MappingHandlerMapping.java
  8. 10 157
      magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicDataSourceController.java
  9. 3 91
      magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicWorkbenchController.java
  10. 12 4
      magic-api/src/main/java/org/ssssssss/magicapi/controller/RequestHandler.java
  11. 1 11
      magic-api/src/main/java/org/ssssssss/magicapi/interceptor/SQLInterceptor.java
  12. 3 1
      magic-api/src/main/java/org/ssssssss/magicapi/model/Constants.java
  13. 48 1
      magic-api/src/main/java/org/ssssssss/magicapi/model/MagicNotify.java
  14. 44 1
      magic-api/src/main/java/org/ssssssss/magicapi/provider/MagicAPIService.java
  15. 1 9
      magic-api/src/main/java/org/ssssssss/magicapi/provider/ResultProvider.java
  16. 3 5
      magic-api/src/main/java/org/ssssssss/magicapi/provider/impl/DefaultGroupServiceProvider.java
  17. 326 7
      magic-api/src/main/java/org/ssssssss/magicapi/provider/impl/DefaultMagicAPIService.java

+ 30 - 0
magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/ClusterConfig.java

@@ -1,12 +1,26 @@
 package org.ssssssss.magicapi.spring.boot.starter;
 
+/**
+ * 集群配置
+ * @since 1.2.0
+ */
 public class ClusterConfig {
 
+	/**
+	 * 是否启用,默认不启用
+	 */
+	private boolean enable = false;
+
 	/**
 	 * 实例ID,集群环境下,要保证每台机器不同。默认启动后随机生成uuid
 	 */
 	private String instanceId;
 
+	/**
+	 * redis 通道
+	 */
+	private String channel = "magic-api:notify:channel";
+
 	public String getInstanceId() {
 		return instanceId;
 	}
@@ -14,4 +28,20 @@ public class ClusterConfig {
 	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;
+	}
 }

+ 11 - 14
magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/MagicAPIAutoConfiguration.java

@@ -144,7 +144,7 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
 									 ObjectProvider<AuthorizationInterceptor> authorizationInterceptorProvider,
 									 Environment environment,
 									 ApplicationContext applicationContext
-									 ) {
+	) {
 		this.properties = properties;
 		this.dialects = dialectsProvider.getIfAvailable(Collections::emptyList);
 		this.requestInterceptors = requestInterceptorsProvider.getIfAvailable(Collections::emptyList);
@@ -323,6 +323,7 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
 	@Bean
 	@ConditionalOnMissingBean(MagicNotifyService.class)
 	public MagicNotifyService magicNotifyService() {
+		logger.info("未配置集群通知服务,本实例不会推送通知,集群环境下可能会有问题,如需开启,请配置magic-api.cluster-config.enable=true");
 		return magicNotify -> {
 		};
 	}
@@ -342,9 +343,11 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
 										   FunctionServiceProvider functionServiceProvider,
 										   GroupServiceProvider groupServiceProvider,
 										   ResultProvider resultProvider,
+										   MagicDynamicDataSource magicDynamicDataSource,
 										   MagicFunctionManager magicFunctionManager,
-										   MagicNotifyService magicNotifyService) {
-		return new DefaultMagicAPIService(mappingHandlerMapping, apiServiceProvider, functionServiceProvider, groupServiceProvider, resultProvider, magicFunctionManager, magicNotifyService, properties.getClusterConfig().getInstanceId(), properties.isThrowException());
+										   MagicNotifyService magicNotifyService,
+										   Resource workspace) {
+		return new DefaultMagicAPIService(mappingHandlerMapping, apiServiceProvider, functionServiceProvider, groupServiceProvider, resultProvider, magicDynamicDataSource, magicFunctionManager, magicNotifyService, properties.getClusterConfig().getInstanceId(), workspace, properties.isThrowException());
 	}
 
 	private void setupSpringSecurity() {
@@ -467,6 +470,7 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
 												 Resource magicResource,
 												 ResultProvider resultProvider,
 												 MagicAPIService magicAPIService,
+												 MagicNotifyService magicNotifyService,
 												 ApiServiceProvider apiServiceProvider,
 												 GroupServiceProvider groupServiceProvider,
 												 MappingHandlerMapping mappingHandlerMapping,
@@ -487,6 +491,7 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
 		configuration.setApiServiceProvider(apiServiceProvider);
 		configuration.setGroupServiceProvider(groupServiceProvider);
 		configuration.setMappingHandlerMapping(mappingHandlerMapping);
+		configuration.setMagicNotifyService(magicNotifyService);
 		configuration.setFunctionServiceProvider(functionServiceProvider);
 		SecurityConfig securityConfig = properties.getSecurityConfig();
 		configuration.setUsername(securityConfig.getUsername());
@@ -520,7 +525,7 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
 			));
 			controllers.forEach(item -> mappingHandlerMapping.registerController(item, base));
 		}
-		dataSourceController.registerDataSource();
+		magicAPIService.registerAllDataSource();
 		// 设置拦截器信息
 		this.requestInterceptors.forEach(interceptor -> {
 			logger.info("注册请求拦截器:{}", interceptor.getClass());
@@ -533,21 +538,13 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
 		configuration.setMagicFunctionManager(magicFunctionManager);
 		// 注册函数加载器
 		magicFunctionManager.registerFunctionLoader();
-		// 注册所有函数a
+		// 注册所有函数
 		magicFunctionManager.registerAllFunction();
-		// 自动刷新
-		magicFunctionManager.enableRefresh(properties.getRefreshInterval());
 		mappingHandlerMapping.setHandler(new RequestHandler(configuration));
 		mappingHandlerMapping.setMagicApiService(apiServiceProvider);
 		mappingHandlerMapping.setGroupServiceProvider(groupServiceProvider);
 		// 注册所有映射
 		mappingHandlerMapping.registerAllMapping();
-		int refreshInterval = properties.getRefreshInterval();
-		// 自动刷新
-		mappingHandlerMapping.enableRefresh(refreshInterval);
-		if (refreshInterval > 0) {
-			Executors.newScheduledThreadPool(1).scheduleAtFixedRate(dataSourceController::registerDataSource, refreshInterval, refreshInterval, TimeUnit.SECONDS);
-		}
 		return configuration;
 	}
 
@@ -556,7 +553,7 @@ public class MagicAPIAutoConfiguration implements WebMvcConfigurer {
 		return new DefaultAuthorizationInterceptor(securityConfig.getUsername(), securityConfig.getPassword());
 	}
 
-	private RestTemplate createRestTemplate(){
+	private RestTemplate createRestTemplate() {
 		RestTemplate restTemplate = new RestTemplate();
 		restTemplate.getMessageConverters().add(new StringHttpMessageConverter(StandardCharsets.UTF_8) {
 			{

+ 1 - 15
magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/MagicAPIProperties.java

@@ -44,13 +44,7 @@ public class MagicAPIProperties {
 	 * @since 0.4.0
 	 */
 	private String autoImportPackage;
-	/**
-	 * 自动刷新间隔,单位为秒,默认不开启
-	 *
-	 * @since 0.3.4
-	 */
-	@Deprecated
-	private int refreshInterval = 0;
+
 	/**
 	 * 是否允许覆盖应用接口,默认为false
 	 *
@@ -226,14 +220,6 @@ public class MagicAPIProperties {
 		return Arrays.asList(autoImportModule.replaceAll("\\s", "").split(","));
 	}
 
-	public int getRefreshInterval() {
-		return refreshInterval;
-	}
-
-	public void setRefreshInterval(int refreshInterval) {
-		this.refreshInterval = refreshInterval;
-	}
-
 	public boolean isAllowOverride() {
 		return allowOverride;
 	}

+ 41 - 1
magic-api-spring-boot-starter/src/main/java/org/ssssssss/magicapi/spring/boot/starter/MagicRedisAutoConfiguration.java

@@ -1,5 +1,8 @@
 package org.ssssssss.magicapi.spring.boot.starter;
 
+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.ConditionalOnBean;
 import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -8,9 +11,17 @@ 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.utils.JsonUtils;
+
+import java.util.Objects;
 
 /**
  * redis配置
@@ -20,10 +31,15 @@ import org.ssssssss.magicapi.modules.RedisModule;
 @AutoConfigureBefore(MagicAPIAutoConfiguration.class)
 public class MagicRedisAutoConfiguration {
 
+	private final static Logger logger = LoggerFactory.getLogger(MagicRedisAutoConfiguration.class);
+
 	private final MagicAPIProperties properties;
 
-	public MagicRedisAutoConfiguration(MagicAPIProperties properties) {
+	private final StringRedisTemplate stringRedisTemplate;
+
+	public MagicRedisAutoConfiguration(MagicAPIProperties properties, ObjectProvider<StringRedisTemplate> stringRedisTemplateProvider) {
 		this.properties = properties;
+		this.stringRedisTemplate = stringRedisTemplateProvider.getIfAvailable();
 	}
 
 	/**
@@ -44,4 +60,28 @@ public class MagicRedisAutoConfiguration {
 		ResourceConfig resource = properties.getResource();
 		return new RedisResource(new StringRedisTemplate(connectionFactory), resource.getPrefix(), resource.isReadonly());
 	}
+
+	/**
+	 * 使用Redis推送通知
+	 */
+	@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)));
+	}
+
+	/**
+	 * 集群通知监听
+	 */
+	@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);
+		redisMessageListenerContainer.addMessageListener((message, pattern) -> magicAPIService.processNotify(JsonUtils.readValue(message.getBody(), MagicNotify.class)), ChannelTopic.of(config.getChannel()));
+		return redisMessageListenerContainer;
+	}
 }

+ 10 - 0
magic-api/src/main/java/org/ssssssss/magicapi/config/MagicConfiguration.java

@@ -74,6 +74,8 @@ public class MagicConfiguration {
 
 	private AuthorizationInterceptor authorizationInterceptor;
 
+	private MagicNotifyService magicNotifyService;
+
 	/**
 	 * debug 超时时间
 	 */
@@ -225,6 +227,14 @@ public class MagicConfiguration {
 		this.magicAPIService = magicAPIService;
 	}
 
+	public MagicNotifyService getMagicNotifyService() {
+		return magicNotifyService;
+	}
+
+	public void setMagicNotifyService(MagicNotifyService magicNotifyService) {
+		this.magicNotifyService = magicNotifyService;
+	}
+
 	/**
 	 * 打印banner
 	 */

+ 0 - 8
magic-api/src/main/java/org/ssssssss/magicapi/config/MagicFunctionManager.java

@@ -15,8 +15,6 @@ import org.ssssssss.script.MagicScriptContext;
 
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
@@ -203,10 +201,4 @@ public class MagicFunctionManager {
 			logger.info("取消注册函数:[{},{}]", functionInfo.getName(), functionInfo.getMappingPath());
 		}
 	}
-
-	public void enableRefresh(int interval) {
-		if (interval > 0) {
-			Executors.newScheduledThreadPool(1).scheduleAtFixedRate(this::registerAllFunction, interval, interval, TimeUnit.SECONDS);
-		}
-	}
 }

+ 2 - 11
magic-api/src/main/java/org/ssssssss/magicapi/config/MappingHandlerMapping.java

@@ -1,8 +1,8 @@
 package org.ssssssss.magicapi.config;
 
+import org.apache.commons.lang3.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
 import org.springframework.web.context.request.NativeWebRequest;
@@ -26,8 +26,6 @@ import javax.servlet.http.HttpServletResponse;
 import java.lang.reflect.Method;
 import java.util.*;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
@@ -107,7 +105,7 @@ public class MappingHandlerMapping {
 	 * @param requestMapping 请求路径
 	 */
 	private static String buildMappingKey(String requestMethod, String requestMapping) {
-		if (!StringUtils.isEmpty(requestMapping) && !requestMapping.startsWith("/")) {
+		if (StringUtils.isNotBlank(requestMapping) && !requestMapping.startsWith("/")) {
 			requestMapping = "/" + requestMapping;
 		}
 		return Objects.toString(requestMethod, "GET").toUpperCase() + ":" + requestMapping;
@@ -481,13 +479,6 @@ public class MappingHandlerMapping {
 		return RequestMappingInfo.paths(path).methods(RequestMethod.valueOf(method.toUpperCase())).build();
 	}
 
-	public void enableRefresh(int interval) {
-		if (interval > 0) {
-			logger.info("启动自动刷新magic-api");
-			Executors.newScheduledThreadPool(1).scheduleAtFixedRate(this::registerAllMapping, interval, interval, TimeUnit.SECONDS);
-		}
-	}
-
 	static class MappingNode {
 
 		private ApiInfo info;

+ 10 - 157
magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicDataSourceController.java

@@ -1,56 +1,24 @@
 package org.ssssssss.magicapi.controller;
 
-import com.fasterxml.jackson.databind.type.TypeFactory;
-import org.apache.commons.lang3.StringUtils;
-import org.springframework.beans.BeanUtils;
-import org.springframework.boot.context.properties.bind.Bindable;
-import org.springframework.boot.context.properties.bind.Binder;
-import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
-import org.springframework.boot.context.properties.source.ConfigurationPropertyNameAliases;
-import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
-import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
-import org.springframework.boot.jdbc.DatabaseDriver;
-import org.springframework.jdbc.datasource.DataSourceUtils;
-import org.springframework.util.ClassUtils;
 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.config.MagicConfiguration;
-import org.ssssssss.magicapi.config.MagicDynamicDataSource;
 import org.ssssssss.magicapi.config.Valid;
-import org.ssssssss.magicapi.exception.InvalidArgumentException;
 import org.ssssssss.magicapi.interceptor.Authorization;
-import org.ssssssss.magicapi.model.Constants;
 import org.ssssssss.magicapi.model.JsonBean;
-import org.ssssssss.magicapi.utils.IoUtils;
-import org.ssssssss.magicapi.utils.JsonUtils;
-import org.ssssssss.script.functions.ObjectConvertExtension;
+import org.ssssssss.magicapi.provider.MagicAPIService;
 
-import javax.sql.DataSource;
-import java.sql.Connection;
-import java.util.*;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
+import java.util.List;
+import java.util.Map;
 
 public class MagicDataSourceController extends MagicController implements MagicExceptionHandler {
 
-	private static final ClassLoader classLoader = MagicDataSourceController.class.getClassLoader();
-
-	// copy from DataSourceBuilder
-	private static final String[] DATA_SOURCE_TYPE_NAMES = new String[]{
-			"com.zaxxer.hikari.HikariDataSource",
-			"org.apache.tomcat.jdbc.pool.DataSource",
-			"org.apache.commons.dbcp2.BasicDataSource"};
-
-	private final Resource resource;
+	private final MagicAPIService magicAPIService;
 
 	public MagicDataSourceController(MagicConfiguration configuration) {
 		super(configuration);
-		resource = configuration.getWorkspace().getDirectory(Constants.PATH_DATASOURCE);
-		if (!resource.exists()) {
-			resource.mkdir();
-		}
+		this.magicAPIService = configuration.getMagicAPIService();
 	}
 
 	/**
@@ -60,30 +28,13 @@ public class MagicDataSourceController extends MagicController implements MagicE
 	@ResponseBody
 	@Valid(authorization = Authorization.VIEW)
 	public JsonBean<List<Map<String, Object>>> list() {
-		List<Map<String, Object>> list = configuration.getMagicDynamicDataSource().datasourceNodes().stream().map(it -> {
-			Map<String, Object> row = new HashMap<>();
-			row.put("id", it.getId());    // id为空的则认为是不可修改的
-			row.put("key", it.getKey());    // 如果为null 说明是主数据源
-			row.put("name", it.getName());
-			return row;
-		}).collect(Collectors.toList());
-		return new JsonBean<>(list);
+		return new JsonBean<>(magicAPIService.datasourceList());
 	}
 
 	@RequestMapping("/datasource/test")
 	@ResponseBody
 	public JsonBean<String> test(@RequestBody Map<String, String> properties) {
-		DataSource dataSource = null;
-		try {
-			dataSource = createDataSource(properties);
-			Connection connection = dataSource.getConnection();
-			DataSourceUtils.doCloseConnection(connection, dataSource);
-		} catch (Exception e) {
-			return new JsonBean<>(e.getMessage());
-		} finally {
-			IoUtils.closeDataSource(dataSource);
-		}
-		return new JsonBean<>();
+		return new JsonBean<>(magicAPIService.testDataSource(properties));
 	}
 
 	/**
@@ -95,31 +46,7 @@ public class MagicDataSourceController extends MagicController implements MagicE
 	@Valid(readonly = false, authorization = Authorization.DATASOURCE_SAVE)
 	@ResponseBody
 	public JsonBean<String> save(@RequestBody Map<String, String> properties) {
-		String key = properties.get("key");
-		// 校验key是否符合规则
-		notBlank(key, DATASOURCE_KEY_REQUIRED);
-		isTrue(IoUtils.validateFileName(key), DATASOURCE_KEY_INVALID);
-		String name = properties.getOrDefault("name", key);
-		String id = properties.get("id");
-		Stream<String> keyStream;
-		if (StringUtils.isBlank(id)) {
-			keyStream = configuration.getMagicDynamicDataSource().datasources().stream();
-		} else {
-			keyStream = configuration.getMagicDynamicDataSource().datasourceNodes().stream()
-					.filter(it -> !id.equals(it.getId()))
-					.map(MagicDynamicDataSource.DataSourceNode::getKey);
-		}
-		String dsId = StringUtils.isBlank(id) ? UUID.randomUUID().toString().replace("-", "") : id;
-		// 验证是否有冲突
-		isTrue(keyStream.noneMatch(key::equals), DATASOURCE_KEY_EXISTS);
-
-		int maxRows = ObjectConvertExtension.asInt(properties.get("maxRows"), -1);
-		properties.remove("id");
-		// 注册数据源
-		configuration.getMagicDynamicDataSource().put(dsId, key, name, createDataSource(properties), maxRows);
-		properties.put("id", dsId);
-		resource.getResource(dsId + ".json").write(JsonUtils.toJsonString(properties));
-		return new JsonBean<>(dsId);
+		return new JsonBean<>(magicAPIService.saveDataSource(properties));
 	}
 
 	/**
@@ -131,87 +58,13 @@ public class MagicDataSourceController extends MagicController implements MagicE
 	@Valid(readonly = false, authorization = Authorization.DATASOURCE_DELETE)
 	@ResponseBody
 	public JsonBean<Boolean> delete(String id) {
-		// 查询数据源是否存在
-		Optional<MagicDynamicDataSource.DataSourceNode> dataSourceNode = configuration.getMagicDynamicDataSource().datasourceNodes().stream()
-				.filter(it -> id.equals(it.getId()))
-				.findFirst();
-		isTrue(dataSourceNode.isPresent(), DATASOURCE_NOT_FOUND);
-		Resource resource = this.resource.getResource(id + ".json");
-		// 删除数据源
-		isTrue(resource.delete(), DATASOURCE_NOT_FOUND);
-		// 取消注册数据源
-		dataSourceNode.ifPresent(it -> configuration.getMagicDynamicDataSource().delete(it.getKey()));
-		return new JsonBean<>(true);
-
+		return new JsonBean<>(magicAPIService.deleteDataSource(id));
 	}
 
 	@RequestMapping("/datasource/detail")
 	@Valid(authorization = Authorization.DATASOURCE_VIEW)
 	@ResponseBody
 	public JsonBean<Object> detail(String id) {
-		Resource resource = this.resource.getResource(id + ".json");
-		byte[] bytes = resource.read();
-		isTrue(bytes != null && bytes.length > 0, DATASOURCE_NOT_FOUND);
-		return new JsonBean<>(JsonUtils.readValue(bytes, LinkedHashMap.class));
-	}
-
-	// 启动之后注册数据源
-	public void registerDataSource() {
-		resource.readAll();
-		List<Resource> resources = resource.files(".json");
-		// 删除旧的数据源
-		configuration.getMagicDynamicDataSource().datasourceNodes().stream()
-				.filter(it -> it.getId() != null)
-				.map(MagicDynamicDataSource.DataSourceNode::getKey)
-				.collect(Collectors.toList())
-				.forEach(it -> configuration.getMagicDynamicDataSource().delete(it));
-		TypeFactory factory = TypeFactory.defaultInstance();
-		for (Resource item : resources) {
-			Map<String, String> properties = JsonUtils.readValue(item.read(), factory.constructMapType(HashMap.class, String.class, String.class));
-			if (properties != null) {
-				String key = properties.get("key");
-				String name = properties.getOrDefault("name", key);
-				String dsId = properties.remove("id");
-				int maxRows = ObjectConvertExtension.asInt(properties.get("maxRows"), -1);
-				configuration.getMagicDynamicDataSource().put(dsId, key, name, createDataSource(properties), maxRows);
-			}
-		}
-	}
-
-	// copy from DataSourceBuilder
-	private DataSource createDataSource(Map<String, String> properties) {
-		Class<? extends DataSource> dataSourceType = getDataSourceType(properties.get("type"));
-		if (!properties.containsKey("driverClassName")
-				&& properties.containsKey("url")) {
-			String url = properties.get("url");
-			String driverClass = DatabaseDriver.fromJdbcUrl(url).getDriverClassName();
-			properties.put("driverClassName", driverClass);
-		}
-		DataSource dataSource = BeanUtils.instantiateClass(dataSourceType);
-		ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
-		ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();
-		aliases.addAliases("url", "jdbc-url");
-		aliases.addAliases("username", "user");
-		Binder binder = new Binder(source.withAliases(aliases));
-		binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(dataSource));
-		return dataSource;
-	}
-
-	@SuppressWarnings("unchecked")
-	private Class<? extends DataSource> getDataSourceType(String datasourceType) {
-		if (StringUtils.isNotBlank(datasourceType)) {
-			try {
-				return (Class<? extends DataSource>) ClassUtils.forName(datasourceType, classLoader);
-			} catch (Exception e) {
-				throw new InvalidArgumentException(DATASOURCE_TYPE_NOT_FOUND.format(datasourceType));
-			}
-		}
-		for (String name : DATA_SOURCE_TYPE_NAMES) {
-			try {
-				return (Class<? extends DataSource>) ClassUtils.forName(name, classLoader);
-			} catch (Exception ignored) {
-			}
-		}
-		throw new InvalidArgumentException(DATASOURCE_TYPE_NOT_SET);
+		return new JsonBean<>(magicAPIService.getDataSource(id));
 	}
 }

+ 3 - 91
magic-api/src/main/java/org/ssssssss/magicapi/controller/MagicWorkbenchController.java

@@ -14,7 +14,6 @@ import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 import org.ssssssss.magicapi.adapter.Resource;
-import org.ssssssss.magicapi.adapter.resource.ZipResource;
 import org.ssssssss.magicapi.config.MagicConfiguration;
 import org.ssssssss.magicapi.config.Valid;
 import org.ssssssss.magicapi.exception.MagicLoginException;
@@ -24,11 +23,7 @@ import org.ssssssss.magicapi.logging.MagicLoggerContext;
 import org.ssssssss.magicapi.model.*;
 import org.ssssssss.magicapi.modules.ResponseModule;
 import org.ssssssss.magicapi.modules.SQLModule;
-import org.ssssssss.magicapi.provider.GroupServiceProvider;
 import org.ssssssss.magicapi.provider.MagicAPIService;
-import org.ssssssss.magicapi.provider.StoreServiceProvider;
-import org.ssssssss.magicapi.utils.JsonUtils;
-import org.ssssssss.magicapi.utils.PathUtils;
 import org.ssssssss.script.MagicResourceLoader;
 import org.ssssssss.script.MagicScriptEngine;
 import org.ssssssss.script.ScriptClass;
@@ -133,10 +128,10 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 			return new JsonBean<>(Collections.emptyList());
 		}
 		List<MagicEntity> entities = new ArrayList<>();
-		if (!"2".equals(type)) {
+		if (!Constants.GROUP_TYPE_FUNCTION.equals(type)) {
 			entities.addAll(configuration.getMappingHandlerMapping().getApiInfos());
 		}
-		if (!"1".equals(type)) {
+		if (!Constants.GROUP_TYPE_API.equals(type)) {
 			entities.addAll(configuration.getMagicFunctionManager().getFunctionInfos());
 		}
 		return new JsonBean<>(entities.stream().filter(it -> it.getScript().contains(keyword)).map(it -> {
@@ -194,93 +189,10 @@ public class MagicWorkbenchController extends MagicController implements MagicEx
 	@ResponseBody
 	public JsonBean<Boolean> upload(MultipartFile file, String mode) throws IOException {
 		notNull(file, FILE_IS_REQUIRED);
-		ZipResource root = new ZipResource(file.getInputStream());
-		Set<String> apiPaths = new HashSet<>();
-		Set<String> functionPaths = new HashSet<>();
-		Set<Group> groups = new HashSet<>();
-		Set<ApiInfo> apiInfos = new HashSet<>();
-		Set<FunctionInfo> functionInfos = new HashSet<>();
-		// 检查上传资源中是否有冲突
-		isTrue(readPaths(groups, apiPaths, functionPaths, apiInfos, functionInfos, "/", root), UPLOAD_PATH_CONFLICT);
-		// 判断是否是强制上传
-		if (!"force".equals(mode)) {
-			// 检测与已注册的接口和函数是否有冲突
-			isTrue(!configuration.getMappingHandlerMapping().hasRegister(apiPaths), UPLOAD_PATH_CONFLICT.format("接口"));
-			isTrue(!configuration.getMagicFunctionManager().hasRegister(apiPaths), UPLOAD_PATH_CONFLICT.format("函数"));
-		}
-		Resource item = root.getResource("group.json");
-		GroupServiceProvider groupServiceProvider = configuration.getGroupServiceProvider();
-		if (item.exists()) {
-			Group group = groupServiceProvider.readGroup(item);
-			// 检查分组是否存在
-			isTrue("0".equals(group.getParentId()) || groupServiceProvider.getGroupResource(group.getParentId()).exists(), GROUP_NOT_FOUND);
-			groups.removeIf(it -> it.getId().equalsIgnoreCase(group.getId()));
-		}
-		for (Group group : groups) {
-			Resource groupResource = groupServiceProvider.getGroupResource(group.getId());
-			if (groupResource != null && groupResource.exists()) {
-				groupServiceProvider.update(group);
-			} else {
-				groupServiceProvider.insert(group);
-			}
-		}
-		Resource backups = configuration.getWorkspace().getDirectory(Constants.PATH_BACKUPS);
-		// 保存
-		write(configuration.getApiServiceProvider(), backups, apiInfos);
-		write(configuration.getFunctionServiceProvider(), backups, functionInfos);
-		// 重新注册
-		configuration.getMappingHandlerMapping().registerAllMapping();
-		configuration.getMagicFunctionManager().registerAllFunction();
+		configuration.getMagicAPIService().upload(file.getInputStream(), "force".equals(mode));
 		return new JsonBean<>(SUCCESS, true);
 	}
 
-	private <T extends MagicEntity> void write(StoreServiceProvider<T> provider, Resource backups, Set<T> infos) {
-		for (T info : infos) {
-			Resource resource = configuration.getGroupServiceProvider().getGroupResource(info.getGroupId());
-			resource = resource.getResource(info.getName() + ".ms");
-			byte[] content = provider.serialize(info);
-			resource.write(content);
-			Resource directory = backups.getDirectory(info.getId());
-			if (!directory.exists()) {
-				directory.mkdir();
-			}
-			directory.getResource(System.currentTimeMillis() + ".ms").write(content);
-			resource.write(content);
-		}
-	}
-
-	private boolean readPaths(Set<Group> groups, Set<String> apiPaths, Set<String> functionPaths, Set<ApiInfo> apiInfos, Set<FunctionInfo> functionInfos, String parentPath, Resource root) {
-		Resource resource = root.getResource("group.json");
-		String path = "";
-		if (resource.exists()) {
-			Group group = JsonUtils.readValue(resource.read(), Group.class);
-			groups.add(group);
-			path = Objects.toString(group.getPath(), "");
-			boolean isApi = Constants.GROUP_TYPE_API.equals(group.getType());
-			for (Resource file : root.files(".ms")) {
-				boolean conflict;
-				if (isApi) {
-					ApiInfo info = configuration.getApiServiceProvider().deserialize(file.read());
-					apiInfos.add(info);
-					conflict = !apiPaths.add(Objects.toString(info.getMethod(), "GET") + ":" + PathUtils.replaceSlash(parentPath + "/" + path + "/" + info.getPath()));
-				} else {
-					FunctionInfo info = configuration.getFunctionServiceProvider().deserialize(file.read());
-					functionInfos.add(info);
-					conflict = !functionPaths.add(PathUtils.replaceSlash(parentPath + "/" + path + "/" + info.getPath()));
-				}
-				if (conflict) {
-					return false;
-				}
-			}
-		}
-		for (Resource directory : root.dirs()) {
-			if (!readPaths(groups, apiPaths, functionPaths, apiInfos, functionInfos, PathUtils.replaceSlash(parentPath + "/" + path), directory)) {
-				return false;
-			}
-		}
-		return true;
-	}
-
 	private ResponseEntity<?> download(Resource resource, String filename) throws IOException {
 		ByteArrayOutputStream os = new ByteArrayOutputStream();
 		resource.export(os, Constants.PATH_BACKUPS);

+ 12 - 4
magic-api/src/main/java/org/ssssssss/magicapi/controller/RequestHandler.java

@@ -491,11 +491,19 @@ public class RequestHandler extends MagicController {
 	 * 执行前置拦截器
 	 */
 	private Object doPreHandle(RequestEntity requestEntity) throws Exception {
-		for (RequestInterceptor requestInterceptor : configuration.getRequestInterceptors()) {
-			Object value = requestInterceptor.preHandle(requestEntity);
-			if (value != null) {
-				return value;
+		try {
+			for (RequestInterceptor requestInterceptor : configuration.getRequestInterceptors()) {
+				Object value = requestInterceptor.preHandle(requestEntity);
+				if (value != null) {
+					return value;
+				}
+			}
+		} catch (Exception e) {
+			if(requestEntity.isRequestedFromTest()){
+				// 修正前端显示,原样输出显示
+				requestEntity.getResponse().setHeader(HEADER_RESPONSE_WITH_MAGIC_API, CONST_STRING_FALSE);
 			}
+			throw e;
 		}
 		return null;
 	}

+ 1 - 11
magic-api/src/main/java/org/ssssssss/magicapi/interceptor/SQLInterceptor.java

@@ -11,17 +11,7 @@ public interface SQLInterceptor {
 	/**
 	 * 1.1.1 新增
 	 */
-	default void preHandle(BoundSql boundSql, RequestEntity requestEntity) {
-		preHandle(boundSql);
-	}
+	void preHandle(BoundSql boundSql, RequestEntity requestEntity);
 
 
-	/**
-	 * @see SQLInterceptor#preHandle(BoundSql, RequestEntity)
-	 */
-	@Deprecated
-	default void preHandle(BoundSql boundSql) {
-
-	}
-
 }

+ 3 - 1
magic-api/src/main/java/org/ssssssss/magicapi/model/Constants.java

@@ -107,6 +107,8 @@ public class Constants {
 
 	public static final String MAGIC_TOKEN_HEADER = "Magic-Token";
 
+	public static final String GROUP_METABASE = "group.json";
+
 	/**
 	 * 执行成功的code值
 	 */
@@ -140,7 +142,7 @@ public class Constants {
 	/**
 	 * 通知更新全部
 	 */
-	public static final int NOTIFY_ACTION_ALL = 4;
+	public static final int NOTIFY_ACTION_ALL = 0;
 
 	/**
 	 * 通知接口刷新

+ 48 - 1
magic-api/src/main/java/org/ssssssss/magicapi/model/MagicNotify.java

@@ -26,7 +26,7 @@ public class MagicNotify {
 	}
 
 	public MagicNotify(String from) {
-		this(from, null, Constants.NOTIFY_ACTION_ALL, -1);
+		this(from, null, Constants.NOTIFY_ACTION_ALL, Constants.NOTIFY_ACTION_ALL);
 	}
 
 	public MagicNotify(String from, String id, int action, int type) {
@@ -67,4 +67,51 @@ public class MagicNotify {
 	public void setType(int type) {
 		this.type = type;
 	}
+
+	@Override
+	public String toString() {
+		StringBuilder builder = new StringBuilder();
+		builder.append("MagicNotify(from=");
+		builder.append(from);
+		builder.append(", action=");
+		switch (action) {
+			case Constants.NOTIFY_ACTION_ADD:
+				builder.append("新增");
+				break;
+			case Constants.NOTIFY_ACTION_UPDATE:
+				builder.append("修改");
+				break;
+			case Constants.NOTIFY_ACTION_DELETE:
+				builder.append("删除");
+				break;
+			case Constants.NOTIFY_ACTION_ALL:
+				builder.append("刷新全部");
+				break;
+			default:
+				builder.append("未知");
+		}
+		if(action != Constants.NOTIFY_ACTION_ALL){
+			builder.append(", type=");
+			switch (type) {
+				case Constants.NOTIFY_ACTION_API:
+					builder.append("接口");
+					break;
+				case Constants.NOTIFY_ACTION_FUNCTION:
+					builder.append("函数");
+					break;
+				case Constants.NOTIFY_ACTION_DATASOURCE:
+					builder.append("数据源");
+					break;
+				case Constants.NOTIFY_ACTION_GROUP:
+					builder.append("分组");
+					break;
+				default:
+					builder.append("未知");
+			}
+			builder.append(", id=");
+			builder.append(id);
+		}
+		builder.append(")");
+		return builder.toString();
+	}
 }

+ 44 - 1
magic-api/src/main/java/org/ssssssss/magicapi/provider/MagicAPIService.java

@@ -6,6 +6,8 @@ import org.ssssssss.magicapi.model.FunctionInfo;
 import org.ssssssss.magicapi.model.Group;
 import org.ssssssss.magicapi.model.MagicNotify;
 
+import java.io.IOException;
+import java.io.InputStream;
 import java.util.List;
 import java.util.Map;
 
@@ -42,7 +44,6 @@ public interface MagicAPIService extends MagicModule {
 	/**
 	 * 获取接口详情
 	 * @param id	接口id
-	 * @return
 	 */
 	ApiInfo getApiInfo(String id);
 
@@ -129,6 +130,48 @@ public interface MagicAPIService extends MagicModule {
 	 */
 	List<Group> groupList(String type);
 
+	/**
+	 * 注册数据源
+	 */
+	void registerAllDataSource();
+
+	/**
+	 * 获取数据源详情
+	 * @param id	数据源id
+	 */
+	Map<String, String> getDataSource(String id);
+
+	/**
+	 * 数据源列表
+	 */
+	List<Map<String, Object>> datasourceList();
+
+	/**
+	 * 测试数据源
+	 * @param properties	数据源属性
+	 * @return	返回错误说明,连接正常返回 null
+	 */
+	String testDataSource(Map<String, String> properties);
+
+	/**
+	 * 保存数据源
+	 * @param properties	数据源属性
+	 * @return 返回数据源ID
+	 */
+	String saveDataSource(Map<String, String> properties);
+
+	/**
+	 * 删除数据源
+	 * @param id	数据源id
+	 */
+	boolean deleteDataSource(String id);
+
+
+	/**
+	 * 上传
+	 */
+	void upload(InputStream inputStream, boolean force) throws IOException;
+
 	/**
 	 * 处理刷新通知
 	 */

+ 1 - 9
magic-api/src/main/java/org/ssssssss/magicapi/provider/ResultProvider.java

@@ -92,15 +92,7 @@ public interface ResultProvider {
 	 * @param data          数据内容
 	 */
 	default Object buildPageResult(RequestEntity requestEntity, Page page, long total, List<Map<String, Object>> data) {
-		return buildPageResult(total, data);
-	}
-
-	/**
-	 * @param total 总数
-	 * @param data  数据内容
-	 */
-	@Deprecated
-	default Object buildPageResult(long total, List<Map<String, Object>> data) {
 		return new PageResult<>(total, data);
 	}
+
 }

+ 3 - 5
magic-api/src/main/java/org/ssssssss/magicapi/provider/impl/DefaultGroupServiceProvider.java

@@ -16,7 +16,6 @@ public class DefaultGroupServiceProvider implements GroupServiceProvider {
 
 	private final Map<String, Resource> mappings = new HashMap<>();
 	private final Resource workspace;
-	private final String metabase = "group.json";
 	private Map<String, Group> cacheApiTree = new HashMap<>();
 	private Map<String, Group> cacheFunctionTree = new HashMap<>();
 
@@ -32,7 +31,7 @@ public class DefaultGroupServiceProvider implements GroupServiceProvider {
 		Resource directory = this.getGroupResource(group.getParentId());
 		directory = directory == null ? this.getGroupResource(group.getType(), group.getName()) : directory.getDirectory(group.getName());
 		if (!directory.exists() && directory.mkdir()) {
-			Resource resource = directory.getResource(metabase);
+			Resource resource = directory.getResource(Constants.GROUP_METABASE);
 			if (resource.write(JsonUtils.toJsonString(group))) {
 				mappings.put(group.getId(), resource);
 				return true;
@@ -52,7 +51,7 @@ public class DefaultGroupServiceProvider implements GroupServiceProvider {
 		newResource = newResource == null ? getGroupResource(group.getType(), group.getName()) : newResource.getDirectory(group.getName());
 		// 重命名或移动目录
 		if (oldResource.renameTo(newResource)) {
-			Resource target = newResource.getResource(metabase);
+			Resource target = newResource.getResource(Constants.GROUP_METABASE);
 			if (target.write(JsonUtils.toJsonString(group))) {
 				mappings.put(group.getId(), target);
 				return true;
@@ -112,14 +111,13 @@ public class DefaultGroupServiceProvider implements GroupServiceProvider {
 	public List<Group> groupList(String type) {
 		Resource resource = this.workspace.getDirectory(Constants.GROUP_TYPE_API.equals(type) ? Constants.PATH_API : Constants.PATH_FUNCTION);
 		resource.readAll();
-		List<Group> groups = resource.dirs().stream().map(it -> it.getResource(metabase)).filter(Resource::exists)
+		return resource.dirs().stream().map(it -> it.getResource(Constants.GROUP_METABASE)).filter(Resource::exists)
 				.map(it -> {
 					Group group = JsonUtils.readValue(it.read(), Group.class);
 					mappings.put(group.getId(), it);
 					return group;
 				})
 				.collect(Collectors.toList());
-		return groups;
 	}
 
 	@Override

+ 326 - 7
magic-api/src/main/java/org/ssssssss/magicapi/provider/impl/DefaultMagicAPIService.java

@@ -1,24 +1,50 @@
 package org.ssssssss.magicapi.provider.impl;
 
+import com.fasterxml.jackson.databind.type.TypeFactory;
 import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeanUtils;
+import org.springframework.boot.context.properties.bind.Bindable;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.boot.context.properties.source.ConfigurationPropertyName;
+import org.springframework.boot.context.properties.source.ConfigurationPropertyNameAliases;
+import org.springframework.boot.context.properties.source.ConfigurationPropertySource;
+import org.springframework.boot.context.properties.source.MapConfigurationPropertySource;
+import org.springframework.boot.jdbc.DatabaseDriver;
+import org.springframework.jdbc.datasource.DataSourceUtils;
+import org.springframework.util.ClassUtils;
+import org.ssssssss.magicapi.adapter.Resource;
+import org.ssssssss.magicapi.adapter.resource.ZipResource;
+import org.ssssssss.magicapi.config.MagicDynamicDataSource;
 import org.ssssssss.magicapi.config.MagicFunctionManager;
 import org.ssssssss.magicapi.config.MappingHandlerMapping;
+import org.ssssssss.magicapi.controller.MagicDataSourceController;
+import org.ssssssss.magicapi.exception.InvalidArgumentException;
 import org.ssssssss.magicapi.exception.MagicServiceException;
 import org.ssssssss.magicapi.model.*;
 import org.ssssssss.magicapi.provider.*;
 import org.ssssssss.magicapi.script.ScriptManager;
 import org.ssssssss.magicapi.utils.IoUtils;
+import org.ssssssss.magicapi.utils.JsonUtils;
+import org.ssssssss.magicapi.utils.PathUtils;
 import org.ssssssss.script.MagicResourceLoader;
 import org.ssssssss.script.MagicScript;
 import org.ssssssss.script.MagicScriptContext;
+import org.ssssssss.script.functions.ObjectConvertExtension;
 import org.ssssssss.script.parsing.Scope;
 import org.ssssssss.script.parsing.Span;
 import org.ssssssss.script.parsing.ast.Expression;
 
 import javax.script.ScriptContext;
 import javax.script.SimpleScriptContext;
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.sql.Connection;
 import java.util.*;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstants {
 
@@ -34,22 +60,54 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 
 	private final GroupServiceProvider groupServiceProvider;
 
+	private final MagicDynamicDataSource magicDynamicDataSource;
+
 	private final MagicFunctionManager magicFunctionManager;
 
 	private final MagicNotifyService magicNotifyService;
 
 	private final String instanceId;
 
-	public DefaultMagicAPIService(MappingHandlerMapping mappingHandlerMapping, ApiServiceProvider apiServiceProvider, FunctionServiceProvider functionServiceProvider, GroupServiceProvider groupServiceProvider, ResultProvider resultProvider, MagicFunctionManager magicFunctionManager, MagicNotifyService magicNotifyService, String instanceId, boolean throwException) {
+	private final Resource workspace;
+
+	private final Resource datasourceResource;
+
+	private final static Logger logger = LoggerFactory.getLogger(DefaultMagicAPIService.class);
+
+	private static final ClassLoader classLoader = MagicDataSourceController.class.getClassLoader();
+
+	// copy from DataSourceBuilder
+	private static final String[] DATA_SOURCE_TYPE_NAMES = new String[]{
+			"com.zaxxer.hikari.HikariDataSource",
+			"org.apache.tomcat.jdbc.pool.DataSource",
+			"org.apache.commons.dbcp2.BasicDataSource"};
+
+	public DefaultMagicAPIService(MappingHandlerMapping mappingHandlerMapping,
+								  ApiServiceProvider apiServiceProvider,
+								  FunctionServiceProvider functionServiceProvider,
+								  GroupServiceProvider groupServiceProvider,
+								  ResultProvider resultProvider,
+								  MagicDynamicDataSource magicDynamicDataSource,
+								  MagicFunctionManager magicFunctionManager,
+								  MagicNotifyService magicNotifyService,
+								  String instanceId,
+								  Resource workspace,
+								  boolean throwException) {
 		this.mappingHandlerMapping = mappingHandlerMapping;
 		this.apiServiceProvider = apiServiceProvider;
 		this.functionServiceProvider = functionServiceProvider;
 		this.groupServiceProvider = groupServiceProvider;
 		this.resultProvider = resultProvider;
+		this.magicDynamicDataSource = magicDynamicDataSource;
 		this.magicFunctionManager = magicFunctionManager;
 		this.magicNotifyService = magicNotifyService;
+		this.workspace = workspace;
 		this.throwException = throwException;
 		this.instanceId = StringUtils.defaultIfBlank(instanceId, UUID.randomUUID().toString());
+		this.datasourceResource = workspace.getDirectory(Constants.PATH_DATASOURCE);
+		if (!this.datasourceResource.exists()) {
+			this.datasourceResource.mkdir();
+		}
 		MagicResourceLoader.addFunctionLoader((name) -> {
 			int index = name.indexOf(":");
 			if (index > -1) {
@@ -270,15 +328,14 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 			isTrue(groupServiceProvider.update(group), GROUP_SAVE_FAILURE);
 			// 如果数据库修改成功,则修改接口路径
 			mappingHandlerMapping.updateGroup(group.getId());
-			return true;
+
 		} else if (isFunctionGroup && magicFunctionManager.checkGroup(group)) {
 			isTrue(groupServiceProvider.update(group), GROUP_SAVE_FAILURE);
 			// 如果数据库修改成功,则修改接口路径
 			magicFunctionManager.updateGroup(group.getId());
-			return true;
 		}
 		magicNotifyService.sendNotify(new MagicNotify(instanceId, group.getId(), Constants.NOTIFY_ACTION_UPDATE, Constants.NOTIFY_ACTION_GROUP));
-		return false;
+		return true;
 	}
 
 	@Override
@@ -316,11 +373,162 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		return groupServiceProvider.groupList(type);
 	}
 
+	@Override
+	public void registerAllDataSource() {
+		datasourceResource.readAll();
+		List<Resource> resources = datasourceResource.files(".json");
+		// 删除旧的数据源
+		magicDynamicDataSource.datasourceNodes().stream()
+				.filter(it -> it.getId() != null)
+				.map(MagicDynamicDataSource.DataSourceNode::getKey)
+				.collect(Collectors.toList())
+				.forEach(magicDynamicDataSource::delete);
+		TypeFactory factory = TypeFactory.defaultInstance();
+		for (Resource item : resources) {
+			registerDataSource(JsonUtils.readValue(item.read(), factory.constructMapType(HashMap.class, String.class, String.class)));
+		}
+	}
+
+	private void registerDataSource(Map<String, String> properties){
+		if (properties != null) {
+			String key = properties.get("key");
+			String name = properties.getOrDefault("name", key);
+			String dsId = properties.remove("id");
+			int maxRows = ObjectConvertExtension.asInt(properties.get("maxRows"), -1);
+			magicDynamicDataSource.put(dsId, key, name, createDataSource(properties), maxRows);
+		}
+	}
+
+	@Override
+	public Map<String, String> getDataSource(String id) {
+		Resource resource = this.datasourceResource.getResource(id + ".json");
+		byte[] bytes = resource.read();
+		isTrue(bytes != null && bytes.length > 0, DATASOURCE_NOT_FOUND);
+		TypeFactory factory = TypeFactory.defaultInstance();
+		return JsonUtils.readValue(bytes, factory.constructMapType(LinkedHashMap.class, String.class, String.class));
+	}
+
+	@Override
+	public List<Map<String, Object>> datasourceList() {
+		return magicDynamicDataSource.datasourceNodes().stream().map(it -> {
+			Map<String, Object> row = new HashMap<>();
+			row.put("id", it.getId());    // id为空的则认为是不可修改的
+			row.put("key", it.getKey());    // 如果为null 说明是主数据源
+			row.put("name", it.getName());
+			return row;
+		}).collect(Collectors.toList());
+	}
+
+	@Override
+	public String testDataSource(Map<String, String> properties) {
+		DataSource dataSource = null;
+		try {
+			dataSource = createDataSource(properties);
+			Connection connection = dataSource.getConnection();
+			DataSourceUtils.doCloseConnection(connection, dataSource);
+		} catch (Exception e) {
+			return e.getMessage();
+		} finally {
+			IoUtils.closeDataSource(dataSource);
+		}
+		return null;
+	}
+
+	@Override
+	public String saveDataSource(Map<String, String> properties) {
+		String key = properties.get("key");
+		// 校验key是否符合规则
+		notBlank(key, DATASOURCE_KEY_REQUIRED);
+		isTrue(IoUtils.validateFileName(key), DATASOURCE_KEY_INVALID);
+		String name = properties.getOrDefault("name", key);
+		String id = properties.get("id");
+		Stream<String> keyStream;
+		int action = Constants.NOTIFY_ACTION_UPDATE;
+		if (StringUtils.isBlank(id)) {
+			action = Constants.NOTIFY_ACTION_ADD;
+			keyStream = magicDynamicDataSource.datasources().stream();
+		} else {
+			keyStream = magicDynamicDataSource.datasourceNodes().stream()
+					.filter(it -> !id.equals(it.getId()))
+					.map(MagicDynamicDataSource.DataSourceNode::getKey);
+		}
+		String dsId = StringUtils.isBlank(id) ? UUID.randomUUID().toString().replace("-", "") : id;
+		// 验证是否有冲突
+		isTrue(keyStream.noneMatch(key::equals), DATASOURCE_KEY_EXISTS);
+
+		int maxRows = ObjectConvertExtension.asInt(properties.get("maxRows"), -1);
+		properties.remove("id");
+		// 注册数据源
+		magicDynamicDataSource.put(dsId, key, name, createDataSource(properties), maxRows);
+		properties.put("id", dsId);
+		datasourceResource.getResource(dsId + ".json").write(JsonUtils.toJsonString(properties));
+		magicNotifyService.sendNotify(new MagicNotify(instanceId, dsId, action, Constants.NOTIFY_ACTION_DATASOURCE));
+		return dsId;
+	}
+
+	@Override
+	public boolean deleteDataSource(String id) {
+		// 查询数据源是否存在
+		Optional<MagicDynamicDataSource.DataSourceNode> dataSourceNode = magicDynamicDataSource.datasourceNodes().stream()
+				.filter(it -> id.equals(it.getId()))
+				.findFirst();
+		isTrue(dataSourceNode.isPresent(), DATASOURCE_NOT_FOUND);
+		Resource resource = this.datasourceResource.getResource(id + ".json");
+		// 删除数据源
+		isTrue(resource.delete(), DATASOURCE_NOT_FOUND);
+		// 取消注册数据源
+		dataSourceNode.ifPresent(it -> magicDynamicDataSource.delete(it.getKey()));
+		magicNotifyService.sendNotify(new MagicNotify(instanceId, id, Constants.NOTIFY_ACTION_DELETE, Constants.NOTIFY_ACTION_DATASOURCE));
+		return true;
+	}
+
+	@Override
+	public void upload(InputStream inputStream, boolean force) throws IOException {
+		ZipResource root = new ZipResource(inputStream);
+		Set<String> apiPaths = new HashSet<>();
+		Set<String> functionPaths = new HashSet<>();
+		Set<Group> groups = new HashSet<>();
+		Set<ApiInfo> apiInfos = new HashSet<>();
+		Set<FunctionInfo> functionInfos = new HashSet<>();
+		// 检查上传资源中是否有冲突
+		isTrue(readPaths(groups, apiPaths, functionPaths, apiInfos, functionInfos, "/", root), UPLOAD_PATH_CONFLICT);
+		// 判断是否是强制上传
+		if (!force) {
+			// 检测与已注册的接口和函数是否有冲突
+			isTrue(!mappingHandlerMapping.hasRegister(apiPaths), UPLOAD_PATH_CONFLICT.format("接口"));
+			isTrue(!magicFunctionManager.hasRegister(apiPaths), UPLOAD_PATH_CONFLICT.format("函数"));
+		}
+		Resource item = root.getResource(Constants.GROUP_METABASE);
+		if (item.exists()) {
+			Group group = groupServiceProvider.readGroup(item);
+			// 检查分组是否存在
+			isTrue("0".equals(group.getParentId()) || groupServiceProvider.getGroupResource(group.getParentId()).exists(), GROUP_NOT_FOUND);
+			groups.removeIf(it -> it.getId().equalsIgnoreCase(group.getId()));
+		}
+		for (Group group : groups) {
+			Resource groupResource = groupServiceProvider.getGroupResource(group.getId());
+			if (groupResource != null && groupResource.exists()) {
+				groupServiceProvider.update(group);
+			} else {
+				groupServiceProvider.insert(group);
+			}
+		}
+		Resource backups = workspace.getDirectory(Constants.PATH_BACKUPS);
+		// 保存
+		write(apiServiceProvider, backups, apiInfos);
+		write(functionServiceProvider, backups, functionInfos);
+		// 重新注册
+		mappingHandlerMapping.registerAllMapping();
+		magicFunctionManager.registerAllFunction();
+		magicNotifyService.sendNotify(new MagicNotify(instanceId));
+	}
+
 	@Override
 	public boolean processNotify(MagicNotify magicNotify) {
 		if (magicNotify == null || instanceId.equals(magicNotify.getFrom())) {
 			return false;
 		}
+		logger.info("收到通知消息:{}", magicNotify);
 		String id = magicNotify.getId();
 		int action = magicNotify.getAction();
 		switch (magicNotify.getType()) {
@@ -330,10 +538,19 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 				return processFunctionNotify(id, action);
 			case Constants.NOTIFY_ACTION_GROUP:
 				return processGroupNotify(id, action);
+			case Constants.NOTIFY_ACTION_DATASOURCE:
+				return processDataSourceNotify(id, action);
+			case Constants.NOTIFY_ACTION_ALL:
+				return processAllNotify();
 		}
 		return false;
 	}
 
+	@Override
+	public String getModuleName() {
+		return "magic";
+	}
+
 	private boolean processApiNotify(String id, int action) {
 		// 刷新缓存
 		this.apiList();
@@ -356,6 +573,22 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		return true;
 	}
 
+	private boolean processDataSourceNotify(String id, int action) {
+		if (action == Constants.NOTIFY_ACTION_DELETE) {
+			// 查询数据源是否存在
+			magicDynamicDataSource.datasourceNodes().stream()
+					.filter(it -> id.equals(it.getId()))
+					.findFirst()
+					.ifPresent(it -> magicDynamicDataSource.delete(it.getKey()));
+		} else {
+			// 刷新数据源缓存
+			datasourceResource.readAll();
+			// 注册数据源
+			registerDataSource(getDataSource(id));
+		}
+		return true;
+	}
+
 	private boolean processGroupNotify(String id, int action) {
 		if (action == Constants.NOTIFY_ACTION_ADD) {    // 新增分组
 			// 新增时只需要刷新分组缓存即可
@@ -385,9 +618,95 @@ public class DefaultMagicAPIService implements MagicAPIService, JsonCodeConstant
 		return true;
 	}
 
+	private boolean processAllNotify() {
+		mappingHandlerMapping.registerAllMapping();
+		magicFunctionManager.registerAllFunction();
+		registerAllDataSource();
+		return true;
+	}
 
-	@Override
-	public String getModuleName() {
-		return "magic";
+	// copy from DataSourceBuilder
+	private DataSource createDataSource(Map<String, String> properties) {
+		Class<? extends DataSource> dataSourceType = getDataSourceType(properties.get("type"));
+		if (!properties.containsKey("driverClassName")
+				&& properties.containsKey("url")) {
+			String url = properties.get("url");
+			String driverClass = DatabaseDriver.fromJdbcUrl(url).getDriverClassName();
+			properties.put("driverClassName", driverClass);
+		}
+		DataSource dataSource = BeanUtils.instantiateClass(dataSourceType);
+		ConfigurationPropertySource source = new MapConfigurationPropertySource(properties);
+		ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();
+		aliases.addAliases("url", "jdbc-url");
+		aliases.addAliases("username", "user");
+		Binder binder = new Binder(source.withAliases(aliases));
+		binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(dataSource));
+		return dataSource;
+	}
+
+	@SuppressWarnings("unchecked")
+	private Class<? extends DataSource> getDataSourceType(String datasourceType) {
+		if (StringUtils.isNotBlank(datasourceType)) {
+			try {
+				return (Class<? extends DataSource>) ClassUtils.forName(datasourceType, classLoader);
+			} catch (Exception e) {
+				throw new InvalidArgumentException(DATASOURCE_TYPE_NOT_FOUND.format(datasourceType));
+			}
+		}
+		for (String name : DATA_SOURCE_TYPE_NAMES) {
+			try {
+				return (Class<? extends DataSource>) ClassUtils.forName(name, classLoader);
+			} catch (Exception ignored) {
+			}
+		}
+		throw new InvalidArgumentException(DATASOURCE_TYPE_NOT_SET);
+	}
+
+	private <T extends MagicEntity> void write(StoreServiceProvider<T> provider, Resource backups, Set<T> infos) {
+		for (T info : infos) {
+			Resource resource = groupServiceProvider.getGroupResource(info.getGroupId());
+			resource = resource.getResource(info.getName() + ".ms");
+			byte[] content = provider.serialize(info);
+			resource.write(content);
+			Resource directory = backups.getDirectory(info.getId());
+			if (!directory.exists()) {
+				directory.mkdir();
+			}
+			directory.getResource(System.currentTimeMillis() + ".ms").write(content);
+			resource.write(content);
+		}
 	}
+
+	private boolean readPaths(Set<Group> groups, Set<String> apiPaths, Set<String> functionPaths, Set<ApiInfo> apiInfos, Set<FunctionInfo> functionInfos, String parentPath, Resource root) {
+		Resource resource = root.getResource(Constants.GROUP_METABASE);
+		String path = "";
+		if (resource.exists()) {
+			Group group = JsonUtils.readValue(resource.read(), Group.class);
+			groups.add(group);
+			path = Objects.toString(group.getPath(), "");
+			boolean isApi = Constants.GROUP_TYPE_API.equals(group.getType());
+			for (Resource file : root.files(".ms")) {
+				boolean conflict;
+				if (isApi) {
+					ApiInfo info = apiServiceProvider.deserialize(file.read());
+					apiInfos.add(info);
+					conflict = !apiPaths.add(Objects.toString(info.getMethod(), "GET") + ":" + PathUtils.replaceSlash(parentPath + "/" + path + "/" + info.getPath()));
+				} else {
+					FunctionInfo info = functionServiceProvider.deserialize(file.read());
+					functionInfos.add(info);
+					conflict = !functionPaths.add(PathUtils.replaceSlash(parentPath + "/" + path + "/" + info.getPath()));
+				}
+				if (conflict) {
+					return false;
+				}
+			}
+		}
+		for (Resource directory : root.dirs()) {
+			if (!readPaths(groups, apiPaths, functionPaths, apiInfos, functionInfos, PathUtils.replaceSlash(parentPath + "/" + path), directory)) {
+				return false;
+			}
+		}
+		return true;
+	}
+
 }