Documentation
¶
Overview ¶
Package generator generate.go 是编排器:把「表元数据 + 配置」组装成 TemplateData,再按约定路径渲染落盘。 与 render.go(模板渲染)和 templates.go(模板嵌入)处于同一包 generator 内,共享 TemplateData 定义。
核心流程:
TableMetadata + Config
↓ BuildTemplateData(类型映射、主键推导、autoFill 判定)
TemplateData
↓ Render(text/template 执行)
Java 代码字符串
↓ Generate(落盘 or dry-run)
*.java 文件 / io.Writer 输出
Go 小白知识点(全文重点):
- io.Writer 接口:只要实现了 Write(p []byte) (n int, err error) 方法,就是 Writer。 os.File、bytes.Buffer、os.Stdout 都是 Writer。Generate 只依赖接口, 测试时传 bytes.Buffer,main 传 os.Stdout,代码一字不改——这是"依赖接口"的典型好处。
- filepath.Join:跨平台路径拼接(Windows 用 \,Unix 用 /),不要手动拼字符串。
- os.MkdirAll:等同于 mkdir -p,目录已存在不报错。
- os.WriteFile:覆盖写文件(Go 1.16+,非原子:中途失败可能留下截断文件),自动创建或覆盖。
- 包级变量 since:由 main 注入,避免库代码调用 time.Now()——库内取系统时间会让测试非确定性。
Package generator 负责把元数据渲染成代码字符串(po/mapper/service/service-impl/query/converter/mapper-xml 多层)。 核心流程:embed.FS 加载模板 → text/template 解析 → Execute 写入 bytes.Buffer → 返回代码字符串(Java 或 XML)。
Package generator 负责把元数据渲染成 Java 代码字符串(po/mapper/service 三层)。 本文件通过 //go:embed 把模板目录编进二进制,实现"单文件可执行"——发布时无需附带 templates/ 目录。
Index ¶
- Variables
- func AllLayers() []string
- func ClassName(layer, modelUpperCamel string, mapping map[string]string) string
- func Generate(cfg config.Config, meta model.TableMetadata, layers []string, dryRun bool, ...) error
- func OutputPath(layer, basePackage, javaRoot, resourcesRoot, modelUpperCamel string, ...) (string, error)
- func Render(layer string, data TemplateData) (string, error)
- func ResolveResourcesRoot(javaRoot, configured string) string
- func SelectLayers(onlyTableModify, withoutApi bool) []string
- func SetSince(s string)
- func ValidateNoClassNameConflict(layers []string, modelUpperCamel string, clsMapping map[string]string) error
- type LayerSpec
- type TemplateData
Constants ¶
This section is empty.
Variables ¶
var Layers = map[string]LayerSpec{ "po": {PkgSuffix: "model.po", NameSuffix: "", Ext: ".java"}, "mapper": {PkgSuffix: "mapper", NameSuffix: "Mapper", Ext: ".java"}, "service": {PkgSuffix: "service", NameSuffix: "Service", Ext: ".java"}, "service-impl": {PkgSuffix: "service.impl", NameSuffix: "ServiceImpl", Ext: ".java"}, "query": {PkgSuffix: "model.query", NameSuffix: "Query", Ext: ".java"}, "converter": {PkgSuffix: "converter", NameSuffix: "Converter", Ext: ".java"}, "mapper-xml": {PkgSuffix: "mapper", NameSuffix: "", Ext: ".xml", Resource: true}, "req-dto": {PkgSuffix: "model.dto.req", NameSuffix: "ReqDto", Ext: ".java"}, "resp-dto": {PkgSuffix: "model.dto.resp", NameSuffix: "RespDto", Ext: ".java"}, "query-req-dto": {PkgSuffix: "model.dto.req", NameSuffix: "QueryReqDto", Ext: ".java"}, "page-query-req-dto": {PkgSuffix: "model.dto.req", NameSuffix: "PageQueryReqDto", Ext: ".java"}, "update-by-query-req-dto": {PkgSuffix: "model.dto.req", NameSuffix: "UpdateByQueryReqDto", Ext: ".java"}, "api": {PkgSuffix: "api", NameSuffix: "Api", Ext: ".java"}, "api-impl": {PkgSuffix: "api.impl", NameSuffix: "ApiImpl", Ext: ".java"}, }
Layers 是层 → 输出约定的全局约定表(M2-A 含非 API 的 7 层;M2-B 再补 api/dto)。
设计原则(开闭原则):新增层只需在此追加一条 LayerSpec 记录, Generate/OutputPath 等编排代码无需任何修改。
Go 小白知识点:var xxx = map[K]V{...} 是包级变量初始化, 整个程序生命周期内共享同一份 map。约定上只读;但它是导出的包级 map, 语言层面 importer 仍可改写——约定不可变即可,无需额外防护。
Functions ¶
func AllLayers ¶
func AllLayers() []string
AllLayers 返回全部 14 层,固定生成顺序(po 优先、api 收尾,便于阅读产物)。
设计原则:顺序固定(而非遍历 Layers map),因为 map 遍历在 Go 中是随机序—— 如果直接 range Layers,每次调用的输出顺序不同,对用户体验(diff、日志)不友好。 用切片显式定义顺序,保证稳定输出。
func ClassName ¶ added in v0.8.2
ClassName 把层名解析为映射后的完整类名——模板(Cls 方法)与落盘文件名(OutputPath) 共用的唯一出口,保证文件名与文件内类声明永远一致。
- 命中映射 → 模式代入:"I{}Service" + "SysUser" → "ISysUserService"
- 未命中 → 约定类名:ModelUpperCamel + Layers[layer].NameSuffix(零配置零变化)
Go 小白知识点:strings.ReplaceAll(s, old, new) 替换全部出现;映射值经 config 校验 恰含一个 {},这里无需再防御。
func Generate ¶
func Generate(cfg config.Config, meta model.TableMetadata, layers []string, dryRun bool, out io.Writer) error
Generate 渲染指定层并落盘(dryRun=false)或写入 out(dryRun=true)。
参数说明:
- cfg:生成器全局配置(包名、输出根目录、数据源等)
- meta:单张表的元数据(列名、类型、注释、主键标记)
- layers:要生成的层列表,如 []string{"po","mapper","service"}
- dryRun:true=只打印到 out,不创建目录/文件;false=落盘
- out:dry-run 时的输出目标(io.Writer 接口);落盘模式传 nil 即可
io.Writer 接口抽象的价值(Go 小白重点):
Generate 内部只调用 fmt.Fprintf(out, ...),不关心 out 是 bytes.Buffer 还是 os.Stdout。
测试:传 &bytes.Buffer{},事后检查 buf.Len() 和 buf.String()——完全内存操作,无副作用。
main:传 os.Stdout,内容直接打印到终端。
两个场景共用同一套代码,零重复。
func OutputPath ¶
func OutputPath(layer, basePackage, javaRoot, resourcesRoot, modelUpperCamel string, pkgMapping, clsMapping map[string]string) (string, error)
OutputPath 由约定 + 包映射 + 类名映射推导落盘路径。
- java 层(Resource=false):javaRoot + 映射后包路径 + 映射后类名文件;包映射键 = 约定包后缀
- resource 层(Resource=true):resourcesRoot + 映射后子目录 + 文件名;包映射键 = 层名(mapper-xml); 文件名跟随 java mapper 的类映射(MyBatis 约定:XML 与 Mapper 接口同名)
pkgMapping/clsMapping 分别与 TemplateData 的 Pkg/Cls 消费同一张表(config.PackageMapping/ClassMapping), 保证落盘位置、文件名与文件内 package 声明、类声明四者永远一致。传 nil = 全约定默认。
返回 (path, error):未知层(不在 Layers 表)返回 error,避免静默写错位置。
Go 小白知识点:多返回值 (string, error) 是 Go 惯用的错误传递方式; 调用方必须检查 error(编译器不强制,但 go vet/lint 会警告忽略 error)。
func Render ¶
func Render(layer string, data TemplateData) (string, error)
Render 渲染指定层的代码模板,返回生成的代码字符串(Java 或 XML)。 layer 取值:"po" / "mapper" / "service" / "service-impl" / "query" / "converter" / "mapper-xml", 对应 templates/<layer>.tmpl。 Go 小白知识点:Go 惯例用 (value, error) 双返回值替代异常;调用方必须检查 error。
func ResolveResourcesRoot ¶
ResolveResourcesRoot 由 java 源根派生 resources 根:把末段 src/main/java 换成 src/main/resources。 若 configured 非空则直接用它(配置优先于约定,便于非标准项目结构覆盖)。
Go 小白知识点:filepath.Join 跨平台拼路径;strings.HasSuffix 检查字符串是否以某子串结尾。 拼路径永远用 filepath.Join,不要手写 "/" 或 "\\"——Windows/Unix 共用同一套代码。
func SelectLayers ¶
SelectLayers 按两个开关对全集做交集过滤:
- 默认(两者 false):返回全 14 层。
- onlyTableModify=true:仅保留"改表影响层"(po/req-dto/resp-dto/mapper-xml/query/query-req-dto)。
- withoutApi=true:仅保留"非 API 层"(service/service-impl/po/query/mapper/mapper-xml)。
- 两者同时 true:取交集(po/query/mapper-xml 这 3 层)。
Go 小白知识点:用 map 做集合(值恒 true)做 O(1) 成员判断,按 AllLayers() 全集顺序过滤 以保持稳定输出顺序(map 遍历是随机序,切片遍历是稳定序)。
func SetSince ¶
func SetSince(s string)
SetSince 供 main 包调用,设置生成文件的日期戳(yyyy-MM-dd)。 测试时无需调用——since="" 时模板里 @since 注释为空白,不影响功能正确性。
func ValidateNoClassNameConflict ¶ added in v0.8.2
func ValidateNoClassNameConflict(layers []string, modelUpperCamel string, clsMapping map[string]string) error
ValidateNoClassNameConflict 校验本次选中的各层类名两两互异。
为什么需要它:改动前类名由 Layers 表的固定 NameSuffix 派生,13 层天然互异, 这条不变量是白送的;class-mapping 把类名变成用户输入后,白送的保证没了, 而 config 的 validateClassMapping 只做逐值孤立校验(恰一个 {}、前后段字符合法), 看不到「代入后各层是否互相撞名」。两种故障同源于重名:
- 同名不同包 → 产出两条同名 single-type import,javac 直接失败
- 同名同包 → 落盘路径相同,按层顺序后者静默覆盖前者,工程凭空少一个类
只查类名即可覆盖两者:路径 = 映射后包 + 映射后类名,故路径相同 ⟹ 类名相同。
只遍历非 Resource 层:mapper-xml 没有自己的类,其文件名刻意跟随 mapper 的类名 (MyBatis 约定 XML 与 Mapper 接口同名),算设计而非冲突。 注意这里隐含依赖「Layers 中只有一个 Resource 层」——当前成立(仅 mapper-xml)。 将来若新增第二个 resource 层,两者可能撞同一个落盘路径而本函数看不见,需一并扩展。
另需说明保证的边界:只覆盖「单次调用内选中的层」。跨次调用不在保证范围内—— 例如先 --with-api 生成了 req-dto,再改映射后跑 --sync-schema(层集更窄), 上一次的产物会作为孤儿文件留在工程里,本函数无从察觉。
Go 小白知识点:map 的迭代顺序是随机的(Go 故意打乱,防止代码依赖顺序), 所以这里用切片 seen 按 layers 的给定顺序收集,保证同样的输入永远报同一条错误—— 错误信息可复现,测试才不会时绿时红。
Types ¶
type LayerSpec ¶
type LayerSpec struct {
PkgSuffix string // 包后缀,如 model.po;resource 层用作子目录(mapper → resources/mapper/)
// NameSuffix 约定类名在 ModelUpperCamel 后追加的后缀,如 ServiceImpl;po 层为空
// (类名即 ModelUpperCamel)。唯一读取点是 ClassName 的「未命中 class-mapping」分支。
//
// Resource 层此字段无意义,一律留空:resource 层没有自己的类,ClassName 永远不会
// 以 resource 层名被调用(模板里没有 Cls "mapper-xml",ClassMappingKeys 也刻意排除它),
// 其文件名由 OutputPath 直接走 mapper 的类映射。若在此填值,改它不会有任何效果,
// 而测试全绿、产物纹丝不动——白白浪费下一个维护者一轮调试,所以宁可空着并写明缘由。
NameSuffix string
Ext string // 文件扩展名:.java 或 .xml
Resource bool // true 表示落 resources 根(如 mapper-xml),false 落 java 根
}
LayerSpec 描述一层的输出约定:包后缀、文件名在 ModelUpperCamel 后追加的后缀、扩展名、是否落 resources 根。
Go 小白知识点:结构体(struct)是 Go 中聚合多字段的基本方式,类似 Java 的 POJO。 这里用值类型(非指针),因为 LayerSpec 是不可变的配置描述,复制开销极低。
type TemplateData ¶
type TemplateData struct {
// Author 代码作者,填入 @author 注释。
Author string
// Since 生成日期,填入 @since 注释。
Since string
// TableName 数据库表名(原始下划线命名),填入 @TableName。
TableName string
// BasePackage Java 基础包名,如 com.dahaoshen.demo。
BasePackage string
// ServiceName Feign 服务名(注册中心应用名),填入 @FeignClient(name=...)。来自 config api.service-name。
ServiceName string
// BasePath 所有 API 端点的基础路径前缀,如 /admin-api/hello。来自 config api.base-path。
BasePath string
// ModelUpperCamel 大驼峰实体类名,如 SysUser。
ModelUpperCamel string
// ModelCamel 小驼峰变量名,如 sysUser。
ModelCamel string
// ModelKebab 中划线名,如 sys-user,用于 URL 路径等场景。
ModelKebab string
// ModelComment 中文表注释,填入 Javadoc 与 @Schema。
ModelComment string
// PkFieldUpperCamel 主键字段的大驼峰名(如 Id),用于拼 getter 方法名。
PkFieldUpperCamel string
// IdType 主键 Java 类型(如 Long / String),模板里直接展开。
IdType string
// UseJakarta 为 true 时引入 jakarta 包并生成 @Serial 注解(Spring Boot 3+ 用 jakarta 而非 javax)。
UseJakarta bool
// IsWithAutoFill 为 true 时 import 整个 annotation 包(含 FieldFill),用于自动填充场景。
IsWithAutoFill bool
// Fields 表的全部字段元数据,模板中用 range .Fields 遍历。
// Go 小白知识点:range 内 "." 指向当前元素(FieldMetadata),父级字段要用 "$." 访问(如 $.UseJakarta)。
Fields []model.FieldMetadata
// contains filtered or unexported fields
}
TemplateData 是模板渲染上下文,所有字段首字母大写(导出),才能被 text/template 的 {{.Field}} 访问。 Go 小白知识点:text/template 通过反射读取结构体字段,未导出(小写)字段对模板不可见。
func BuildTemplateData ¶
func BuildTemplateData(meta model.TableMetadata, cfg config.Config) (TemplateData, error)
BuildTemplateData 把表元数据 + 配置组装成模板上下文(TemplateData)。
步骤:
- 解析方言,获取 TypeMapper(类型映射器)
- 遍历 Columns,映射 Java/JDBC 类型、转换字段名、判定 autoFill
- 查找单列主键(无主键 / 复合主键 → error,快速失败)
- 判定 IsWithAutoFill(任意字段有 autoFill 注解)
- 填充 TemplateData 所有字段
错误情形:
- 未知方言
- 方言无对应 TypeMapper
- 无主键 / 复合主键(框架 @TableId 仅支持单列主键)
func (TemplateData) Cls ¶ added in v0.8.2
func (d TemplateData) Cls(layer string) string
Cls 把层名解析为映射后的完整类名——模板(类声明/跨层引用/XML namespace)与 落盘文件名(OutputPath)共用同一 ClassName helper,两者天然一致。 变量名不走此出口({{.ModelCamel}} 保持模型派生,见设计决策)。
func (TemplateData) Pkg ¶ added in v0.8.2
func (d TemplateData) Pkg(suffix string) string
Pkg 把约定包后缀解析为映射后的完整包名——模板(package/import/XML namespace) 与落盘路径(OutputPath)共用的唯一出口,两者天然一致。
- 命中映射 → base-package + 映射值
- 未命中 → base-package + 约定后缀(零配置零变化)
- 空值 → 直接返回 base-package(类落 base-package 下)
Go 小白知识点:text/template 支持带参数的方法调用 {{.Pkg "model.po"}}—— 方法必须导出(大写开头)、接收者为值或指针均可,参数由模板按字面量传入。