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

作者: pq217 | 来源:发表于2022-08-07 08:05 被阅读0次

    前言

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

    相关文章

      网友评论

        本文标题:五步搭建自己的低代码平台

        本文链接:https://www.haomeiwen.com/subject/xzlpwrtx.html