前言
平時(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é)果如下