每一秒钟的时间都值得铭记

0%

低代码开发,手写一款代码生成工具

低代码平台的一个核心就是,使用可视化的操作,生成项目系统中的非核心代码,从而大大降低了重复代码的编写工作,使开发者们可以更加专注于核心代码和业务功能的设计和开发工作。

低代码平台

低代码平台有很多,强大的低代码平台基本上可以通过可视化操作来编写简单的业务逻辑,而一些简单的低代码平台主要是用来生成一些通用的 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>
<!--SpringMVC的启动器-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--lombok的依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--MySQL数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--MyBatis-Plus的依赖-->
<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 {

/**
* 模板渲染并返回内容
*
* @param object 模板参数对象
* @param templateLoaderPath FreeMarker模板文件加载路径
* @param ftlFileName FreeMarker模板文件名称
* @return FreeMarker模板内容内容字符串
*/
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 语句查询出当前使用的数据库的所有表信息。

img

1
2
3
select table_name as name,table_comment as comment
from information_schema.tables
where table_schema = (select database())
  • 查询结果
name comment
数据库表名称 数据库表注释信息

同理,我们也可以用同样的方式获取数据库表字段的具体信息。

img

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.columns
where 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
/**
* 命名风格转换工具类
*
* @author herenpeng
* @since 2020-11-15 15:58
*/
@Slf4j
@Component
public class CamelCaseUtils {

/**
* 下划线分隔符 Underscore
*/
private static final char UNDERSCORE_SEPARATOR = '_';

/**
* 下划线命名,转换为小驼峰命名
*
* @param string 下划线命名名称
* @return 小驼峰命名名称
*/
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
/**
* 数据类型常量,能够通过数据库类型映射为JDBC类型和JAVA类型
*
* @author herenpeng
* @since 2020-11-12 23:56
*/
@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();
}

/**
* 初始化数据库类型和Jdbc类型的映射关系
*/
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");
}

/**
* 初始化数据库类型和Java类型的映射关系
*/
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");
}

/**
* 获取数据库类型对应的JDBC类型
*
* @param databaseType 数据库类型
* @return JDBC类型
* @throws Exception
*/
public String getJdbcType(String databaseType) {
return jdbcTypeMap.get(databaseType);
}

/**
* 获取数据库类型对应的Java类型
*
* @param databaseType 数据库类型
* @return Java类型
* @throws Exception
*/
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>

/**
* ${comment}
*
* @author ${codeAuthor}
* @since ${.now?string("yyyy-MM-dd HH:mm")}
*/
@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">
/**
* ${column.comment}
*/
@ApiModelProperty(value = "${column.comment}")
@Excel(name = "${column.comment}", width = 15, needMerge = true)
@TableField(value = "${column.name}")
private ${column.javaType} ${column.javaName};
</#if>
</#list>

}

代码模板参数

代码模板参数都是由数据库相关的信息延伸出来的,主要分为数据库表信息和数据库表字段信息。

  • TableInfo

这里的 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
/**
* 系统数据库表信息实体类
*
* @author herenpeng
* @since 2020-11-08 10:55
*/
@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;

/**
* 表名对应的实体类Controller请求路径
*/
@TableField(value = "request_mapping")
private String requestMapping;

/**
* Java包前缀名称
*/
@TableField(value = "java_package_name")
private String javaPackageName;

/**
* Java代码生成路径
*/
@TableField(value = "java_code_path")
private String javaCodePath;

/**
* Vue代码生成路径
*/
@TableField(value = "vue_code_path")
private String vueCodePath;

/**
* Vue包路径
*/
@TableField(value = "vue_package")
private String vuePackage;

/**
* 代码作者
*/
@TableField(value = "code_author")
private String codeAuthor;

/**
* 数据库表的字段信息
*/
@TableField(exist = false)
private List<TableColumn> tableColumnList;

}
  • TableColumn
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
/**
* 系统数据库表字段信息实体类
*
* @author herenpeng
* @since 2020-11-11 23:11
*/
@Data
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
@TableName("dev_table_column")
public class TableColumn extends BaseEntity {

/**
* 数据库表字段名称
*/
@TableField(value = "name")
private String name;

/**
* Java属性名称
*/
@TableField(value = "java_name")
private String javaName;

/**
* 数据库表字段注释
*/
@TableField(value = "comment")
private String comment;

/**
* 数据库表字段类型
*/
@TableField(value = "database_type")
private String databaseType;

/**
* 数据库表字段对应的JDBC类型
*/
@TableField(value = "jdbc_type")
private String jdbcType;

/**
* 数据库表字段对应的JAVA类型
*/
@TableField(value = "java_type")
private String javaType;

/**
* 是否为查询字段
*/
@TableField(value = "query")
private Boolean query;

/**
* 数据库表字段排序顺序,数据库默认升序排序
*/
@TableField(value = "sort")
private Integer sort;

/**
* 表信息主键,关联dev_table_info表的主键
*/
@TableField(value = "table_info_id")
private Integer tableInfoId;

}

模板类型枚举

为了能够区分不同的模板,以及不同模板之前的一些固定参数,我们可以写一些枚举类型,对这些不同模板的固定参数进行定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 代码类型,后续可以添加一些其他类型的代码,比如 VUE
*
* @author herenpeng
* @since 2021-03-29 20:43
*/
public enum CodeTypeEnum {
/**
* 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
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
/**
* FreeMarker模板文件相关枚举
*
* @author herenpeng
* @since 2020-11-15 13:07
*/
@Getter
@AllArgsConstructor
public enum TemplateEnum {

/**
* 实体类相关信息枚举
*/
ENTITY(CodeTypeEnum.JAVA,
"/templates/java",
"entity.ftlh",
"/src/main/java",
".entity",
"",
".java"),

/**
* Mapper层接口相关信息枚举
*/
MAPPER(CodeTypeEnum.JAVA,
"/templates/java",
"mapper.ftlh",
"/src/main/java",
".mapper",
"Mapper",
".java"),

/**
* Mapper层XML文件相关信息枚举
*/
MAPPER_XML(CodeTypeEnum.JAVA,
"/templates/resources",
"mapperXml.ftlh",
"/src/main/resources",
".mapper",
"Mapper",
".xml"),

/**
* Service层接口相关信息枚举
*/
SERVICE(CodeTypeEnum.JAVA,
"/templates/java",
"service.ftlh",
"/src/main/java",
".service",
"Service",
".java"),

/**
* Service层接口实现类相关信息枚举
*/
SERVICE_IMPL(CodeTypeEnum.JAVA,
"/templates/java",
"serviceImpl.ftlh",
"/src/main/java",
".service.impl",
"ServiceImpl",
".java"),

/**
* Controller层相关信息枚举
*/
CONTROLLER(CodeTypeEnum.JAVA,
"/templates/java",
"controller.ftlh",
"/src/main/java",
".controller",
"Controller",
".java");

/**
* 代码类型
*/
private final CodeTypeEnum codeTypeEnum;
/**
* 模板文件加载路径
*/
private final String templateLoaderPath;

/**
* FreeMarker模板文件名称
*/
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
/**
* 代码生成工具类
*
* @author herenpeng
* @since 2020-11-11 22:53
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class CodeGenerationUtils {

private final TableInfoMapper tableInfoMapper;

private final TableColumnMapper tableColumnMapper;

private final FreeMarkerUtils freeMarkerUtils;

/**
* 代码生成方法
*
* @param id 表信息主键
* @throws Exception
*/
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);
}


/**
* 通过模板文件生成对应的文件
*
* @param tableInfo 表信息,需要在模板文件中渲染的内容信息
* @param templateEnum 需要生成的文件类型枚举,其中含有对应的生成信息
* @throws IOException IO异常
*/
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();
}

/**
* 将包名转换为文件路径名,并在包文件路径的前后拼接上文件路径分隔符号
*
* @param packageName 包名转
* @return 文件路径名称
*/
private String packageNameToPath(String packageName) {
return File.separator + packageName.replace(StringConst.POINT, File.separator);
}
}

可视化效果

  • 代码生成

img

  • 表信息配置

img

  • 表字段配置

img

优点和缺点

优点

  • 使用便捷,开发者可以使用这款低代码工具生成简单重复的代码。
  • 代码简单,开发者可以根据自己的需求,对这款低代码工具进行定制化的修改。

缺点

  • 功能单一,相比较其他成熟的低代码平台,这款低代码工具的功能还是比较单一的,无法承担比较复杂的业务场景。
  • 不支持多表关联,这款低代码工具目前只支持单表代码生成。

有兴趣的小伙伴可以基于这个的基础上,对这款低代码工具进行扩展和升级,增加更多复杂,更加强大的功能。

最后

这款低代码平台能够生成简单的 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,非常感谢!

img

坚持原创技术分享,您的支持将鼓励我继续创作!
-------------这是我的底线^_^-------------