低代码平台的一个核心就是,使用可视化的操作,生成项目系统中的非核心代码,从而大大降低了重复代码的编写工作,使开发者们可以更加专注于核心代码和业务功能的设计和开发工作。
低代码平台 低代码平台有很多,强大的低代码平台基本上可以通过可视化操作来编写简单的业务逻辑,而一些简单的低代码平台主要是用来生成一些通用的 CRUD 操作,我们今天要写的就是一款比较简单的低代码平台,主要生成一些通用的后端 CRUD 操作,当然,也可以生成一些简单的前端数据管理页面等等。
低代码平台的主要思路是,以数据库表结构为基础,获取数据库表的结构信息,从而生成后端对应的领域模型以及 CRUD 操作。
本篇博客中,所有的功能都是基于 SpringBoot 的,所以在最开始,我们需要引入 SpringBoot 相关的依赖,数据库使用 MySQL,数据库操作使用 MyBatisPlus,所以在阅读本篇博客之前,我希望阅读的朋友对于这些相关的知识已经有所了解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <parent > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-parent</artifactId > <version > 2.5.2</version > <relativePath /> </parent > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > </dependency > <dependency > <groupId > com.baomidou</groupId > <artifactId > mybatis-plus-boot-starter</artifactId > <version > 3.4.0</version > </dependency > </dependencies >
其实低代码平台的本质就是一个模板操作,将数据库表的结构信息填入对应的模板中,即可根据模板内容生成对应的模板页面。
说到模板生成,其实 JSP 技术本质上就是一种模板,JavaEE 服务器将数据信息填入 JSP 模板中,从而生成对应的 HTML 静态页面,然后响应给前端。
当然,目前的 Java 技术生态圈中,JSP 是一种比较老旧的技术了,学习和使用价值都不算太高,所以今天的低代码平台并不使用 JSP 技术来作为通用模板,而是使用 FreeMarker 模板引擎作为模板工具。
FreeMarker模板 既然要使用 FreeMarker 模板引擎作为模板工具,自然需要对 FreeMarker 技术有所了解。
我们来看一下 FreeMarker 中文官方参考手册的描述:
FreeMarker 是一款 模板引擎 : 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。
简而言之,FreeMarker 就是一款类似于 JSP 的模板工具,但是比起 JSP 来,FreeMarker 效率更高,使用更加方便简单。
如果需要使用 FreeMarker 技术,SpringBoot 中已经为我们集成了 FreeMarker 模板引擎,我们在已经引入 SpringBoot 的前提下,只需要再引入 FreeMarker 的依赖即可。
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-freemarker</artifactId > </dependency >
可以先简单封装一下 FreeMarker 模板引擎的模板生成功能,使得我们可以在后面更加简便地使用 FreeMarker 模板引擎。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Slf4j @Component public class FreeMarkerUtils { public String getTemplateContent (final Object object, final String templateLoaderPath, final String ftlFileName) { StringWriter out = null ; try { Configuration cfg = new Configuration (Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS); cfg.setClassForTemplateLoading(FreeMarkerUtils.class, templateLoaderPath); cfg.setDefaultEncoding(EncodingEnums.UTF_8.getValue()); Template template = cfg.getTemplate(ftlFileName); out = new StringWriter (); template.process(object, out); return out.toString(); } catch (Exception e) { e.printStackTrace(); log.error("[freemarker工具类]FreeMarker读取模板文件异常" ); } finally { if (out != null ) { try { out.flush(); out.close(); } catch (IOException e) { e.printStackTrace(); log.error("[freemarker工具类]FreeMarker读取模板文件输出流关闭异常" ); } } } return null ; } }
数据库表结构信息 表结构信息是低代码平台的基础信息,其他的信息都是从表结构信息延伸出来的。
在获取表结构信息上,需要解决几个问题。
怎么获取数据库源对应的表信息。
怎么获取数据库表对应的字段信息。
其实数据库的表结构信息,也是存储在数据库表中的。这些信息存储在 MySQL 数据库默认的数据库 information_schema
中,我们可以使用 SQL 语句查询出当前使用的数据库的所有表信息。
1 2 3 select table_name as name,table_comment as commentfrom information_schema.tableswhere table_schema = (select database())
name
comment
数据库表名称
数据库表注释信息
同理,我们也可以用同样的方式获取数据库表字段的具体信息。
1 2 3 4 5 6 select column_name as name, ordinal_position as sort,column_comment as comment, data_type as database_type from information_schema.columnswhere table_schema = (select database())AND table_name = 'auth_role' order by ordinal_position;
name
sort
comment
database_type
表字段名称
表字段排序
表字段注释信息
表字段数据库类型
命名风格转换 在 Java 代码中,我们一般会使用遵循 Java 代码规范的驼峰命名法,而在数据库中,则使用下划线命名法,即各个词语之间使用下划线隔开。驼峰命名法和下划线命名法都是遵循一定规范的命名方式,两者之间的命名风格可以相互转换。
如果数据库的命名和 Java 代码的命名都遵循一定的规范,那么可以使用命名风格转换工具,将数据库的名称信息自动转换为 Java 代码风格的名称。
当然,如果命名确实不具备相同的规范,无法自动转换,也可以考虑使用在可视化编程界面,由低代码平台的使用者自己对命名名称进行手动修正。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Slf4j @Component public class CamelCaseUtils { private static final char UNDERSCORE_SEPARATOR = '_' ; public String toCamelCase (String string) { if (StringUtils.isBlank(string)) { return null ; } string = string.toLowerCase(); StringBuilder sb = new StringBuilder (string.length()); boolean upperCase = false ; for (int i = 0 ; i < string.length(); i++) { char ch = string.charAt(i); if (ch == UNDERSCORE_SEPARATOR) { upperCase = true ; } else if (upperCase) { sb.append(Character.toUpperCase(ch)); upperCase = false ; } else { sb.append(ch); } } return sb.toString(); } }
数据类型转换 从数据库中只能获取数据库表字段的数据类型,而在 Java 代码中,我们使用的是 Java 数据类型,在 MyBatisPlus 中,我们一般还会使用 jdbcType,为了能够把数据库类型转换为 Java 类型和 jdbcType,需要我们在后台对这三者直接的关系进行映射,从而实现数据库类型转换为 Java 类型和 jdbcType 类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 @Component public class DataBaseTypeConst { private Map<String, String> jdbcTypeMap = new HashMap <>(16 ); private Map<String, String> javaTypeMap = new HashMap <>(16 ); @PostConstruct private void init () { initJdbcTypeMap(); initJavaTypeMap(); } private void initJdbcTypeMap () { jdbcTypeMap.put("int" , "INTEGER" ); jdbcTypeMap.put("bigint" , "BIGINT" ); jdbcTypeMap.put("char" , "CHAR" ); jdbcTypeMap.put("varchar" , "VARCHAR" ); jdbcTypeMap.put("datetime" , "TIMESTAMP" ); jdbcTypeMap.put("timestamp" , "TIMESTAMP" ); jdbcTypeMap.put("date" , "DATE" ); jdbcTypeMap.put("time" , "TIME" ); jdbcTypeMap.put("tinyint" , "BOOLEAN" ); jdbcTypeMap.put("decimal" , "DECIMAL" ); jdbcTypeMap.put("numeric" , "NUMERIC" ); jdbcTypeMap.put("float" , "FLOAT" ); jdbcTypeMap.put("double" , "DOUBLE" ); } private void initJavaTypeMap () { javaTypeMap.put("int" , "Integer" ); javaTypeMap.put("bigint" , "Long" ); javaTypeMap.put("char" , "String" ); javaTypeMap.put("varchar" , "String" ); javaTypeMap.put("datetime" , "Date" ); javaTypeMap.put("timestamp" , "Date" ); javaTypeMap.put("date" , "Date" ); javaTypeMap.put("time" , "Date" ); javaTypeMap.put("tinyint" , "Boolean" ); javaTypeMap.put("decimal" , "BigDecimal" ); javaTypeMap.put("numeric" , "BigDecimal" ); javaTypeMap.put("float" , "Double" ); javaTypeMap.put("double" , "Double" ); } public String getJdbcType (String databaseType) { return jdbcTypeMap.get(databaseType); } public String getJavaType (String databaseType) { return javaTypeMap.get(databaseType); } }
代码生成 代码生成需要两部分数据:
第一部分是代码模板,代码模板是固定的代码,在后台以 FreeMarker 的形式存在后台中。
第二部分是代码模板参数,代码模板参数是可变的,在代码生成中,代码模板参数就是数据库表信息和数据库字段信息,以及由数据库延伸出来的 Java 名称信息。
代码模板 代码模板由 FreeMarker 技术开发,例如实体类的代码模板,名称是 entity.ftlh
,模板内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 package ${javaPackageName}.entity;import cn.afterturn.easypoi.excel.annotation.Excel;import com.baomidou.mybatisplus.annotation.SqlCondition;import com.baomidou.mybatisplus.annotation.TableField;import com.baomidou.mybatisplus.annotation.TableName;import com.zero.common.base.entity.BaseEntity;import io.swagger.annotations.ApiModel;import io.swagger.annotations.ApiModelProperty;import lombok.AllArgsConstructor;import lombok.Data;import lombok.EqualsAndHashCode;import lombok.NoArgsConstructor;<#assign HasBigDecimal = true > <#assign HasDate = true > <#list tableColumnList as column> <#if column.javaType == "BigDecimal" && HasBigDecimal> import java.math.BigDecimal; <#assign HasBigDecimal = false > </#if > <#if column.javaType == "Date" && HasDate && column.name != "create_time" && column.name != "update_time" > import java.util.Date; <#assign HasDate = false > </#if > </#list> @ApiModel(value = "${comment}") @Data @EqualsAndHashCode(callSuper = false) @NoArgsConstructor @AllArgsConstructor @TableName("${name}") public class $ {entityName} extends BaseEntity {<#list tableColumnList as column> <#if column.name != "id" && column.name != "create_time" && column.name != "create_user_id" && column.name != "update_time" && column.name != "update_user_id" && column.name != "deleted" > @ApiModelProperty(value = "${column.comment}") @Excel(name = "${column.comment}", width = 15, needMerge = true) @TableField(value = "${column.name}") private ${column.javaType} ${column.javaName}; </#if > </#list> }
代码模板参数 代码模板参数都是由数据库相关的信息延伸出来的,主要分为数据库表信息和数据库表字段信息。
这里的 TableInfo 有一部分信息是和前端 vue 相关的,如果有不需要可以根据自己的需求进行代码改造。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 @Data @EqualsAndHashCode(callSuper = false) @NoArgsConstructor @AllArgsConstructor @TableName("dev_table_info") public class TableInfo extends BaseEntity { @TableField(value = "name") private String name; @TableField(value = "comment") private String comment; @TableField(value = "entity_name") private String entityName; @TableField(value = "request_mapping") private String requestMapping; @TableField(value = "java_package_name") private String javaPackageName; @TableField(value = "java_code_path") private String javaCodePath; @TableField(value = "vue_code_path") private String vueCodePath; @TableField(value = "vue_package") private String vuePackage; @TableField(value = "code_author") private String codeAuthor; @TableField(exist = false) private List<TableColumn> tableColumnList; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 @Data @EqualsAndHashCode(callSuper = false) @NoArgsConstructor @AllArgsConstructor @TableName("dev_table_column") public class TableColumn extends BaseEntity { @TableField(value = "name") private String name; @TableField(value = "java_name") private String javaName; @TableField(value = "comment") private String comment; @TableField(value = "database_type") private String databaseType; @TableField(value = "jdbc_type") private String jdbcType; @TableField(value = "java_type") private String javaType; @TableField(value = "query") private Boolean query; @TableField(value = "sort") private Integer sort; @TableField(value = "table_info_id") private Integer tableInfoId; }
模板类型枚举 为了能够区分不同的模板,以及不同模板之前的一些固定参数,我们可以写一些枚举类型,对这些不同模板的固定参数进行定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 public enum CodeTypeEnum { JAVA; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 @Getter @AllArgsConstructor public enum TemplateEnum { ENTITY(CodeTypeEnum.JAVA, "/templates/java" , "entity.ftlh" , "/src/main/java" , ".entity" , "" , ".java" ), MAPPER(CodeTypeEnum.JAVA, "/templates/java" , "mapper.ftlh" , "/src/main/java" , ".mapper" , "Mapper" , ".java" ), MAPPER_XML(CodeTypeEnum.JAVA, "/templates/resources" , "mapperXml.ftlh" , "/src/main/resources" , ".mapper" , "Mapper" , ".xml" ), SERVICE(CodeTypeEnum.JAVA, "/templates/java" , "service.ftlh" , "/src/main/java" , ".service" , "Service" , ".java" ), SERVICE_IMPL(CodeTypeEnum.JAVA, "/templates/java" , "serviceImpl.ftlh" , "/src/main/java" , ".service.impl" , "ServiceImpl" , ".java" ), CONTROLLER(CodeTypeEnum.JAVA, "/templates/java" , "controller.ftlh" , "/src/main/java" , ".controller" , "Controller" , ".java" ); private final CodeTypeEnum codeTypeEnum; private final String templateLoaderPath; private final String ftlTemplateFile; private final String fileBasePath; private final String packageName; private final String suffix; private final String fileSuffix; }
生成代码 拥有代码模板和代码模板参数之后,就可以使用之前写的 FreeMarkerUtils 直接生成代码,不过为了方便使用,我们可以再一次包装一个 CodeGenerationUtils 工具类。后续我们只需要调用 CodeGenerationUtils 工具类的 generation
方法即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 @Slf4j @RequiredArgsConstructor @Component public class CodeGenerationUtils { private final TableInfoMapper tableInfoMapper; private final TableColumnMapper tableColumnMapper; private final FreeMarkerUtils freeMarkerUtils; public void generation (Integer id) throws Exception { TableInfo tableInfo = tableInfoMapper.selectById(id); List<TableColumn> tableColumnList = tableColumnMapper.getByTableInfoId(tableInfo.getId()); tableInfo.setTableColumnList(tableColumnList); generationFile(tableInfo, TemplateEnum.ENTITY); generationFile(tableInfo, TemplateEnum.MAPPER); generationFile(tableInfo, TemplateEnum.SERVICE); generationFile(tableInfo, TemplateEnum.SERVICE_IMPL); generationFile(tableInfo, TemplateEnum.CONTROLLER); generationFile(tableInfo, TemplateEnum.MAPPER_XML); generationFile(tableInfo, TemplateEnum.VUE); generationFile(tableInfo, TemplateEnum.API); } private void generationFile (TableInfo tableInfo, TemplateEnum templateEnum) throws IOException { StringBuilder generationFilePath = new StringBuilder (); CodeTypeEnum codeTypeEnum = templateEnum.getCodeTypeEnum(); switch (codeTypeEnum) { case JAVA: generationFilePath.append(tableInfo.getJavaCodePath()).append(templateEnum.getFileBasePath()) .append(packageNameToPath(tableInfo.getJavaPackageName() + templateEnum.getPackageName())) .append(File.separator).append(tableInfo.getEntityName()).append(templateEnum.getSuffix()) .append(templateEnum.getFileSuffix()); break ; default : log.error("[代码生成工具]系统当前不支持{}类型的代码生成功能" , codeTypeEnum); } String content = freeMarkerUtils.getTemplateContent(tableInfo, templateEnum.getTemplateLoaderPath(), templateEnum.getFtlTemplateFile()); File generationFile = new File (generationFilePath.toString()); generationFile.getParentFile().mkdirs(); OutputStreamWriter out = new OutputStreamWriter (new FileOutputStream (generationFile), EncodingEnums.UTF_8.getValue()); BufferedWriter writer = new BufferedWriter (out); writer.write(content); writer.close(); } private String packageNameToPath (String packageName) { return File.separator + packageName.replace(StringConst.POINT, File.separator); } }
可视化效果
优点和缺点 优点
使用便捷,开发者可以使用这款低代码工具生成简单重复的代码。
代码简单,开发者可以根据自己的需求,对这款低代码工具进行定制化的修改。
缺点
功能单一,相比较其他成熟的低代码平台,这款低代码工具的功能还是比较单一的,无法承担比较复杂的业务场景。
不支持多表关联,这款低代码工具目前只支持单表代码生成。
有兴趣的小伙伴可以基于这个的基础上,对这款低代码工具进行扩展和升级,增加更多复杂,更加强大的功能。
最后 这款低代码平台能够生成简单的 Java 和前端代码,对于基础的增删改查可以直接生成,开发者只需要在这个基础上进行一些简单的修改,这些代码就可以立即生效,从而大大节省开发者的时间,让开发者从无意义的重复代码中解放出来。
这款低代码工具其实是我的开源项目 zero-admin
下的一个子模块 zero-dev
,这款低代码工具的所有的源码都在该模块中,有兴趣的可以前往我的 GitHub 进行相关的了解。
GitHub:https://github.com/herenpeng/zero-admin.git
同时,我还在 Gitee 上提供了仓库镜像。
Gitee:https://gitee.com/herenpeng/zero-admin.git
如果你喜欢这款低代码工具,希望各位同学可以给我的 GitHub 或者 Gitee 仓库点一个 Star
,非常感谢!