最近在做数据中台资产管理系统,主要核心功能是数据资产发布成api或者文档,客户通过api直接获取发布的数据资产。需要一下一下几点功能:
1 界面sql编辑数据产品
2. 自动发布数据产品到api
调研后一段时间主要技术有Rocket-api和Dataway(hasor) https://www.hasor.net/doc/display/dataway
简介:
"Rocket-API" 基于spring boot 的API敏捷开发框架,通过写SQL或者 mongodb原始执行脚本代替CRUD完成开发。提升开发效率。
特性:
1. api配置化开发,不再定义Controller
,Service
,Dao
,Mybatis
,xml
,Entity
,VO
等对象和方法.
2. 可视化界面,将入参自动封装到可执行脚本上,支持sql执行语句
3. 完全基于spring boot 2.x作为springboot项目的starter方式集成
4. 在线动态编译,无需重新,立即生效,多数据源操作(静态配置)
5.版本控制,历史记录,回滚功能
6. 代码提示:sql提示,语法提示
7. 远程一建发布到生产环境
8. 线上POSTMAN调试,保存POSTMAN信息或三方文档的自动生成,历史调用记录存储,回塑
9. 用户控制管理,安全性管理
10. 动态数据源管理
工作原理:
1. 将api信息保存到数据库,调用springboot的RequestMapperHandlerMapping.registerMapping/unregisterMapping 实现动态管理RequestMapping。
2. 依赖java 1.8的ScriptEngineManager方法,调用Groovy引擎,赋于数据处理能力以及使代码逻辑能够实现动态编译,发布,而不用重启
搭建:
源码地址: https://gitee.com/alenfive/rocket-api
需要下载2.3.5.RELEAS版本,最新亲测有bug
带入idea,修改application.yml文件的数据库信息
datasource:
url: jdbc:mysql://xxxxxxx:3317/dap?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: myuser
password: mypassword
driver-class-name: com.mysql.cj.jdbc.Driver
配置数据源:
@Component
public class DefaultDataSourceManager extends DataSourceManager {
@Autowired
private JdbcTemplate jdbcTemplate;
@Autowired
private MongoTemplate mongoTemplate;
@PostConstruct
public void init() {
Map<String, DataSourceDialect> dialects = new LinkedHashMap<>();
dialects.put("mysql",new MySQLDataSource(jdbcTemplate,true));
//dialects.put("postgres",new PostgreSQLDataSource(jdbcTemplate,false));
//dialects.put("oracle",new OracleDataSource(jdbcTemplate,true));
// dialects.put("mongodb",new MongoDataSource(mongoTemplate,true));
super.setDialectMap(dialects);
}
}
创建数据库:
CREATE TABLE `api_info` (
`id` varchar(45) NOT NULL,
`method` varchar(45) DEFAULT NULL,
`path` varchar(100) DEFAULT NULL,
`type` varchar(5) DEFAULT NULL COMMENT '类型:CODE,QL',
`service` varchar(45) DEFAULT NULL,
`editor` varchar(45) DEFAULT NULL,
`name` varchar(200) DEFAULT NULL,
`datasource` varchar(45) DEFAULT NULL,
`script` text,
`options` text,
`create_time` varchar(45) DEFAULT NULL,
`full_path` varchar(200) DEFAULT NULL,
`directory_id` varchar(45) DEFAULT NULL,
`update_time` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `unique_path_method` (`service`,`full_path`,`method`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='路径明细';
CREATE TABLE `api_info_history` (
`id` varchar(45) NOT NULL,
`api_info_id` varchar(45) NOT NULL,
`method` varchar(45) DEFAULT NULL,
`path` varchar(100) DEFAULT NULL,
`type` varchar(5) DEFAULT NULL COMMENT '类型:CODE,QL',
`service` varchar(45) DEFAULT NULL,
`editor` varchar(45) DEFAULT NULL,
`name` varchar(200) DEFAULT NULL,
`datasource` varchar(45) DEFAULT NULL,
`script` text,
`options` text,
`create_time` varchar(45) DEFAULT NULL,
`full_path` varchar(200) DEFAULT NULL,
`directory_id` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='路径明细历史';
CREATE TABLE `api_example` (
`id` varchar(45) NOT NULL,
`api_info_id` varchar(45) NOT NULL,
`method` varchar(45) DEFAULT NULL,
`url` text,
`request_header` text,
`request_body` text,
`response_header` text,
`response_body` text,
`status` varchar(10) DEFAULT NULL,
`elapsed_time` int(11) DEFAULT NULL,
`options` text,
`editor` varchar(45) DEFAULT NULL,
`create_time` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `index_api_id` (`api_info_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='模拟数据';
//在开启了spring.rocket-api.config-enabled=true才需要
CREATE TABLE `api_config` (
`id` varchar(45) NOT NULL,
`service` varchar(45) NOT NULL,
`config_context` text,
PRIMARY KEY (`id`),
UNIQUE KEY `key_UNIQUE` (`id`),
UNIQUE KEY `service_UNIQUE` (`service`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `api_directory` (
`id` varchar(45) NOT NULL,
`name` varchar(45) DEFAULT NULL,
`path` varchar(200) DEFAULT NULL,
`parent_id` varchar(20) DEFAULT NULL,
`service` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
编译,重启,访问地址:http://localhost:8080/interface-ui
操作:
创建request:
文档同步:
实现与 yapi 等文档中心的对接
远程发布:
增量同步
对同步的数据存在更新,不存在新增,不会对其他API进行清除
全量同步
对目前库进行清空,导入同步的数据,会对其他API进行清除
数据流:
服务启动:
spring boot启动 -->
QLRequestMappingFactory.init() -->
注册脚本解析类dataSourceManager.setParseService(parseService)-->
加载数据库api dataSourceManager.listApiInfoByEntity(ApiInfo.builder().service(service).build()) -->
加载代码方式的API getPathListForCode(); -->
注册mapping registerMappingForApiInfo(apiInfo) -->
requestMappingHandlerMapping.registerMapping(mappingInfo,this, targetMethod);-->
AbstractHandlerMethodMapping<T>.MappingRegistry.register(mapping, handler, method);
初始化界面:
/**
* Api ui 页面显示
*/
@Controller
@RequestMapping("${spring.rocket-api.base-register-path:/interface-ui}")
@ConditionalOnProperty(name = "spring.rocket-api.view-enabled",havingValue = "true",matchIfMissing = true)
public class ViewController {
@GetMapping
public String index(Model model, HttpServletRequest request){
model.addAttribute("dataSourceList",dataSourceManager.getDialectMap().keySet());
model.addAttribute("service", service);
model.addAttribute("configEnabled",properties.isConfigEnabled());
model.addAttribute("version", PackageUtils.getVersion());
if (request.getRequestURI().endsWith("/")){
return "redirect:"+properties.getBaseRegisterPath();
}
return "rocketapi/api-index";
}
}
API 展现
/**
* LOAD API LIST
*/
@GetMapping("/api-list")
public ApiResult getPathList(boolean isDb) throws Exception {
return ApiResult.success(mappingFactory.getPathList(isDb).stream().map(item->{
Map<String,Object> newItem = new HashMap<>();
newItem.put("id",item.getId());
newItem.put("groupName",item.getGroupName());
newItem.put("name",item.getName());
newItem.put("method",item.getMethod());
newItem.put("path",item.getPath());
newItem.put("options",item.getOptions());
newItem.put("datasource",item.getDatasource());
return newItem;
}).collect(Collectors.toList()));
}
获取组件工具类信息: 前端缓存,自动提示
/**
* 自动完成,类型获取
*/
@GetMapping("/completion-items")
public ApiResult provideCompletionTypes() throws Exception {
String cacheKey = "completion-items-cache";
CompletionResult result = null;
if ((result = (CompletionResult) cache.get(cacheKey)) != null){
return ApiResult.success(result);
}
result = new CompletionResult();
Map<String,List<MethodVo>> clazzs = new LinkedHashMap<>();
Map<String,String> variables = new HashMap<>();
Map<String,String> syntax = new HashMap<>();
Map<String,List<TableInfo>> dbInfos = new HashMap<>();
result.setClazzs(clazzs);
result.setVariables(variables);
result.setSyntax(syntax);
result.setDbInfos(dbInfos);
//获取内置自定义函数变量
Collection<IFunction> functionList = context.getBeansOfType(IFunction.class).values();
functionList.forEach(item->{
variables.put(item.getVarName(),item.getClass().getName());
});
//spring bean对象获取
Map<String,Object> beans = context.getBeansOfType(Object.class);
for (String key : beans.keySet()){
buildClazz(clazzs,beans.get(key).getClass());
}
//本包JAVA类
List<Class> classList = PackageUtil.loadClassByLoader(Thread.currentThread().getContextClassLoader());
for (Class clazz : classList){
buildClazz(clazzs,clazz);
}
//基础包 java.util java类
List<String> classNames = PackageUtil.scan();
for (String clazz : classNames){
buildClazz(clazzs,clazz);
}
//常用语法提示
syntax.put("foreach","for(item in ${1:collection}){\n\t\n}");
syntax.put("fori","for(${1:i}=0;${1:i}<;${1:i}++){\n\t\n}");
syntax.put("for","for(${1}){\n\t\n}");
syntax.put("if","if(${1:condition}){\n\n}");
syntax.put("ifelse","if(${1:condition}){\n\t\n}else{\n\t\n}");
syntax.put("import","import ");
syntax.put("continue","continue;");
syntax.put("break","break;");
//数据库信息获取
Map<String, DataSourceDialect> dataSourceDialectMap = dataSourceManager.getDialectMap();
dataSourceDialectMap.forEach((key,value)->{
List<TableInfo> tableInfos = value.buildTableInfo();
if (tableInfos != null){
dbInfos.put(key,tableInfos);
}
});
//常用工具类获取
cache.put(cacheKey,result);
return ApiResult.success(result);
}
新建api:
/**
* SAVE APIINFO
* @param apiInfo
*/
@PostMapping("/api-info")
public ApiResult saveOrUpdateApiInfo(@RequestBody ApiInfo apiInfo,HttpServletRequest request) {
String user = loginService.getUser(request);
if(StringUtils.isEmpty(user)){
return ApiResult.fail("Permission denied");
}
apiInfo.setEditor(user);
try {
if (!StringUtils.isEmpty(apiInfo.getScript())){
apiInfo.setScript(scriptEncrypt.encrypt(apiInfo.getScript()));
}
return ApiResult.success(mappingFactory.saveOrUpdateApiInfo(apiInfo));
}catch (Exception e){
e.printStackTrace();
return ApiResult.fail(e.getMessage());
}
}
@Transactional
public String saveOrUpdateApiInfo(ApiInfo apiInfo) throws Exception {
if (existsPattern(apiInfo)){
throw new IllegalArgumentException("method: "+apiInfo.getMethod()+" path:"+apiInfo.getPath()+" already exist");
}
ApiInfo dbInfo = apiInfoCache.getAll().stream().filter(item->item.getId().equals(apiInfo.getId())).findFirst().orElse(null);
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
apiInfo.setUpdateTime(sdf.format(new Date()));
if (dbInfo == null){
apiInfo.setType(ApiType.Ql.name());
apiInfo.setCreateTime(sdf.format(new Date()));
apiInfo.setService(service);
apiInfo.setId(GenerateId.get().toHexString());
dataSourceManager.saveApiInfo(apiInfo);
}else{
apiInfo.setType(dbInfo.getType());
apiInfo.setCreateTime(dbInfo.getCreateTime());
apiInfo.setService(dbInfo.getService());
dataSourceManager.updateApiInfo(apiInfo);
//取消mapping注册
unregisterMappingForApiInfo(dbInfo);
//清理缓存
apiInfoCache.remove(dbInfo);
}
dbInfo = dataSourceManager.findApiInfoById(apiInfo);
//入缓存
apiInfoCache.put(dbInfo);
//注册mapping
this.registerMappingForApiInfo(dbInfo);
//存储历史
saveApiHistory(dbInfo);
return dbInfo.getId();
}
测试api:
/**
* 脚本执行
*/
@PostMapping("/api-info/run")
public ApiResult runScript(@RequestBody RunApiReq runApiReq, HttpServletRequest request){
String user = loginService.getUser(request);
if(StringUtils.isEmpty(user)){
return ApiResult.fail("Permission denied");
}
RunApiRes runApiRes = new RunApiRes();
try {
apiInfoContent.setIsDebug(runApiReq.isDebug());
ApiInfo apiInfo = ApiInfo.builder()
.path(runApiReq.getPattern())
.options(runApiReq.getOptions())
.datasource(runApiReq.getDatasource())
.script(runApiReq.getScript())
.build();
ApiParams apiParams = ApiParams.builder()
.header(decodeHeaderValue(runApiReq.getHeader()))
.pathVar(getPathVar(runApiReq.getPattern(),runApiReq.getUrl()))
.param(getParam(runApiReq.getUrl()))
.body(buildBody(runApiReq.getBody()))
.session(RequestUtils.buildSessionParams(request))
.build();
Object value = scriptParse.runScript(apiInfo.getScript(),apiInfo,apiParams);
runApiRes.setData(value);
return ApiResult.success(runApiRes);
}catch (Throwable e){
e.printStackTrace();
return ApiResult.fail(e.getMessage(),runApiRes);
}finally {
runApiRes.setLogs(apiInfoContent.getLogs());
apiInfoContent.removeAll();
}
}
@Override
@Transactional(rollbackFor=Exception.class)
public Object runScript(String script, ApiInfo apiInfo, ApiParams apiParams) throws Throwable {
Integer pageNo = buildPagerNo(apiParams);
Integer pageSize = buildPagerSize(apiParams);
apiParams.putParam(apiPager.getPageNoVarName(),pageNo);
apiParams.putParam(apiPager.getPageSizeVarName(),pageSize);
apiParams.putParam(apiPager.getIndexVarName(),apiPager.getIndexVarValue(pageSize,pageNo));
try {
//注入变量
apiInfoContent.setApiInfo(apiInfo);
apiInfoContent.setApiParams(apiParams);
Bindings bindings = new SimpleBindings();
apiInfoContent.setEngineBindings(bindings);
for(IFunction function : functionList){
bindings.put(function.getVarName(),function);
}
//注入属性变量
buildScriptParams(bindings,apiParams);
Object result = this.engineEval(script,bindings);
return result;
}catch (Exception e){
if (e.getCause() != null && e.getCause().getCause() != null){
throw e.getCause().getCause();
}else{
throw e;
}
}
}
@Override
public Object engineEval(String script,Bindings bindings) throws Throwable {
try {
return engine.eval(script,bindings);
}catch (Exception e){
if (e.getCause() != null && e.getCause().getCause() != null){
throw e.getCause().getCause();
}else{
throw e;
}
}
}
javax.script.ScriptEngine.eval(String script, Bindings n)
文档同步:
/**
* API DOC 同步
*/
@GetMapping("/api-doc-push")
public ApiResult apiDocPush(String apiInfoId) throws Exception {
Collection<ApiInfo> apiInfos = mappingFactory.getPathList(false);
String result = null;
if (!StringUtils.isEmpty(apiInfoId)){
ApiInfo apiInfo = apiInfos.stream().filter(item->item.getId().equals(apiInfoId)).findFirst().orElse(null);
result = apiDocSync.sync(apiInfo,buildLastApiExample(apiInfo.getId()));
}else{
for(ApiInfo apiInfo : apiInfos){
result = apiDocSync.sync(apiInfo,buildLastApiExample(apiInfo.getId()));
}
}
return ApiResult.success(result);
}
/**
* 默认API信息接口同步,
*/
@Slf4j
@Component
public class DefaultApiDocSync implements IApiDocSync {
@Override
public String sync(ApiInfo apiInfo, ApiExample apiExample) {
return "Successful push";
}
}
啥也没做
源码:
总结
修改