一个"脱裤子放屁"的功能!
哩语
“脱了裤子放屁”,此话通常用来讥讽别人说话做事画蛇添足、多此一举。
1.背景
故事是这样的.公司的其他部门经常会要求我们部门以接口形式提供一些数据,这种突然的,毫无规划的,杂乱的需求很多.但是好在通过sql都可以完成查询,但是通过接口的形式就需要开发.
针对上面的情况,每次都需要开发功能(mybatis),然后测试上线,流程很长,对业务支撑也不好.
于是,我想到一个"脱裤子放屁"的功能(因为使用现有模式可以实现功能).我能不能开发一个"系统",在业务部门提出需求的时候,可以快速的通过sql,把数据以接口的形式返回呢?
答案当然是能,说着说着,我把功能实现了.至于是不是脱裤子放屁,已经不重要了,大不了不用.
2.问题
需要解决的问题:
1.通过接口形式提供数据,要控制好入参和出参.
2.入参的统一格式控制
3.出参的统一格式返回
4.内部的业务逻辑实现.
4.1需要实现sql可配置,因为接口的数据是根据录入的接口来返回的.
3.实现
1.一个web页面,完成数据源和sql的 crud;网上有很多现成的后台管理框架,我使用若依.
2.一个接口程序,根据传入的参数,到数据库中查询对应的数据源与sql完成查询,并返回结果集;
1.数据源信息管理
2.sql管理
在sql管理页面,记录2个值,后面我们会用到.
1.数据源id
2.接口名称
3.接口应用程序
4.实现思路
当访问接口被访问的时候,接口程序,会根据传入的数据源id和接口名称,到数据库中(之前在后台管理程序中添加的)查找对应的数据源id与接口名称.并且在指定的数据源中运行sql名称对应的sql脚本,并且把结果返回.
5.核心代码
5.1http请求入口
/**
* 公用查询方法2
* @param db 选择的db,根据输入的值来判断使用那个数据源
* @param sqlName 选择执行的sql
* @param paramStr 查询参数
* @return 返回json数据
*/
@RequestMapping(value={"/commonv2_2/interface/{db}/{sqlName}/{paramStr}",
"/xxxx_2/xxxxx/{db}/{sqlName}/{paramStr}"}, method = {RequestMethod.POST,RequestMethod.GET})
public Object commonInterfaceV2(@PathVariable String db,@PathVariable String sqlName,@PathVariable String paramStr) {
return commonQueryv2(db, sqlName, paramStr);
}
5.2接口程序,根据数据源和sql完成sql的执行
private Map<String, Object> commonQueryv2(String db, String sqlName, String paramStr) {
Map<String, Object> resultMap = new HashMap<>();
List<CommonInterfaceEntity> commonInterfaceEntities = commonService.getSqlByName(db,sqlName);
if (commonInterfaceEntities.isEmpty()){
resultMap.put("code", ResultCode.APIKEY_INVALID);
resultMap.put("message",ResultCode.APIKEY_INVALID.msg());
resultMap.put("data",new ArrayList<Map<String, Object>>());
return resultMap;
}
CommonInterfaceEntity commonInterfaceEntity = commonInterfaceEntities.get(0);
NamedParameterJdbcTemplate namedParameterJdbcTemplate = null;
//判断数据库是否存在
Optional dataSource = Optional.ofNullable(Constants.DATASOURCE_MAP.get(commonInterfaceEntity.getAliasName()));
if (!dataSource.isPresent()){
//创建数据源
namedParameterJdbcTemplate = sqlUtils.getDataSource(commonInterfaceEntity);
}else{
namedParameterJdbcTemplate = (NamedParameterJdbcTemplate)(Constants.DATASOURCE_MAP.get(commonInterfaceEntity.getAliasName()));
}
//封装注释
try {
String document = commonInterfaceEntity.getSqlDocument();
if (null != document) {
resultMap.put("INAME", sqlName); //请求的接口名称
String[] documentList = document.trim().split("\\|");
for (String d : documentList){
String[] documents = d.trim().split("=");
resultMap.put(documents[0], documents[1]);
}
}else{
log.warn("接口={},没有注释,请完善!",sqlName);
}
} catch (Exception e) {
e.printStackTrace();
log.error("解析注解异常"+sqlName,e);
}
List<Map<String, Object>> resultList = new ArrayList<Map<String, Object>>();
//sql注入验证;
if (sqlInj(paramStr.toLowerCase())){
resultMap.put("code", ResultCode.DATA_FORMAT_ISVALID);
resultMap.put("message",ResultCode.DATA_FORMAT_ISVALID.msg());
resultMap.put("data",resultList);
return resultMap;
}
String[] paramsList1 = paramStr.split("&");
Map<String,Object> paramsMap = new HashMap<>();
String sql = commonInterfaceEntity.getSqlContent();
try {
for (String param : paramsList1) {
String[] params2 = param.split("=");
/**
* 对参数转换,
* 如果是 l_ 开头的,转换成long 类型
* 如果是 i_ 开头的,转换成int 类型
* 如果是 s_ 开头的,转换成string 类型
* 其他没有标记的,默认转换成string类型
*/
if (params2[0].startsWith("l_")){
paramsMap.put(params2[0],Long.parseLong(params2[1]));
}else if (params2[0].startsWith("i_")) {
paramsMap.put(params2[0],Integer.parseInt(params2[1]));
}else if (params2[0].startsWith("s_")) {
paramsMap.put(params2[0],params2[1]);
}else{
paramsMap.put(params2[0],params2[1]);
}
//进行字符串替换
sql = sql.replaceAll(String.format("\\$\\{%s}",params2[0]),params2[1]); //字符串拼接
sql = sql.replaceAll(String.format("\\#\\{%s}",params2[0]),":"+params2[0]); //参数拼接
}
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage(),e);
resultMap.put("code", ResultCode.PARAMS_INVALID);
resultMap.put("message","参数无效或参数格式不正确");
resultMap.put("data",resultList);
return resultMap;
}
try {
resultList = namedParameterJdbcTemplate.queryForList(sql,paramsMap);
if(resultList.isEmpty()){
resultMap.put("code", ResultCode.DATA_IS_NULL.val());
resultMap.put("message", ResultCode.DATA_IS_NULL.msg());
}else{
resultMap.put("code", ResultCode.SUCCESS.val());
resultMap.put("message",ResultCode.SUCCESS.msg());
}
resultMap.put("data",resultList);
} catch (Exception e) {
e.printStackTrace();
log.error(e.getMessage(),e);
resultMap.put("code",ResultCode.DATABASE_OPER_ERROR);
resultMap.put("message","服务器内部错误,查询结果异常");
resultMap.put("data",resultList);
}
return resultMap;
}
5.3根据db和接口名,查询数据库中对应的数据源与需要执行的sql
/**
* 通过db和接口名,查询数据库中对应的数据源与需要执行的sql
* @param dbAliasName
* @param sqlName
* @return 接口对象
*/
@Override
public List<CommonInterfaceEntity> getSqlByName(String dbAliasName, String sqlName) {
//通过dbAliasName 初始化数据源
String sql = " select *\n" +
" from ci_data_source\n" +
" inner join ci_sql_properties on ci_data_source.id = ci_sql_properties.data_source_id\n" +
" where ci_data_source.alias_name = :alias_name\n" +
" and ci_sql_properties.sql_name = :sql_name\n" +
" and ci_data_source.is_effective = :is_effective\n" +
" and ci_sql_properties.is_effective = :is_effective;";
NamedParameterJdbcTemplate namedParameterJdbcTemplateMaster = new NamedParameterJdbcTemplate(jdbcTemplate_master.getDataSource());
Map<String,Object> params = new HashMap<>();
params.put("alias_name",dbAliasName);
params.put("sql_name",sqlName);
params.put("is_effective",1);
List<CommonInterfaceEntity> commonInterfaceEntities = new ArrayList<>();
try {
commonInterfaceEntities = namedParameterJdbcTemplateMaster.query(sql.replaceAll("[\r\n]", " "),params,
new BeanPropertyRowMapper<CommonInterfaceEntity>(CommonInterfaceEntity.class));
} catch (DataAccessException e) {
e.printStackTrace();
}
return commonInterfaceEntities;
}
6.使用体验
代码实现了,也运行了几年了.发现确实可以解决一些问题,尤其是一些在会议上的"遭遇战",响应及时,配置灵活.而且因为都是"简单sql"完成的,可以保证接口的性能,同时好扩展.
7.后续
在使用程序的这段时间,发现一个很严重的问题,就是要控制好需求的边界,要知道哪些东西可以做,哪些东西不可以做,不能无休止的无边界.现在这个功能实现起来,发现越来越像mybatis了.如果不及时控制,我恐怕要实现一个新的mybatis了.
我的例子:本来代码第一版实现,只是通过xml配置的方式来完成对sql的管理,但是业务部门抱怨,每次修改代码,都需要找运维到服务器上修改配置,还是不够灵活,然后我就实现了通过数据库来管理sql的版本.
8.后续的问题
1. 对可变参数的查询条件支持不好 – mybatis支持,抗住,不改.
2. 对系统的验证功能不够强大 – 接口现在只支持查询,同时只支持nginx代理的方式访问,可以在nginx上做一些验证,例如jwt.
3. 接口大部分都是原子性的,但是调用方总是希望通过一个接口来获取到所有的数据,这就出现了一个问题.
3.1 针对这种情况,是通过一个接口来满足调用方呢?(一个大而全的接口,多个表的联合查询,判断等等).
3.2 让调用方,通过调用多次接口来完成数据的获取呢?
3.3 我接着"脱裤子放屁",再做一个所谓的"二级接口",通过调用多次原子接口来把数据组合好,再返回给调用方.就是我来完成调用方的需求?
9.代码下载
https://download.csdn.net/download/taotao6086/86568163?spm=1001.2014.3001.5503
具体使用方法,详见附件中readme.md