五步搭建自己的低代碼平臺(tái)

前言

平時(shí)開發(fā)項(xiàng)目時(shí),總會(huì)寫很多crud代碼,開發(fā)過程基本一個(gè)套路,定義controller、service、dao、mapper、dto,感覺一直在repeat yourself

也接觸過很多快速開發(fā)框架,定義一個(gè)sql就可以生成接口,或者定義一個(gè)框架腳本自動(dòng)生成接口,但感覺這些框架沒有說太成熟廣泛使用的,出了問題也很難解決

本文重點(diǎn)研究一下如何只通過定義sql就自動(dòng)生成接口,但是只是簡單實(shí)現(xiàn),為提供思路,畢竟真的實(shí)現(xiàn)高可用性工作量很大

思路

再實(shí)現(xiàn)之前,首先屢清一下思路,使用springboot+swagger2, 大概分為以下5個(gè)步驟

  • 數(shù)據(jù)源信息的配置及測試連接
    url,用戶名,密碼等信息
  • 自定義接口信息的配置
    路徑,請求方式,參數(shù),使用數(shù)據(jù)源, sql腳本等信息
  • 注冊spring接口
    需按自定義的接口信息動(dòng)態(tài)生成一個(gè)spring訪問路徑
  • 執(zhí)行sql并返回
    接口請求時(shí),執(zhí)行自定義接口設(shè)置的sql腳本,并將結(jié)果返回json
  • 注冊swgger2接口(這一步也可以不要)
    把自定義的接口發(fā)布到swagger2文檔中

實(shí)現(xiàn)

思路研究好,開始實(shí)現(xiàn)

數(shù)據(jù)源

作為一個(gè)低代碼平臺(tái),我們希望數(shù)據(jù)源(即數(shù)據(jù)庫)是可配的,并且不同的接口可以訪問不同的數(shù)據(jù)源

在維護(hù)一個(gè)數(shù)據(jù)源表,主要字段如下

public class Source {

    /**
     * 數(shù)據(jù)源key
     */
    private String key;

    /**
     * 數(shù)據(jù)源名稱
     */
    private String name;

    /**
     * 類型
     */
    private DbTypeEnum type;

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

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

    /**
     * 密碼
     */
    private String password;

}

其中DbType我做的簡單一點(diǎn),只支持mysql和orcale

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

而URL使用的是jdbc url這樣通用性比較強(qiáng)且簡單,客戶端填寫如:

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

代碼就是簡單的crud+測試連接

測試連接由于需要兩種數(shù)據(jù)庫的驅(qū)動(dòng),引入maven依賴

<!--oracle數(shù)據(jù)庫驅(qū)動(dòng)-->
<dependency>
    <groupId>com.oracle</groupId>
    <artifactId>ojdbc6</artifactId>
</dependency>
<!--mysql數(shù)據(jù)庫驅(qū)動(dòng)-->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

測試連接的代碼如下

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

jdk的DriverManager會(huì)自動(dòng)去找適合的驅(qū)動(dòng)并連接(使用spi)

接口

接下來就是數(shù)據(jù)接口的管理,支持增刪查改和發(fā)布

public class Api {

    @TableId("ID")
    private Long id;

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

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

    @ApiModelProperty(value = "數(shù)據(jù)源key")
    private String sourceKey;

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

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

其中sourceKey為數(shù)據(jù)源的key, path即為接口發(fā)布的路徑, method即“GET/POST/PUT/DELETE”, sql即執(zhí)行的sq腳本

注冊spring接口

比如我們通過客戶端新增了一個(gè)接口,路徑為/user,怎么能讓該路徑真實(shí)可訪問,不可能用戶沒新增一個(gè)接口我們就寫個(gè)@RequestMapping("/user")吧,那樣太笨拙了

可以想一下spring是如何注冊接口,平時(shí)開發(fā)springboot,寫一個(gè)@RequestMapping("/xxx"),springboot啟動(dòng)時(shí)會(huì)掃描該注解,并獲取路徑進(jìn)行注冊,此時(shí)通過/xxx就可以訪問,那么我們只需要找到這個(gè)注冊器,創(chuàng)建自定義接口時(shí)手動(dòng)注冊即可

經(jīng)查找,spring的web路徑注冊器就是RequestMappingHandlerMapping,并且也是在spring容器中,它的主要方法

void registerMapping(RequestMappingInfo mapping, 
Object handler, Method method)
// mapping 即路徑信息,包含請求的Method等
// handler 即注冊該路徑發(fā)起請求時(shí)處理的對(duì)象
// method 即執(zhí)行該對(duì)象的具體方法

因此我們向spring注冊路徑信息時(shí),需要告知spring該請求出現(xiàn)時(shí)執(zhí)行的對(duì)象和方法

此時(shí)我們寫一個(gè)動(dòng)態(tài)注冊器,把Api注冊到RequestMappingHandlerMapping,實(shí)現(xiàn)如下

@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 {
    }

    /**
     * 轉(zhuǎn)換為spring所需路徑信息
     * @param api
     * @return
     */
    private RequestMappingInfo toRequestMappingInfo(Api api) {
        return RequestMappingInfo.paths(api.getPath())
                .methods(RequestMethod.valueOf(api.getOpType().name().toUpperCase()))
                .build();
    }

    /**
     * 把a(bǔ)pi注冊到spring
     * @param api
     * @return
     */
    public boolean register(Api api) {
        // 準(zhǔn)備參數(shù) 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) {
        // 準(zhǔn)備參數(shù) 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());
    }

    /**
     * 根據(jù)http請求獲取緩存的api信息,以便請求出現(xiàn)時(shí)按api設(shè)置執(zhí)行方法
     * @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);
    }
}

以上就實(shí)現(xiàn)了一個(gè)動(dòng)態(tài)按Api信息注冊到spring請求匹配的方法,并把所有的Api請求發(fā)起的處理者指向了RequestHandler對(duì)象的invoke方法,這也是我們自定義的處理器,定義如下

@Component
@Slf4j
public class RequestHandler {

    /**
    ** 動(dòng)態(tài)api注冊器
    **/
    @Autowired
    private RequestDynamicRegistry requestDynamicRegistry;

    /**
     * 自定義接口實(shí)際執(zhí)行入口
     * 參數(shù)都是spring自動(dòng)塞進(jìn)來的請求信息
     */
    @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("{}找不到對(duì)應(yīng)接口", request.getRequestURI());
            throw new Exception("接口不存在");
        }
        // todo 只簡單返回ok測試是否可通
        return CommonResult.success("ok");
    }
}

此時(shí)我們定義一個(gè)Api對(duì)象(GET 請求),并使用動(dòng)態(tài)注冊器RequestDynamicRegistry注冊后,瀏覽器訪問改路徑,即可返回"ok"

執(zhí)行sql并返回

接口搭建起來了,下面就是具體執(zhí)行了,上面RequestHandler已經(jīng)獲取到Api信息了,再獲取sql執(zhí)行即可

@Component
@Slf4j
public class RequestHandler {

    @Autowired
    private RequestDynamicRegistry requestDynamicRegistry;

    @Autowired
    private SourceService sourceService;

    /**
     * 自定義接口實(shí)際執(zhí)行入口
     */
    @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("{}找不到對(duì)應(yīng)接口", request.getRequestURI());
            throw new BusinessException("接口不存在");
        }
        // todo 參數(shù)校驗(yàn)
        // todo requestBody 處理
        // todo 參數(shù)填充sql
        // todo 單條記錄處理
        // todo 分頁處理
        // todo 數(shù)據(jù)庫連接池

        // 獲取連接
        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();
            // 執(zhí)行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 {
        // 轉(zhuǎn)換為JsonArray, 省略
    }

}

到此一個(gè)配置sql后自動(dòng)生成接口的低代碼平臺(tái)就搭建完了,只是個(gè)超簡版,省略了很多功能,如參數(shù)處理、分頁處理、使用數(shù)據(jù)庫連接池等,這些功能一點(diǎn)點(diǎn)加就可以了

接口文檔

自動(dòng)生成接口實(shí)現(xiàn)了,但是如果沒有接口文檔還是很難用,所以結(jié)合Swagger2再實(shí)現(xiàn)一下自動(dòng)接口文檔

這里代碼比較多,也不太熟悉,就不介紹了,主要參照了magic-api的實(shí)現(xiàn),可以自行參考magic-api-plugin-swagger,主要是通過自定義SwaggerResourcesProvider來把所有Api對(duì)象信息注冊給swagger中

最后結(jié)果如下

swagger2
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容