项目背景
在项目开发中,通常是由后端写好接口,前端调用的方式进行开发,而后端开发中,常常需要写Controller,Service,Dao,等一系列方法,而这些方法中,很多都是写一行代码调用,最后返回JSON给前端,而在我们系统开发中,写的比较多的一般都是在SQL(这里不讨论ORM框架)、和前端,其它的都是一写很啰嗦很繁琐的东西,于是我就想着能不能简化这些开发,只写SQL就可以了,经过一番思考,最终还是想到了解决方案
预期想法
采用类似Mybatis的XML方式定义SQL语句和请求路径以及其他信息、解析XML、注册HTTP接口、执行的简单方法来实现。
为什么使用XML而不用其他格式
我们先看一下XML的写法
<validate id="rule1">
<!-- 验证参数username -->
<param name="username" code="50" message="自定义错误提示">
<min-len value="6">username最小长度为6</min-len>
</param>
</validate>
<select-one request-mapping="/list" request-method="get" page="true" validate="rule1">
select
<!-- 引用自定义的sql片段 -->
<include refid="customSql"/>
from sys_user
where 1=1
<if test="username != null and username != ''">
and username like concat('%',#{username},'%')
</if>
<if test="roleIds != null and roleIds != ''">
and role_id in
<foreach collection="roleIds.split(',')" item="roleId" open="(" close=")" separator=",">
#{roleId}
</foreach>
</if>
order by create_date desc
</select-one>
首先格式上选择了与Mybatis基本一致的写法,好理解,额外的我们发现有request-mapping、request-method、page、validate这些属性,从字面意思上也很好理解,分别是请求路径、请求方法、开启分页以及验证。这是最基本的一个查询,如果改用JSON或者YML或者自定义格式,你会发现if和foreach都是一个头疼的问题。设计出来的学习成本肯定会比XML高,因为XML的方式跟Mybatis的方式很像,学习成本很低
代码实现
项目启动之后解析XML文件
// 代码节选自org.ssssssss.magicapi.utils.XmlFileLoader
// 提取所有符合表达式的XML文件
Resource[] resources = resourceResolver.getResources(pattern);
for (Resource resource : resources) {
File file = resource.getFile();
// 获取上次修改时间
Long lastModified = fileMap.get(resource.getDescription());
// 修改缓存
fileMap.put(resource.getDescription(), file.lastModified());
// 判断是否更新
if (lastModified == null || lastModified < file.lastModified()) {
XMLStatement xmlStatement = S8XMLFileParser.parse(file);
// 注册HTTP接口
xmlStatement.getStatements().forEach(configuration::addStatement);
}
}
通过RequestMappingHandlerMapping的registerMapping方法注册接口
/**
* 代码节选自org.ssssssss.magicapi.session.Configuration
* 注册Statement成接口,当已存在时,刷新其配置
*/
public void addStatement(Statement statement) {
RequestMappingInfo requestMappingInfo = getRequestMappingInfo(statement);
if (StringUtils.isNotBlank(statement.getId())) {
// 设置ID与statement的映射
statementIdMap.put(statement.getId(), statement);
}
if (requestMappingInfo == null) {
return;
}
// 如果已经注册过,则先取消注册(主要是热更新)
if (statementMappingMap.containsKey(statement.getRequestMapping())) {
logger.debug("刷新接口:{}", statement.getRequestMapping());
// 取消注册
requestMappingHandlerMapping.unregisterMapping(requestMappingInfo);
}else{
logger.debug("注册接口:{}", statement.getRequestMapping());
}
// 添加至缓存
statementMappingMap.put(statement.getRequestMapping(), statement);
// 注册接口
requestMappingHandlerMapping.registerMapping(requestMappingInfo,requestHandler,statement.isRequestBody() ? requestWithRequestBodyHandleMethod : requestHandleMethod);
}
至此已经可以把XML中写的配置给映射出HTTP接口
请求处理
/**
* 代码节选自org.ssssssss.magicapi.executor.RequestExecutor
* http请求入口
* @param request
* @return
*/
@ResponseBody
public Object invoke(HttpServletRequest request) {
return invoke(request, null);
}
/**
* http请求入口(带RequestBody)
*/
@ResponseBody
public Object invoke(HttpServletRequest request, @RequestBody(required = false) Object requestBody) {
try {
// 创建RequestContex对象,供后续使用
RequestContext requestContext = new RequestContext(request, expressionEngine);
if (!requestContext.containsKey("body")) {
requestContext.setRequestBody(requestBody);
}
Statement statement = configuration.getStatement(requestContext.getRequestMapping());
requestContext.setStatement(statement);
// 执行前置拦截器
for (RequestInterceptor requestInterceptor : requestInterceptors) {
Object value = requestInterceptor.preHandle(requestContext);
if (value != null) {
return value;
}
}
// 执行校验
Object value = validate(statement, requestContext);
if (value != null) {
return value;
}
// 执行语句
value = new JsonBean<>(statementExecutor.execute(statement, requestContext));
// 执行后置拦截器
for (RequestInterceptor requestInterceptor : requestInterceptors) {
Object target = requestInterceptor.postHandle(requestContext, value);
if (target != null) {
return target;
}
}
return value;
} catch (Exception e) {
if (configuration.isThrowException()) {
throw new MagicAPIException("magic-api执行出错", e);
}
logger.error("系统出现错误", e);
return new JsonBean<>(-1, e.getMessage());
}
}
至此,整个流程基本结束。
最后有额外的实现了spring-boot-starter,进一步简化配置与开发
使用方式
maven引入
<!-- 以spring-boot-starter的方式引用 -->
<dependency>
<groupId>org.ssssssss</groupId>
<artifactId>magic-api-spring-boot-starter</artifactId>
<version>0.1.1</version>
</dependency>
修改application.properties
server.port=9999
#配置magic-api的xml所在位置
magic-api.xml-locations: classpath*:magic-api/*.xml
#以下配置需跟实际情况修改
spring.datasource.url=jdbc:mysql://localhost/test
spring.datasource.username=root
spring.datasource.password=123456789
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
创建XML
在src/main/resources/magic-api/下建立user.xml文件
<?xml version="1.0" encoding="utf-8" ?>
<magic request-mapping="/user"
xmlns="http://ssssssss.org/schema"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://ssssssss.org/schema http://ssssssss.org/schema/magic-0.1.xsd">
<!-- 访问地址/user/list,访问方法get,并开启分页 -->
<select-list request-mapping="/list" request-method="get" page="true">
select username,password from sys_user
</select-list>
</magic>
测试
访问http://localhost:9999/user/list
结果如下:
{
"code": 1,
"message": "success",
"data": {
"total": 2,
"list": [{
"password": "123456",
"username": "admin"
}, {
"password": "1234567",
"username": "1234567"
}]
},
"timestamp": 1588586539249
}
结语
目前还有几处需要优化和改进,也欢迎提出意见和建议(可以在评论区留言,也可以在gitee/github上提issues,也可以加入交流群讨论)
- 支持存储过程(待改进)
- 增加可视化操作界面(待改进)
- 单表自动映射CRUD接口(待改进)
- 文档地址:ssssssss.org
- Gitee:gitee.com/ssssssss-te…
- Github:github.com/ssssssss-te…