五步搭建自己的低代码平台

前言

平时开发项目时,总会写很多crud代码,开发过程基本一个套路,定义controller、service、dao、mapper、dto,感觉一直在repeat yourself

也接触过很多快速开发框架,定义一个sql就可以生成接口,或者定义一个框架脚本自动生成接口,但感觉这些框架没有说太成熟广泛使用的,出了问题也很难解决

本文重点研究一下如何只通过定义sql就自动生成接口,但是只是简单实现,为提供思路,毕竟真的实现高可用性工作量很大

思路

再实现之前,首先屡清一下思路,使用springboot+swagger2, 大概分为以下5个步骤

  • 数据源信息的配置及测试连接
    url,用户名,密码等信息
  • 自定义接口信息的配置
    路径,请求方式,参数,使用数据源, sql脚本等信息
  • 注册spring接口
    需按自定义的接口信息动态生成一个spring访问路径
  • 执行sql并返回
    接口请求时,执行自定义接口设置的sql脚本,并将结果返回json
  • 注册swgger2接口(这一步也可以不要)
    把自定义的接口发布到swagger2文档中

实现

思路研究好,开始实现

数据源

作为一个低代码平台,我们希望数据源(即数据库)是可配的,并且不同的接口可以访问不同的数据源

在维护一个数据源表,主要字段如下

public class Source {

    /**
     * 数据源key
     */
    private String key;

    /**
     * 数据源名称
     */
    private String name;

    /**
     * 类型
     */
    private DbTypeEnum type;

    /**
     * jdbc URL
     */
    private String url;

    /**
     * 用户名
     */
    private String username;

    /**
     * 密码
     */
    private String password;

}

其中DbType我做的简单一点,只支持mysql和orcale

public enum DbTypeEnum {
    MYSQL(0, "MYSQL"),
    ORACLE(1, "ORACLE"),
}

而URL使用的是jdbc url这样通用性比较强且简单,客户端填写如:

jdbc:mysql://192.0.0.1:3306/test?characterEncoding=UTF8

代码就是简单的crud+测试连接

测试连接由于需要两种数据库的驱动,引入maven依赖

<!--oracle数据库驱动-->
<dependency>
    <groupId>com.oracle</groupId>
    <artifactId>ojdbc6</artifactId>
</dependency>
<!--mysql数据库驱动-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

测试连接的代码如下

try {
    Connection conn = DriverManager.getConnection(url, username, password);
} catch(Exception e) {
    // 连接出错
} finally {
    connection.close()       
}

jdk的DriverManager会自动去找适合的驱动并连接(使用spi)

接口

接下来就是数据接口的管理,支持增删查改和发布

public class Api {

    @TableId("ID")
    private Long id;

    @ApiModelProperty(value = "接口名称")
    private String name;

    @ApiModelProperty(value = "路径")
    private String path;

    @ApiModelProperty(value = "数据源key")
    private String sourceKey;

    @ApiModelProperty(value = "操作类型")
    private OpTypeEnum method;

    @ApiModelProperty(value = "sql脚本")
    private String sql;
    
}

其中sourceKey为数据源的key, path即为接口发布的路径, method即“GET/POST/PUT/DELETE”, sql即执行的sq脚本

注册spring接口

比如我们通过客户端新增了一个接口,路径为/user,怎么能让该路径真实可访问,不可能用户没新增一个接口我们就写个@RequestMapping("/user")吧,那样太笨拙了

可以想一下spring是如何注册接口,平时开发springboot,写一个@RequestMapping("/xxx"),springboot启动时会扫描该注解,并获取路径进行注册,此时通过/xxx就可以访问,那么我们只需要找到这个注册器,创建自定义接口时手动注册即可

经查找,spring的web路径注册器就是RequestMappingHandlerMapping,并且也是在spring容器中,它的主要方法

void registerMapping(RequestMappingInfo mapping, 
Object handler, Method method)
// mapping 即路径信息,包含请求的Method等
// handler 即注册该路径发起请求时处理的对象
// method 即执行该对象的具体方法

因此我们向spring注册路径信息时,需要告知spring该请求出现时执行的对象和方法

此时我们写一个动态注册器,把Api注册到RequestMappingHandlerMapping,实现如下

@Component
public class RequestDynamicRegistry {

    /**
     * spring 注册器
     */
    @Autowired
    private RequestMappingHandlerMapping requestMappingHandlerMapping;

    /**
     * 请求到来的处理者
     */
    @Autowired
    private RequestHandler requestHandler;

    /**
     * 请求到来的处理者方法
     */
    private final Method method = RequestHandler.class.getDeclaredMethod("invoke", HttpServletRequest.class, HttpServletResponse.class, Map.class, Map.class, Map.class);

    /**
     * 已缓存的映射信息
     */
    private final Map<String, Api> apiCaches = new ConcurrentHashMap<>();

    public RequestDynamicRegistry() throws NoSuchMethodException {
    }

    /**
     * 转换为spring所需路径信息
     * @param api
     * @return
     */
    private RequestMappingInfo toRequestMappingInfo(Api api) {
        return RequestMappingInfo.paths(api.getPath())
                .methods(RequestMethod.valueOf(api.getOpType().name().toUpperCase()))
                .build();
    }

    /**
     * 把api注册到spring
     * @param api
     * @return
     */
    public boolean register(Api api) {
        // 准备参数 RequestMappingInfo
        RequestMappingInfo requestMappingInfo = toRequestMappingInfo(api);
        if (requestMappingHandlerMapping.getHandlerMethods().containsKey(requestMappingInfo)) {
            throw new BusinessException("接口冲突,无法注册");
        }
        // 注册到spring web
        requestMappingHandlerMapping.registerMapping(requestMappingInfo, requestHandler, method);
        // 添加缓存
        apiCaches.put(api.getKey(), api);
        return true;
    }

    /**
     * 取消api在spring的注册
     * @param api
     * @return
     */
    public boolean unregister(Api api) {
        // 准备参数 RequestMappingInfo
        RequestMappingInfo requestMappingInfo = toRequestMappingInfo(api);
        // 注册到spring web
        requestMappingHandlerMapping.unregisterMapping(requestMappingInfo);
        // 移除缓存
        apiCaches.remove(api.getKey());
        return true;
    }

    /**
     * 获取所有缓存的api信息
     * @return
     */
    public List<Api> apiCaches() {
        return this.apiCaches.values().stream().collect(Collectors.toList());
    }

    /**
     * 根据http请求获取缓存的api信息,以便请求出现时按api设置执行方法
     * @param request
     * @return
     */
    public Api getApiFromReqeust(HttpServletRequest request) {
        String mappingKey = Objects.toString(request.getMethod(), "GET").toUpperCase() + ":" + request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
        return apiCaches.get(mappingKey);
    }
}

以上就实现了一个动态按Api信息注册到spring请求匹配的方法,并把所有的Api请求发起的处理者指向了RequestHandler对象的invoke方法,这也是我们自定义的处理器,定义如下

@Component
@Slf4j
public class RequestHandler {

    /**
    ** 动态api注册器
    **/
    @Autowired
    private RequestDynamicRegistry requestDynamicRegistry;

    /**
     * 自定义接口实际执行入口
     * 参数都是spring自动塞进来的请求信息
     */
    @ResponseBody
    public CommonResult<Object> invoke(HttpServletRequest request, HttpServletResponse response,
                                      @PathVariable(required = false) Map<String, Object> pathVariables,
                                      @RequestHeader(required = false) Map<String, Object> defaultHeaders,
                                      @RequestParam(required = false) Map<String, Object> parameters) throws Throwable {
        // 获取api的定义
        Api api = requestDynamicRegistry.getApiFromReqeust(request);
        if (api == null) {
            log.error("{}找不到对应接口", request.getRequestURI());
            throw new Exception("接口不存在");
        }
        // todo 只简单返回ok测试是否可通
        return CommonResult.success("ok");
    }
}

此时我们定义一个Api对象(GET 请求),并使用动态注册器RequestDynamicRegistry注册后,浏览器访问改路径,即可返回"ok"

执行sql并返回

接口搭建起来了,下面就是具体执行了,上面RequestHandler已经获取到Api信息了,再获取sql执行即可

@Component
@Slf4j
public class RequestHandler {

    @Autowired
    private RequestDynamicRegistry requestDynamicRegistry;

    @Autowired
    private SourceService sourceService;

    /**
     * 自定义接口实际执行入口
     */
    @ResponseBody
    public CommonResult<Object> invoke(HttpServletRequest request, HttpServletResponse response,
                                      @PathVariable(required = false) Map<String, Object> pathVariables,
                                      @RequestHeader(required = false) Map<String, Object> defaultHeaders,
                                      @RequestParam(required = false) Map<String, Object> parameters) throws Throwable {
        // 获取api的定义
        Api api = requestDynamicRegistry.getApiFromReqeust(request);
        if (api == null) {
            log.error("{}找不到对应接口", request.getRequestURI());
            throw new BusinessException("接口不存在");
        }
        // todo 参数校验
        // todo requestBody 处理
        // todo 参数填充sql
        // todo 单条记录处理
        // todo 分页处理
        // todo 数据库连接池

        // 获取连接
        Connection conn = null;
        Statement statement = null;
        ResultSet rs = null;
        try {
            Source dbSource = sourceService.getById(api.getSourceKey());
            conn = JdbcUtils.getConnection(dbSource.getUrl(), dbSource.getUsername(), dbSource.getPassword());
            statement = conn.createStatement();
            // 执行sql
            rs = statement.executeQuery(api.getSql());
            return CommonResult.success(convert(rs));
        } finally {
            if (rs!=null) {
                rs.close();
            }
            if (statement!=null) {
                statement.close();
            }
            if (conn!=null) {
                conn.close();
            }
        }
    }

    public static JSONArray convert( ResultSet rs ) throws SQLException, JSONException {
        // 转换为JsonArray, 省略
    }

}

到此一个配置sql后自动生成接口的低代码平台就搭建完了,只是个超简版,省略了很多功能,如参数处理、分页处理、使用数据库连接池等,这些功能一点点加就可以了

接口文档

自动生成接口实现了,但是如果没有接口文档还是很难用,所以结合Swagger2再实现一下自动接口文档

这里代码比较多,也不太熟悉,就不介绍了,主要参照了magic-api的实现,可以自行参考magic-api-plugin-swagger,主要是通过自定义SwaggerResourcesProvider来把所有Api对象信息注册给swagger中

最后结果如下

swagger2
?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容