Foxnic-Web 代码生成 (1) —— 开始生成代码
基本原理
使用 Foxnic-Web 以及 Foxnic-SQL 进行应用开发时,都可以支持代码生成。他们的区别是,基于 Foxnic-SQL 的快速 main 函数启动的应用,只需要生成 Model 和 Service 即可。基于 Foxnic-Web 开发 Web 应用时,除了生成 Model 和 Service 以外,还要生成 Proxy、Controller、UI界面等。
Foxnic 的代码生成是基于数据表的,所以当表结构变更,甚至只是注释的调整,我们也是建议重新生成必要的代码。在 Foxnic 的体系中,我们认为最初的表结构设计、ER图设计,就是这个系统设计的起点。后续的程序设计或数据结构设计都是表结构设计的延续。
Foxnic 的代码生成体系希望开发者可以有一个较高的开发起点,可以基于生成的代码直接开发应用,甚至是代码生成后无需修改就可以直接使用了。另一方面,我们又不关闭二次开发的开放性,毕竟自由的修改代码才是软件系统可以按需定制的终极路径。这也是 Foxnic 体系没有走无代码或低代码平台的原因。
本文中的示例代码均可在 https://gitee.com/LeeFJ/foxnic-samples 项目中找到。
生成表结构元数据
代码生成的第一步是生成表结构元数据。我们知道,数据表、列、索引等信息都存储在数据库内,我们在 java 程序内无法直接引用。但有些时候,我们又需要用到这些东西,特别是表名、字段等。Foxnic-Web 在代码生成、以及其它业务逻辑编写时都有可能用到这些元数据。
所以,我们要通过一种方式将数据库元数据转换成 java 结构,这种结构是通过代码生成的。Java 生成的元数据它是静态的,不会随着变结构的改变而改变,Foxnic-Web 要求表结构变动后,需要重新生成元数据类。
示例项目的 webfull 项目下的 WebFullDBMetaGenerator 类用于生成数据库元数据类:
package org.github.foxnic.web.generator.constants;
import com.github.foxnic.dao.spec.DAO;
import com.github.foxnic.generator.builder.constants.DBMetaClassFile;
import org.github.foxnic.web.generator.config.WebFullConfigs;
public class WebFullDBMetaGenerator {
/**
* 运行main函数生成代码
* */
public static void main(String[] args) throws Exception {
WebFullDBMetaGenerator g = new WebFullDBMetaGenerator();
g.buildDBMeta();
}
/**
* 生成DBMeta数据
* */
private void buildDBMeta() {
WebFullConfigs configs=new WebFullConfigs("webfull-service-example");
DAO dao=configs.getDAO();
DBMetaClassFile dbMetaBuilder=new DBMetaClassFile(dao,configs.getDomainProject(),configs.getProjectConfigs().getDomainConstantsPackage(),"WebFullTables");
dbMetaBuilder.setTableFilter(table->{
table=table.toLowerCase();
// 仅生成以 example_ 开头的表
if(table.startsWith("webfull_")) return true;
return false;
});
dbMetaBuilder.save(true);
}
}
执行 main 函数后,在 domain 模块内生成 WebFullTables 类,后面生成其它代码,我们会用到这个类中数据库表结构定义的常量。
非 Web 环境的代码生成
事实上,非 Web 环境的开发是很少的,但是我们在讲解 Foxnic-SQL 部分的时候,也用到了非 Web 环境的代码生成,大家参考 Foxnic-SQL 相关的文档即可。
在 https://gitee.com/LeeFJ/foxnic-samples 项目的 com/leefj/foxnic/sql/demo/generator 目录的 ExampleCodeGenerator.java 中有示例,大家看代码结合我们的文档,很容易理解。后面的篇幅我们着重介绍 Web 环境下的代码生成,所有 Web 环境下代码生成的原理一样适用于非 Web 环境。
为了方便理解,我们还是贴一下代码:
package com.leefj.foxnic.sql.demo.generator;
import com.github.foxnic.commons.project.maven.MavenProject;
import com.github.foxnic.dao.spec.DAO;
import com.github.foxnic.generator.builder.model.PoClassFile;
import com.github.foxnic.generator.config.ModuleContext;
import com.github.foxnic.sql.meta.DBTable;
import com.leefj.foxnic.sql.demo.app.domain.example.Address;
import com.leefj.foxnic.sql.demo.app.domain.example.Goods;
import com.leefj.foxnic.sql.demo.app.domain.example.Order;
import com.leefj.foxnic.sql.demo.app.domain.example.OrderItem;
import com.leefj.foxnic.sql.demo.config.DBInstance;
import com.leefj.foxnic.sql.demo.config.db.ExampleTables;
/**
* 代码生成器
* */
public class ExampleCodeGenerator {
public static interface Config {
void config(PoClassFile poType);
}
private static final String BASE_PACKAGE = "com.leefj.foxnic.sql.demo.app";
/**
* 需要首先运行 ExampleDBMetaGenerator 生成 ExampleTables 类
* */
public static void main(String[] args) {
ExampleCodeGenerator generator = new ExampleCodeGenerator();
// 生成商品实体类
generator.generate(ExampleTables.EXAMPLE_GOODS.$TABLE, poType -> {
// Goods 对象 通过 orderList 属性持有 Order
poType.addListProperty(Goods.class,"orderList","订单明细商品","订单明细商品");
// Goods 对象 通过 addressList 属性持有 Address
poType.addListProperty(Address.class,"addressList","收件地址","收件地址,包括收件人以及手机号码");
// Goods 对象 通过 itemList 属性持有 OrderItem
poType.addListProperty(OrderItem.class,"itemList","订单明细","订单明细");
});
// 生成订单实体类
generator.generate(ExampleTables.EXAMPLE_ORDER.$TABLE , poType -> {
// Order 对象 通过 goodsList 属性持有 Goods
poType.addListProperty(Goods.class,"goodsList","订单明细商品","订单明细商品");
// Order 对象 通过 address 属性持有 Address
poType.addSimpleProperty(Address.class,"address","收件地址","收件地址,包括收件人以及手机号码");
// Order 对象 通过 itemList 属性持有 OrderItem
poType.addListProperty(OrderItem.class,"itemList","订单明细","订单明细");
});
// 生成订单明细实体类
generator.generate(ExampleTables.EXAMPLE_ORDER_ITEM.$TABLE, poType -> {
// OrderItem 对象 通过 goodsList 属性持有 Goods
poType.addSimpleProperty(Goods.class,"goods","订单明细商品","订单明细商品");
// OrderItem 对象 通过 address 属性持有 Address
poType.addSimpleProperty(Address.class,"address","收件地址","收件地址,包括收件人以及手机号码");
// OrderItem 对象 通过 order 属性持有 Order
poType.addListProperty(Order.class,"order","订单","订单");
});
// 生成地址实体类
generator.generate(ExampleTables.EXAMPLE_ADDRESS.$TABLE, poType -> {
// Address 对象 通过 goodsList属性 持有 Goods
poType.addListProperty(Goods.class,"goodsList","订单明细商品","订单明细商品");
// Address 对象 通过 orderList 持有 Order
poType.addListProperty(Address.class,"orderList","收件地址","收件地址,包括收件人以及手机号码");
// Address 对象 通过 itemList 持有 OrderItem
poType.addListProperty(OrderItem.class,"itemList","订单明细","订单明细");
});
}
/**
* 按表生成
* */
public void generate(DBTable table) {
generate(table,null);
}
/**
* 按表生成
* */
public void generate(DBTable table,Config config) {
DAO dao = DBInstance.DEFAULT.dao();
MavenProject project = GeneratorUtil.getProject();
String pkg = table.name().split("_")[0];
String prefix = pkg + "_";
ModuleContext context = new ModuleContext(GeneratorUtil.initGlobalSettings(),table,prefix,BASE_PACKAGE + "." + pkg);
context.setDomainProject(project);
context.setServiceProject(project);
context.setDAO(dao);
if(config!=null) {
config.config(context.getPoClassFile());
}
context.buildPo();
context.buildVo();
context.buildService();
}
}
Web 环境的代码生成
Web 环境的代码生成本文以 webfull 项目为示例, 它以 WebFullCodeStarter 开始,初始化代码生成器,各模块配置,最后按生成用户指定的模块代码。代码生成的模块配置是按表配置的,一个表对应一套代码。我们先来看一下 WebFullCodeStarter 的代码:
package org.github.foxnic.web.generator.module;
import com.github.foxnic.generator.util.ModuleCodeGenerator;
import org.github.foxnic.web.generator.module.bpm.ExampleReimbursementConfig;
import org.github.foxnic.web.generator.module.mall.ExampleAddressConfig;
import org.github.foxnic.web.generator.module.mall.ExampleGoodsConfig;
import org.github.foxnic.web.generator.module.mall.ExampleOrderConfig;
import org.github.foxnic.web.generator.module.mall.ExampleOrderItemConfig;
/**
* 代码生成启动类
* */
public class WebFullCodeStarter extends ModuleCodeGenerator {
public static void main(String[] args) {
// 新建启动类对象
WebFullCodeStarter g=new WebFullCodeStarter();
// 初始化本次需要生成代码的模块
g.initModules();
// 启动
g.start();
}
/**
* 初始化本次需要生成代码的模块
* */
public void initModules() {
initExampleModules();
initBPMModules();
}
/**
* 初始化 BPM 示例模块
* */
private void initBPMModules() {
this.addConfig(new ExampleReimbursementConfig());
}
/**
* 初始化订单示例模块
* */
private void initExampleModules() {
this.addConfig(new ExampleGoodsConfig());
this.addConfig(new ExampleAddressConfig());
this.addConfig(new ExampleOrderConfig());
this.addConfig(new ExampleOrderItemConfig());
}
}
启动 WebFullCodeStarter 的 main 函数,输出如下:
输入 ALL 生成全部
或输入需模块序号: (1) webfull_example_goods (2) webfull_example_address (3) webfull_example_order (4) webfull_example_order_item (5) webfull_example_reimbursement
|
此时,输入 all 或 a 生成列表中全部模块的代码,如果输入指定模块的序号,则社能成指定模块的代码。
从这个动图中,我们不难发现,代码生成器不必重启可以反复生成。针对一些界面的生成,可以边修改代码生成的配置,边生成边测试,我们把它叫做迭代式的代码生成。
为了能够反复进行代码生成,我们针对模块代码的结构做了一些特殊的设计,尽量把业务逻辑的代码剥离到某个单独的文件,其它文件则可以反复生成,这种代码生成的方式极大的提高了开发效率。
代码生成的结果
某个模块的代码生成后,在项目的各个 Maven 子模块下生成相应的 Java 类、Html 文件、js 文件。下面对各个文件说明如下:
文件类型 | 示例 | 位置 | 说明 |
---|---|---|---|
PO | Order.java | domain | 与数据表对应的实体类。 |
POMeta | OrderMeta.java | domain | 针对 PO 类的元数据描述,包含一个 PO Proxy 内部类。 |
VO | OrderVO.java | domain | 用于 API 接口传参、返回等。 |
VOMeta | OrderVOMeta.java | domain | 针对 VO 类的元数据描述,包含一个 VO Proxy 内部类。 |
自定义模型 | CustomModel | domain | 代码生成时自定义的模型类。 |
Proxy | OrderServiceProxy.java | proxy | 接口代理、Feign代理接口、接口路径定义。 |
服务接口 | IOrderService.java | service | 服务接口 |
服务实现 | OrderServiceImpl.java | service | 服务接口实现 |
流程支持 | OrderBpmEventAdaptor.java | service | 可选;开启流程审批时会生成,处理流程逻辑。 |
API控制器 | OrderController | service | Rest API 接口控制器 |
页面控制器 | OrderPageController | view | 页面控制器 |
列表页 | order_list.html | view | 列表页,表格展示数据。 |
列表页JS | order_list.js | view | 列表页对应的JS文件 |
表单页 | order_form.html | view | 表单页,表单方式展示、编辑数据。 |
表单页JS | order_form.js | view | 表单页JS |
逻辑扩展JS | order_ext.js | view | 剥离表单、表格业务逻辑,使页面代码可反复生成。 |
从上面的表格我们可以看到,一个数据表对用的简单模块,就包含可诸多文件。其中,黑色加粗的几项不建议手动修改,如要修改可以通过代码生成的方式重新生成。
页面如果有业务逻辑,尽量到 xxx_ext.js 文件修改,因为一旦修改也列表页或表单也的代码就无法再次重新生成代码。
项目依赖
Foxnic-Web 代码生成的方式是迭代式的,所以被生成的代码需要依赖到代码生成的项目中。以 webfull 项目的 generator 项目为例,在它的 pom 文件中需要加入 domain、proxy、service 和 view 的依赖。
<dependencies>
<!-- 通用基础模块 -->
<dependency>
<groupId>com.github.foxnic.web</groupId>
<artifactId>framework-boot</artifactId>
<version>${foxnic.web.version}</version>
</dependency>
<dependency>
<groupId>com.github.foxnic.web</groupId>
<artifactId>framework-cloud</artifactId>
<version>${foxnic.web.version}</version>
</dependency>
<dependency>
<groupId>com.github.foxnic</groupId>
<artifactId>foxnic-generator</artifactId>
<version>${foxnic.version}</version>
</dependency>
<!-- 当前项目基础模块 -->
<dependency>
<groupId>com.github.foxnic.example.webfull</groupId>
<artifactId>webfull-proxy</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.github.foxnic.example.webfull</groupId>
<artifactId>webfull-domain</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.github.foxnic.example.webfull</groupId>
<artifactId>webfull-framework</artifactId>
<version>${project.version}</version>
</dependency>
<!-- 当前项目业务模块 -->
<dependency>
<groupId>com.github.foxnic.example.webfull</groupId>
<artifactId>webfull-service-example</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.github.foxnic.example.webfull</groupId>
<artifactId>webfull-view-example</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
小结
本节主要介绍了在 Foxnic-SQL 和 oxnic-Web 代码生成的基本步骤,以及代码生成的文件分布,具体作用等。尤其要注意是代码生成项目的依赖问题,可能导致异常而无法正确生成代码。
后面的章节中,我们将进一步介绍代码生成的细节。
相关项目
https://gitee.com/LeeFJ/foxnic
https://gitee.com/LeeFJ/foxnic-web
https://gitee.com/LeeFJ/foxnic-samples