01-代码生成器核心逻辑

image-20220305215234413

参考资料 mybatis-plus: https://baomidou.com/

参考资料 freemarker: http://freemarker.foofun.cn/

1. 核心是2条SQL

  • 查询当前数据库中所有表的表信息
1
select * from information_schema.TABLES where TABLE_SCHEMA=(select database())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
TABLE_CATALOG=def,
TABLE_COMMENT=, //表注释
TABLE_NAME=..., //表名
TABLE_SCHEMA=..., //数据库名
ENGINE=InnoDB,
TABLE_TYPE=BASETABLE,
TABLE_ROWS=...,
AVG_ROW_LENGTH=...,
DATA_LENGTH=...,
DATA_FREE=0,
INDEX_LENGTH=0,
ROW_FORMAT=Dynamic,
VERSION=10,
CREATE_OPTIONS=,
CREATE_TIME=2022-02-22T18: 54: 40,
MAX_DATA_LENGTH=0,
TABLE_COLLATION=utf8mb4_general_ci
},
{...}
  • 查询具体表名的所有字段的字段信息(order by的作用确保主键在第一个元素)
1
select * from information_schema.COLUMNS where TABLE_SCHEMA = (select database()) and TABLE_NAME='表名' order by ORDINAL_POSITION
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
TABLE_CATALOG=def,
IS_NULLABLE=NO,
TABLE_NAME=..., //表名
TABLE_SCHEMA=..., //数据库名
EXTRA=,
COLUMN_NAME=..., //字段名
COLUMN_KEY=PRI,
CHARACTER_OCTET_LENGTH=80,
PRIVILEGES=select,insert,update,references,
COLUMN_COMMENT=..., //字段注释
COLLATION_NAME=utf8mb4_general_ci,
COLUMN_TYPE=varchar(20),
GENERATION_EXPRESSION=,
ORDINAL_POSITION=1,
CHARACTER_MAXIMUM_LENGTH=...,
DATA_TYPE=varchar,
CHARACTER_SET_NAME=utf8mb4
},
{...}

2. 读取表和字段Demo

2.1 Demo

image-20220305215732976

pom.xml

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
      <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- mybatis-plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.22</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
</dependency>

application.yml

1
2
3
4
5
6
7
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://localhost:3306/mydata?serverTimezone=Asia/Shanghai
username: root
password: 123456

ReadtableApplication.java

1
2
3
4
5
6
7
8
9
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ReadtableApplication {
public static void main(String[] args) {
SpringApplication.run(ReadtableApplication.class, args);
}
}

TableDao.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;

@Mapper
public interface TableDao {

@Select("select * from information_schema.TABLES where TABLE_SCHEMA=(select database())")
List<Map<String, String>> listTable();

@Select("select * from information_schema.COLUMNS where TABLE_SCHEMA = (select database()) and TABLE_NAME=#{tableName} order by ORDINAL_POSITION")
List<Map<String, String>> listTableColumn(String tableName);
}

Column.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import lombok.Data;

/**
* 字段
*/
@Data
public class Column {
private String fieldID; //null
private String isPk; //COLUMN_KEY=PRI, 主键
private String hbaseFieldName; //null
private String fieldName; //COLUMN_NAME
private String dataType; //DATA_TYPE
private String isNull; //IS_NULLABLE=NO,值n; IS_NULLABLE=YES,值y
private String description; //COLUMN_COMMENT
private String fieldLength; //COLUMN_TYPE=类型(20),值20
private String fieldInput; //null
private String internationalKey; //null
private String internationalValue; //null
}

Table.java

1
2
3
4
5
6
7
8
9
10
11
12
import lombok.Data;
import java.util.List;

/**
* 表
*/
@Data
public class Table {
private String tableEn; //TABLE_NAME
private String tableCn; //TABLE_COMMENT
private List<Column> columns; //表内的字段
}

ReadtableApplicationTests.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
import com.jerry.readtable.model.Column;
import com.jerry.readtable.model.Table;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.util.CollectionUtils;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

/**
* 通过Mybatis-Plus读取数据库表结构和字段
* @author Jerry(姜源)
* @date 2022-03-01 23:24
*/
@SpringBootTest
class ReadtableApplicationTests {

@Autowired
private TableDao tableDao;

@Test
void testTableDao() {
//数据表列表
List<Map<String, String>> tables = tableDao.listTable();
System.out.println("tables = " + tables);

List<Table> tableList = new LinkedList<>();
//数据表结构
if (!CollectionUtils.isEmpty(tables)) {
//遍历表
for (Map<String, String> table : tables) {
Table t = new Table();
t.setTableEn(table.get("TABLE_NAME"));
t.setTableCn(table.get("TABLE_COMMENT"));

List<Map<String, String>> tableColumns = tableDao.listTableColumn(t.getTableEn());
System.out.println("tableColumns = " + tableColumns);

List<Column> columnList = new LinkedList<>();
//遍历表中的字段
for (Map<String, String> tableColumn : tableColumns) {
Column column = new Column();
column.setFieldID(null);
column.setIsPk("PRI".equals(tableColumn.get("COLUMN_KEY")) ? "y" : "n");
column.setHbaseFieldName(null);
column.setFieldName(tableColumn.get("COLUMN_NAME"));
//兼容text或longtext类型也标记entity与varchar的类型一样,都是char类型,entity中会写入为String
String dataType = tableColumn.get("DATA_TYPE");
column.setDataType("text".equals(dataType) || "longtext".equals(dataType) ? "char" : dataType);
column.setIsNull("NO".equals(tableColumn.get("IS_NULLABLE")) ? "n" : "y");
column.setDescription(tableColumn.get("COLUMN_COMMENT"));
column.setFieldLength(extDigit(tableColumn.get("COLUMN_TYPE")));
column.setFieldInput(null);
column.setInternationalKey(null);
column.setInternationalValue(null);
columnList.add(column);
}
t.setColumns(columnList);
tableList.add(t);
}
}
System.out.println("result = " + tableList);
}

/**
* 提取数字
*/
public static String extDigit(String source) {
String regEx = "[^0-9]";
return Pattern.compile(regEx).matcher(source).replaceAll("").trim();
}
}

2.2 运行结果

1
2
3
4
5
6
7
2022-03-05 21:14:37.390  INFO 6900 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited
#score, student
tables = [{TABLE_CATALOG=def, TABLE_COMMENT=, TABLE_NAME=score, TABLE_SCHEMA=mydata, ENGINE=InnoDB, TABLE_TYPE=BASE TABLE, TABLE_ROWS=18, AVG_ROW_LENGTH=910, DATA_LENGTH=16384, DATA_FREE=0, INDEX_LENGTH=0, ROW_FORMAT=Dynamic, VERSION=10, CREATE_OPTIONS=, CREATE_TIME=2022-02-22T18:54:40, MAX_DATA_LENGTH=0, TABLE_COLLATION=utf8mb4_general_ci}, {TABLE_CATALOG=def, TABLE_COMMENT=, TABLE_NAME=student, TABLE_SCHEMA=mydata, ENGINE=InnoDB, TABLE_TYPE=BASE TABLE, TABLE_ROWS=8, AVG_ROW_LENGTH=2048, DATA_LENGTH=16384, DATA_FREE=0, INDEX_LENGTH=0, ROW_FORMAT=Dynamic, VERSION=10, CREATE_OPTIONS=, CREATE_TIME=2022-02-22T18:46:32, MAX_DATA_LENGTH=0, TABLE_COLLATION=utf8mb4_general_ci}]
#score字段
tableColumns = [{TABLE_CATALOG=def, IS_NULLABLE=NO, TABLE_NAME=score, TABLE_SCHEMA=mydata, EXTRA=, COLUMN_NAME=s_id, COLUMN_KEY=PRI, CHARACTER_OCTET_LENGTH=80, PRIVILEGES=select,insert,update,references, COLUMN_COMMENT=成绩id, COLLATION_NAME=utf8mb4_general_ci, COLUMN_TYPE=varchar(20), GENERATION_EXPRESSION=, ORDINAL_POSITION=1, CHARACTER_MAXIMUM_LENGTH=20, DATA_TYPE=varchar, CHARACTER_SET_NAME=utf8mb4}, {TABLE_CATALOG=def, IS_NULLABLE=NO, TABLE_NAME=score, TABLE_SCHEMA=mydata, EXTRA=, COLUMN_NAME=c_id, COLUMN_KEY=PRI, CHARACTER_OCTET_LENGTH=80, PRIVILEGES=select,insert,update,references, COLUMN_COMMENT=课程id, COLLATION_NAME=utf8mb4_general_ci, COLUMN_TYPE=varchar(20), GENERATION_EXPRESSION=, ORDINAL_POSITION=2, CHARACTER_MAXIMUM_LENGTH=20, DATA_TYPE=varchar, CHARACTER_SET_NAME=utf8mb4}, {TABLE_CATALOG=def, IS_NULLABLE=YES, TABLE_NAME=score, TABLE_SCHEMA=mydata, EXTRA=, COLUMN_NAME=s_score, COLUMN_KEY=, NUMERIC_PRECISION=10, PRIVILEGES=select,insert,update,references, COLUMN_COMMENT=成绩分数, NUMERIC_SCALE=0, COLUMN_TYPE=int(3), GENERATION_EXPRESSION=, ORDINAL_POSITION=3, DATA_TYPE=int}]
#student字段
tableColumns = [{TABLE_CATALOG=def, IS_NULLABLE=NO, TABLE_NAME=student, TABLE_SCHEMA=mydata, EXTRA=, COLUMN_NAME=s_id, COLUMN_KEY=PRI, CHARACTER_OCTET_LENGTH=80, PRIVILEGES=select,insert,update,references, COLUMN_COMMENT=学生id, COLLATION_NAME=utf8mb4_general_ci, COLUMN_TYPE=varchar(20), GENERATION_EXPRESSION=, ORDINAL_POSITION=1, CHARACTER_MAXIMUM_LENGTH=20, DATA_TYPE=varchar, CHARACTER_SET_NAME=utf8mb4}, {TABLE_CATALOG=def, IS_NULLABLE=YES, TABLE_NAME=student, TABLE_SCHEMA=mydata, EXTRA=, COLUMN_NAME=s_name, COLUMN_KEY=, CHARACTER_OCTET_LENGTH=80, PRIVILEGES=select,insert,update,references, COLUMN_COMMENT=学生姓名, COLLATION_NAME=utf8mb4_general_ci, COLUMN_TYPE=varchar(20), GENERATION_EXPRESSION=, ORDINAL_POSITION=2, CHARACTER_MAXIMUM_LENGTH=20, DATA_TYPE=varchar, CHARACTER_SET_NAME=utf8mb4}, {TABLE_CATALOG=def, IS_NULLABLE=YES, TABLE_NAME=student, TABLE_SCHEMA=mydata, EXTRA=, COLUMN_NAME=s_birth, COLUMN_KEY=, CHARACTER_OCTET_LENGTH=80, PRIVILEGES=select,insert,update,references, COLUMN_COMMENT=学生生日, COLLATION_NAME=utf8mb4_general_ci, COLUMN_TYPE=varchar(20), GENERATION_EXPRESSION=, ORDINAL_POSITION=3, CHARACTER_MAXIMUM_LENGTH=20, DATA_TYPE=varchar, CHARACTER_SET_NAME=utf8mb4}, {TABLE_CATALOG=def, IS_NULLABLE=YES, TABLE_NAME=student, TABLE_SCHEMA=mydata, EXTRA=, COLUMN_NAME=s_sex, COLUMN_KEY=, CHARACTER_OCTET_LENGTH=40, PRIVILEGES=select,insert,update,references, COLUMN_COMMENT=学生性别, COLLATION_NAME=utf8mb4_general_ci, COLUMN_TYPE=varchar(10), GENERATION_EXPRESSION=, ORDINAL_POSITION=4, CHARACTER_MAXIMUM_LENGTH=10, DATA_TYPE=varchar, CHARACTER_SET_NAME=utf8mb4}]

3. 模板文件生成代码

方案: freemarker

3.0 application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#新增数据库连接配置
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/xxx?serverTimezone=Asia/Shanghai
username: root
password: 123456
#自定义配置
path:
template: F:\\code\\generate\\generate-server-code\\src\\main\\resources\\templates
target: F:\\target
name:
project: test-gen-server
module: cn.test.generate
tablePrefix: tt_
author: Jerry

3.1 java文件

参考示例1:serviceImpl.ftl

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
package ${moduleName}.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import cn.hutool.core.bean.BeanUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import ${moduleName}.api.dto.${entityName}DTO;
import ${moduleName}.api.entity.${entityName};
import ${moduleName}.dao.${entityName}Dao;
import ${moduleName}.service.${entityName}Service;

/**
* <p>Description:${tableCn}Service实现</p>
* <p>Copyright: Copyright (c)${year}</p>
* <p>Author: ${authorName}</p>
* <P>Created Date :${date}</P>
* @version 1.0
*/
@Component
@Transactional
public class ${entityName}ServiceImpl extends ServiceImpl<${entityName}Dao, ${entityName}> implements ${entityName}Service {

@Autowired
private ${entityName}Dao ${entityNameFirstLower}Dao;

/**
* 分页查询
* @create: ${currentTime}
* @author: ${authorName}
* @throws Exception
*/
@Override
public Page<${entityName}> list(IQuery<${entityName}DTO> query) {
QueryWrapper<${entityName}> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("isValid", 1);
${entityName}DTO dto = query.getDto();
IQuery.sortHandle(query, queryWrapper);

Page<${entityName}> page = new Page<>(query.getPageNo(), query.getPageSize());
Page<${entityName}> ${entityNameFirstLower}Page = ${entityNameFirstLower}Dao.selectPage(page, queryWrapper);

return ${entityNameFirstLower}Page;
}

/**
* 根据主键获取
* @create: ${currentTime}
* @author: ${authorName}
* @throws Exception
*/
@Override
public ${entityName} getBy${pkFirstUpper}(Long ${pk}){
return this.getById(${pk});
}

<#if isUser == 1>
/**
* 根据用户id获取
* @create: ${currentTime}
* @author: ${authorName}
* @throws Exception
*/
@Override
public ${entityName} getByUserId(Long userId){
QueryWrapper<${entityName}> wrapper = new QueryWrapper<>();
wrapper.lambda().eq(${entityName}::getIsValid,1)
.eq(BeanUtil.isNotEmpty(userId),${entityName}::getUserId,userId)
.last("limit 1");

${entityName} ${entityNameFirstLower} = ${entityNameFirstLower}Dao.selectOne(wrapper);
return ${entityNameFirstLower};
}
</#if>
/**
* 新增
* @create: ${currentTime}
* @author: ${authorName}
* @throws Exception
*/
@Override
public void insert(${entityName} ${entityNameFirstLower}){
this.save(${entityNameFirstLower});
}

/**
* 修改
* @create: ${currentTime}
* @author: ${authorName}
* @throws Exception
*/
@Override
public void update(${entityName} ${entityNameFirstLower}){
this.updateById(${entityNameFirstLower});
}
}

生成方法:

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
//调用
GeneratorServiceImpl.buildServiceImpl(targetPath, projectName, moduleName, templatePathFile, tableList, authorName);
//类和方法
@SuppressWarnings("all")
public class GeneratorServiceImpl {
/**
* 生成Service
*
* @param targetPath 目标路径
* @param projectName 项目名称
* @param moduleName 模块名称
* @param templatePath 模板路径
* @param tableList 表结构字段列表
* @return void
* @throws
* @author Jerry(姜源)
* @date 2022-03-02 11:26
*/
public static void buildServiceImpl(String targetPath, String projectName, String moduleName, File templatePath, List<Table> tableList, String authorName) {
String entityPath = targetPath + "/" + projectName + "/" + projectName + "-admin" + "/src/main/java/" + StringUtils.replace(moduleName, ".", "/") + "/service/impl/";

File file = new File(entityPath);
file.mkdirs();

for (Table table : tableList) {
try {
Configuration freemarkerCfg = new Configuration();
freemarkerCfg.setDirectoryForTemplateLoading(templatePath);
freemarkerCfg.setEncoding(Locale.getDefault(), "UTF-8");
Template template;

String templateFile = "/serviceImpl.ftl";//模板文件 resources/templates/serviceImpl.ftl

String entityName = "";
String tableEn = table.getTableEn();

String[] tableEns = tableEn.split("_");
for (Integer index = 1; index < tableEns.length; index++) {
entityName = entityName + StringUtils.capitalize(tableEns[index]);
}
String targetFileName = entityPath + entityName + "ServiceImpl.java";//生成文件完整路径及文件名

Map<String, Object> dataMap = new HashMap();

List<Field> fieldList = table.getFieldList();
dataMap.put("fieldList", EntityUtil.getEntity(fieldList));

DateTime dateTime = new DateTime();
dataMap.put("year", dateTime.getYear());
dataMap.put("date", dateTime.toString("yyyy年MM月dd日"));
dataMap.put("currentTime", new DateTime().toString("yyyy年MM月dd日 mm:ss"));

dataMap.put("moduleName", moduleName);
dataMap.put("applicationName", StringUtils.capitalize(StringUtils.substringAfterLast(moduleName, ".")));
dataMap.put("packageName", StringUtils.substringBeforeLast(moduleName, "."));
dataMap.put("basePackage", StringUtils.substringBeforeLast(StringUtils.substringBeforeLast(moduleName, "."), "."));

dataMap.put("entityName", entityName);
dataMap.put("entityNameFirstLower", StringUtils.uncapitalize(entityName));
dataMap.put("tableCn", table.getTableCn());

Field field = fieldList.stream().filter(f -> 1 == f.getIsPk()).findFirst().orElse(null);
dataMap.put("pk", field.getFieldName());
dataMap.put("pkFirstUpper", StringUtils.capitalize(field.getFieldName()));

dataMap.put("authorName", authorName);

template = freemarkerCfg.getTemplate(templateFile);
template.setEncoding("UTF-8");
File targetFile = new File(targetFileName);
Writer out = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(targetFile), "UTF-8"));
template.process(dataMap, out);
out.flush();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

3.2 xml文件

参考示例2:mapper.ftl - 生成方法同 3.1

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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace必须指向Dao接口 -->
<mapper namespace="${moduleName}.dao.${entityName}Dao">
<!-- < 小于号 -->
<sql id="LESS_THAN_SYMBOL">
<![CDATA[
<
]]>
</sql>

<!-- & 按位与 -->
<sql id="COLLATION_OPERATION">
<![CDATA[
&
]]>
</sql>

<resultMap id="BaseResultMap" type="${moduleName}.api.entity.${entityName}">
<!--@Table ${entityName}-->
<#list fieldList as field>
<result column="${field.fieldName}" property="${field.fieldName}"/>
</#list>
</resultMap>

<sql id="Base_Column_List">
<#list fieldList as field>
<#if field_has_next == true>
${field.fieldName},
<#else>
${field.fieldName}
</#if>
</#list>
</sql>
</mapper>

3.3 yml文件

参考示例3:bootstrap.ftl - 生成方法同 3.1

使最终生成是显示为 ${aaa} 的样式,则需要进行特殊处理为:

${r'${aaa}'}

否则会因为通过表达式 ${aaa} 找 aaa 变量找不到而报错。

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
server:
port: 8081

spring:
profiles:
active: dev
application:
name: ${projectName}
cloud:
nacos:
config:
server-addr: http://${r'${spring.profiles.active}'}-xxx-nacos.xxx.com
file-extension: yaml
encode: utf-8
group: DEFAULT_GROUP
# dev 命名空间
namespace: xxx
# test 命名空间
#namespace: xxx
extension-configs:
# 服务发现配置
- data-id: xxx-config-discovery-${r'${spring.profiles.active}'}.${r'${spring.cloud.nacos.config.file-extension}'}
refresh: true
group: common
# 基础配置文件
- data-id: xxx-common-${r'${spring.profiles.active}'}.${r'${spring.cloud.nacos.config.file-extension}'}
refresh: true
group: xxx
# 数据配置文件
- data-id: xxx-db-${r'${spring.profiles.active}'}.${r'${spring.cloud.nacos.config.file-extension}'}
refresh: true
group: xxx

01-代码生成器核心逻辑
https://janycode.github.io/2022/03/05/17_分布式/02_代码生成器/01-代码生成器核心逻辑/
作者
Jerry(姜源)
发布于
2022年3月5日
许可协议