zl程序教程

您现在的位置是:首页 >  Java

当前栏目

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

2023-02-18 16:46:19 时间

前言

平时开发项目时,总会写很多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中

最后结果如下

出处:https://www.jianshu.com/p/8d5f1c584cb1