开发http接口只需要写SQL就可以了?magic-api:是的

2,639 阅读4分钟

项目背景

在项目开发中,通常是由后端写好接口,前端调用的方式进行开发,而后端开发中,常常需要写Controller,Service,Dao,等一系列方法,而这些方法中,很多都是写一行代码调用,最后返回JSON给前端,而在我们系统开发中,写的比较多的一般都是在SQL(这里不讨论ORM框架)、和前端,其它的都是一写很啰嗦很繁琐的东西,于是我就想着能不能简化这些开发,只写SQL就可以了,经过一番思考,最终还是想到了解决方案

预期想法

采用类似MybatisXML方式定义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-mappingrequest-methodpagevalidate这些属性,从字面意思上也很好理解,分别是请求路径、请求方法、开启分页以及验证。这是最基本的一个查询,如果改用JSON或者YML或者自定义格式,你会发现ifforeach都是一个头疼的问题。设计出来的学习成本肯定会比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,也可以加入交流群讨论)