通过直接写SQL的方式实现快速提供接口

基于jfinal 实现的dataway插件 ,用于编写sql 即可直接提供接口

插件背景

再大多数的应用中,我们的接口主要是对数据库的经行简单的增删改查,而大多数的的操作都是都可以直接通一条sql就结束了,并没有业务逻辑,因此我们再写接口的时候是否有一种可以不书写controller +service+ dao 即可实现接口提供。再spring生态中就有DataWay 这个项目用于实现通过书sql 提供接口。

插件原理

jfinal的handler + sql模板引擎+ monaco-editor+ mysql, 通过拦截地址,根据请求地址去数据去查询改接口需要执行的sql,通过DB执行后,返回即可。具体组件说明

jfinal 的hanlder : 为什么选择handler而不是拦截器。因为通过自定义的接口地址找不到对应Java的方法,如果使用拦截器,就会提示404 错误
sql模板引擎: 可以执行普通sql ,也可以使用:Db.templateByString()来执行带参数的sql
monaco-editor :前端的代码编辑插件,用于语法高亮显示
mysql: 用于储存接口基本型,包括接口地址,执行sql,执行方法,返回类型等

具体步骤

  1. 创建数据库,具体表结构如下

  2. CREATE TABLE core_api  (
     id int NOT NULL AUTO_INCREMENT COMMENT '主键',
     name varchar(100) COMMENT '接口的唯一名称',
     url varchar(255)  NOT NULL COMMENT 'url 地址',
     sql text  NOT NULL COMMENT '需要执行的模板sql',
     method varchar(20)  NOT NULL DEFAULT 'GET' COMMENT 'get,put,delete,post',
     type int NOT NULL DEFAULT 0 COMMENT 'get请求方式返回数据类型,0: 自动判断,1:对象,2:集合',
     state int NOT NULL DEFAULT 0 COMMENT '0:新建,1:已测试,2:已发布,3:取消发布',
     des varchar(300)  NOT NULL COMMENT '该接口的描述',
      updateTime datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) COMMENT '最后更新时间',
      PRIMARY KEY (id) USING BTREE,
      UNIQUE INDEX unique_index_url(url) USING BTREE COMMENT '唯一索引',
      UNIQUE INDEX unique_index_name(name) USING BTREE COMMENT '唯一索引'
    ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;
  3. 创建ApiHandler 再ApiHandler 中实现具体执行sql,该handler 实现包括了GET,POST,PUT,DELETE ,分别对应select ,insert,update,delete,具体实现请参考https://gitee.com/HingLo-C/PermissionAdmin/blob/master/src/main/java/cn/hinglo/common/handler/ApiHandler.java

  4. 效果图如下(管理页面与创建页面效果图)

image.png

  1. 创建与编辑页面效果图image.png

  2. 核心代码如下

/**
 * @author HingLo
 * @createTime: 2020/9/7 21:15
 * @description: 接口拦截器的
 */
public class ApiHandler extends Handler {

    /***
     * API前缀,用于对该前缀的信息进行拦截
     */
    private String preUrl;
    /***
     * API 信息缓存名称
     */
    private static String URL_CACHE_NAME = "urlCacheName";

    public ApiHandler(String preUrl) {
        this.preUrl = preUrl;
    }

    @Override
    public void handle(String target, HttpServletRequest request, HttpServletResponse response, boolean[] isHandled) {
        if (StrUtil.startWith(target, preUrl)) {
            String apiName = request.getParameter(SystemConstant.API_NAME);
            Record record = new Record();
            if (StrUtil.isBlank(apiName)) {
                record = getRequestApi(target);
            } else {
                record.setColumns(this.getKv(request));
                record.set("type", Integer.valueOf(record.getStr("type")));
            }
            if (record != null) {
                // 权限校验
                if (!this.hasPermission(request, response)) {
                    this.printWriter(response, ResultKit.error("无权限访问该资源"));
                } else {
                    String sql = getRealSql(record.getStr("sql"));
                    Integer type = record.getInt("type");
                    String method = record.getStr("method");
                    String method1 = request.getMethod();
                    try {
                        if (!method.equals(method1)) {
                            this.printWriter(response, ResultKit.error(405, "请求方法不支持,请联系管理员"));
                        } else if ("GET".equals(method)) {
                            this.getMethod(request, response, sql, type);
                        } else if ("POST".equals(method)) {
                            this.postMethod(request, response, sql);
                        } else if ("PUT".equals(method)) {
                            this.putMethod(request, response, sql);
                        } else if ("DELETE".equals(method)) {
                            this.deleteMethod(request, response, sql);
                        }
                    } catch (Exception e) {
                        this.printWriter(response, ResultKit.error(e.getMessage()));
                    }
                }
                // 结束直接调用,防止多次调用响应流
                isHandled[0] = true;
            } else {
                next.handle(target, request, response, isHandled);
            }
        } else {
            next.handle(target, request, response, isHandled);
        }
    }

    /***
     * 通过URL获取 该API相关信息
     * @param url 请求的URL
     * @return 返回结果
     */
    private Record getRequestApi(String url) {
        // 移除前缀
        url = StrUtil.removePrefix(url, this.preUrl);
        Record record = SelfCacheKit.get(URL_CACHE_NAME, url);
        if (record == null) {
            record = Db.findFirst("select * from core_api where state=2 and url=?", url);
            if (record != null) {
                SelfCacheKit.put(URL_CACHE_NAME, url, record);
            }
        }
        return record;
    }

    /***
     * 获取所有参数
     * @param request 请求对象
     * @return 返回结果
     */
    private Kv getKv(HttpServletRequest request) {
        Kv kv = new Kv();
        Map<String, String[]> paraMap = request.getParameterMap();
        for (Map.Entry<String, String[]> entry : paraMap.entrySet()) {
            String[] values = entry.getValue();
            String value = (values != null && values.length > 0) ? values[0] : null;
            kv.put(entry.getKey(), "".equals(value) ? null : value);
        }
        return kv;
    }

    /***
     * 响应json 数据
     * @param response 响应对象
     */
    @SneakyThrows
    private void printWriter(HttpServletResponse response, Result<Object> result) {
        response.setStatus(HttpServletResponse.SC_OK);
        response.setCharacterEncoding("utf-8");
        PrintWriter out = response.getWriter();
        response.setContentType("application/json; charset=UTF-8");
        out.println(JsonKit.toJson(result));
        out.flush();
    }

    /***
     *  权限校验规则方法
     * @param request 请求信息
     * @param response 响应信息
     * @return 是否通过校验
     */
    private boolean hasPermission(HttpServletRequest request, HttpServletResponse response) {


        return true;
    }

    /***
     * GET 请求方式
     * @param request 请求信息
     * @param response 响应对象
     * @param sql sql
     * @param type 返回类型
     */
    private void getMethod(HttpServletRequest request, HttpServletResponse response, String sql, Integer type) {
        if (!StrUtil.startWith(StrUtil.trim(sql), "select")) {
            this.printWriter(response, ResultKit.error("请求方式与执行SQL不匹配"));
            return;
        }
        Kv kv = this.getKv(request);
        List<Record> result = Db.templateByString(sql, kv).find();
        if (result.size() == 0) {
            this.printWriter(response, ResultKit.success(result));
        } else {
            if (type == 0) {
                if (result.size() > 1) {
                    this.printWriter(response, ResultKit.success(result));
                } else {
                    this.printWriter(response, ResultKit.success(result.get(0)));
                }
            } else if (type == 1) {
                if (result.size() == 1) {
                    this.printWriter(response, ResultKit.success(result.get(0)));
                } else {
                    this.printWriter(response, ResultKit.error("返回类型与数据不匹配"));
                }
            } else {
                this.printWriter(response, ResultKit.success(result));
            }
        }
    }

    /***
     * POST 请求方式,其参数使用body 方式
     * @param request 请求信息
     * @param response 响应信息
     * @param sql 执行sql
     */
    private void postMethod(HttpServletRequest request, HttpServletResponse response, String sql) {
        if (!StrUtil.startWith(StrUtil.trim(sql), "insert")) {
            this.printWriter(response, ResultKit.error("请求方式与执行SQL不匹配"));
            return;
        }
        int update = 0;
        String header = request.getHeader("Content-Type");
        if (header.contains("application/json")) {
            String body = HttpKit.readData(request);
            JSONObject jsonObject = JSONUtil.parseObj(body);
            update = Db.templateByString(sql, jsonObject).update();
        } else {
            Kv kv = this.getKv(request);
            update = Db.templateByString(sql, kv).update();
        }
        this.printWriter(response, ResultKit.success(update));
    }

    /***
     * 更新操作
     * @param request 请求信息
     * @param response 响应信息
     * @param sql 执行sql模板
     */
    private void putMethod(HttpServletRequest request, HttpServletResponse response, String sql) {
        if (!StrUtil.startWith(StrUtil.trim(sql), "update")) {
            this.printWriter(response, ResultKit.error("请求方式与执行SQL不匹配"));
            return;
        }
        Kv kv = this.getKv(request);
        int update = Db.templateByString(sql, kv).update();
        this.printWriter(response, ResultKit.success(update));
    }

    /***
     * 删除操作
     * @param request 请求信息
     * @param response 响应信息
     * @param sql 执行sql模板
     */
    private void deleteMethod(HttpServletRequest request, HttpServletResponse response, String sql) {
        if (!StrUtil.startWith(StrUtil.trim(sql), "delete")) {
            this.printWriter(response, ResultKit.error("请求方式与执行SQL不匹配"));
            return;
        }
        Kv kv = this.getKv(request);
        int delete = Db.templateByString(sql, kv).delete();
        this.printWriter(response, ResultKit.success(delete));
    }


    /***
     * 获取真实的sql,去掉了注释
     * @param sql sql
     * @return 返回结构
     */
    public static String getRealSql(String sql) {
        String[] split = sql.split("[\r\n]");
        StringBuilder stringBuilder = new StringBuilder();
        for (String s : split) {
            if (StrUtil.isNotBlank(s) && !StrUtil.startWith(s, "--") && !StrUtil.startWith(s, "//")) {
                stringBuilder.append(s).append(" ");
            }
        }
        return stringBuilder.toString();
    }

}


评论区

HingLo

2020-09-24 14:22

由于使用的是markdown格式编写。代码有点乱,请直接参考地址代码

山东小木

2020-09-25 09:11

风险 安全性做好就可以

HingLo

2020-09-25 10:06

@山东小木 更多用于select操作,相对来说风险低一些。并支持自定义安全校验

山东小木

2020-09-25 18:26

每个接口都可以定义自己的sql模板 存在数据库里?还是每次请求 都传递一个sql模板+参数?

HingLo

2020-09-27 09:05

@山东小木 sql模板存在数据库里,每次只需要传参数就可以了