项目中常常用到代码生成器生成代码,下面介绍 velocity 代码生成原理,及如何编写代码生成器。
Velocity 介绍
Velocity 是一个基于 Java 的模板引擎,基于 MVC 模型实现,其提供了一个 Context 容器(相当于 Spring 的 Model),在 java 代码里面我们可以往容器中存值,然后在 vm 文件中使用特定的语法获取(相当于 Spring 页面中取值如 freemarker、thymeleaf)。
官网:http://velocity.apache.org/
maven 引入
<!-- https://mvnrepository.com/artifact/org.apache.velocity/velocity -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>
复制代码
velocity 基本语法
变量
设置变量 #set($foo =“hello”)
取值 $foo
访问对象属性 $user.name
${user.name}
使用vari获取变量时,如果变量不存在,Velocity引擎会将其原样输出,通过使用!{}的形式可以将不存在的变量变成空白输出. 见示例 ${notExist}
$!{notExistEmpty}
velocity 中大小写敏感。
循环
#foreach($i in $list)
$i
#end
复制代码
velocity 只会替换变量,所以 velocity 的语句一般顶行写,以保持文件格式
如上 $i 前的空格将会原样输出
条件
#if(condition)
...dosonmething...
#elseif(condition)
...dosomething...
#else
...dosomething...
#end
复制代码
hello world generator
初始化了VelocityEngine
这个模板引擎,对其设置参数进行初始化,指定使用 ClasspathResourceLoader 来加载 vm 文件。
在VelocityContext
这个 Velocity 容器中存放对象了。
在.vm
文件中我们可以取出这些变量,
Template
模板输出 template.merge(ctx,sw)
public class HelloWorldVelocity {
public static void main(String[] args) {
// 初始化模板引擎
VelocityEngine velocityEngine = new VelocityEngine();
velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
velocityEngine.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
velocityEngine.init();
// 获取模板文件
Template template = velocityEngine.getTemplate("helloVelocity.vm");
// 设置变量
VelocityContext ctx = new VelocityContext();
ctx.put("name", "Velocity");
User user = new User();
user.setName("zhang san");
user.setPhone("18612345678");
ctx.put("user", user);
List list = new ArrayList();
list.add("1");
list.add("2");
ctx.put("list", list);
// 输出
StringWriter sw = new StringWriter();
template.merge(ctx,sw);
System.out.println(sw.toString());
}
复制代码
resouces 目录下的模板文件helloVelocity.vm
#set($foo = 'hello')
$foo $name
${notExist}
$!{notExistEmpty}
$user.name
${user.name}
#foreach($i in $list)
$i
#end
复制代码
Gitee: https://gitee.com/tg_seahorse/paw-demos/tree/master/paw-generator
mybatis-plus-generator
Mybatsi-plus官网文档
官网源码Git
generator 使用
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
复制代码
MyBatis-Plus 支持 Velocity(默认)、Freemarker、Beetl 模板引擎
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
复制代码
参照官网修改的生成代码类
配置信息写在了 main 方法的开头,项目路径、包名、要生成的表、表前缀
配置数据源 mysql 引入依赖mysql-connector-java
cfg.setFileCreate 文件生成策略,return true 会生成文件,覆盖原有文件。
开启 swaggergc.setSwagger2(true);
代码使用时需引入 swagger 依赖
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
复制代码
代码生成类
public class CodeGenerator {
public static void main(String[] args) {
String projectPath = "/Users/rubble/workSpace/paw/paw-demos/paw-generator";
String author = "Rubble";
String packageParent = "com.paw.generator";
String module = "system";
// 多个用,分隔
String tables = "sys_user";
String tablePrefix = "sys_";
// 代码生成器
AutoGenerator mpg = new AutoGenerator();
// 全局配置
GlobalConfig gc = new GlobalConfig();
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor(author);
gc.setOpen(false);
// gc.setSwagger2(true); 实体属性 Swagger2 注解
mpg.setGlobalConfig(gc);
// 数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://localhost:3306/ry?useUnicode=true&useSSL=false&characterEncoding=utf8");
// dsc.setSchemaName("public");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("123456");
mpg.setDataSource(dsc);
// 包配置
PackageConfig pc = new PackageConfig();
pc.setModuleName(module);
pc.setParent(packageParent);
mpg.setPackageInfo(pc);
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap() {
// to do nothing
}
};
// 如果模板引擎是 freemarker
// String templatePath = "/templates/mapper.xml.ftl";
// 如果模板引擎是 velocity
String templatePath = "/templates/mapper.xml.vm";
// 自定义输出配置
List<FileOutConfig> focList = new ArrayList<>();
// 自定义配置会被优先输出
focList.add(new FileOutConfig(templatePath) {
@Override
public String outputFile(TableInfo tableInfo) {
// 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
+ "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
}
});
/* 允许生成模板文件 */
cfg.setFileCreate(new IFileCreate() {
@Override
public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
// 判断自定义文件夹是否需要创建
checkDir(projectPath);
if (fileType == FileType.MAPPER) {
// 已经生成 mapper 文件判断存在,不想重新生成返回 false
return !new File(filePath).exists();
}
// 允许生成模板文件
return true;
}
});
cfg.setFileOutConfigList(focList);
mpg.setCfg(cfg);
// 配置模板
TemplateConfig templateConfig = new TemplateConfig();
// 配置自定义输出模板
//指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
// templateConfig.setEntity("templates/entity2.java");
// templateConfig.setService();
// templateConfig.setController();
templateConfig.setXml(null);
mpg.setTemplate(templateConfig);
// 策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
// strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
// 公共父类
// strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");
// 写于父类中的公共字段
strategy.setSuperEntityColumns("id");
strategy.setInclude(tables.split(","));
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix(tablePrefix);
mpg.setStrategy(strategy);
mpg.setTemplateEngine(new VelocityTemplateEngine());
mpg.execute();
}
}
复制代码
CodeGenerator
可以用于日常基于 mybatis-plus 项目的开发中。
自定义模板
扩展 control 模板
Mybatis-plus 模板默认位置 resources/templates 下,可在配置中进行修改 templatePath
从 git 项目或 jar 包中复制controller.java.vm
增加 CRUD 的方法,只写了简单的 add、list 方法,可自行就行扩展,如增加分页查找。
package ${package.Controller};
import java.util.List;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.paw.generator.system.entity.User;
import com.paw.generator.system.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
#if(${restControllerStyle})
import org.springframework.web.bind.annotation.RestController;
#else
import org.springframework.stereotype.Controller;
#end
#if(${superControllerClassPackage})
import ${superControllerClassPackage};
#end
/**
* <p>
* $!{table.comment} 前端控制器
* </p>
*
* @author ${author}
* @since ${date}
*/
#if(${restControllerStyle})
@RestController
#else
@Controller
#end
@RequestMapping("#if(${package.ModuleName})/${package.ModuleName}#end/#if(${controllerMappingHyphenStyle})${controllerMappingHyphen}#else${table.entityPath}#end")
#if(${kotlin})
class ${table.controllerName}#if(${superControllerClass}) : ${superControllerClass}()#end
#else
#if(${superControllerClass})
public class ${table.controllerName} extends ${superControllerClass} {
#else
public class ${table.controllerName} {
#end
@Autowired
private ${table.serviceName} service;
@GetMapping("add")
public Object add(${entity} entity){
boolean saved = service.save(entity);
return entity;
}
@GetMapping("list")
public List<${entity}> list(${entity} entity){
return service.list(new QueryWrapper<>(entity));
}
}
#end
复制代码
生成代码
/**
* <p>
* 用户信息表 前端控制器
* </p>
*
* @author Rubble
* @since 2021-07-07
*/
@RestController
@RequestMapping("/system/user")
public class UserController {
@Autowired
private IUserService service;
@GetMapping("add")
public Object add(User entity){
boolean saved = service.save(entity);
return entity;
}
@GetMapping("list")
public List<User> list(User entity){
return service.list(new QueryWrapper<>(entity));
}
}
复制代码
自定义模板 hello
配置中增加模板hello.java.vm
,定义文件输出位置
String helloTemplatePath = "/templates/hello.java.vm";
focList.add(new FileOutConfig(helloTemplatePath) {
@Override
public String outputFile (TableInfo tableInfo) {
return projectPath + "/src/main/java/" + packageParent.replace(".", File.separator) + File.separator + pc.getModuleName() + File.separator + "entity"
+ File.separator + "Hello" + tableInfo.getEntityName() + StringPool.DOT_JAVA;
}
});
复制代码
最简单的模板
package ${package.Entity};
public class Hello${entity}{
}
复制代码
执行输出
package com.paw.generator.system.entity;
public class HelloUser{
}
复制代码
mybatis-plus-generator 解析
git 下载项目,用 jdk8, gradle 6.3 编译通过。
自动配置类AutoGenerator
, 除 datasource 外其他均可默认设置。
/**
* 配置信息
*/
protected ConfigBuilder config;
/**
* 注入配置
*/
protected InjectionConfig injection;
/**
* 数据源配置
*/
private DataSourceConfig dataSource;
/**
* 数据库表配置
*/
private StrategyConfig strategy;
/**
* 包 相关配置
*/
private PackageConfig packageInfo;
/**
* 模板 相关配置
*/
private TemplateConfig template;
/**
* 全局 相关配置
*/
private GlobalConfig globalConfig;
复制代码
模板引擎
AbstractTemplateEngine 实现了文件的输出 controller、service、 entity、mapper。
VelocityTemplateEngine 模板引擎
init()初始化VelocityEngine
指定文件位置、编码等;
writer 引擎模板的渲染 template.merge(new VelocityContext(objectMap), writer);
Map<String, Object> objectMap = this.getObjectMap(config, tableInfo);
@Override
public @NotNull VelocityTemplateEngine init(@NotNull ConfigBuilder configBuilder) {
if (null == velocityEngine) {
Properties p = new Properties();
p.setProperty(ConstVal.VM_LOAD_PATH_KEY, ConstVal.VM_LOAD_PATH_VALUE);
p.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, StringPool.EMPTY);
p.setProperty(Velocity.ENCODING_DEFAULT, ConstVal.UTF8);
p.setProperty(Velocity.INPUT_ENCODING, ConstVal.UTF8);
p.setProperty("file.resource.loader.unicode", StringPool.TRUE);
velocityEngine = new VelocityEngine(p);
}
return this;
}
@Override
public void writer(@NotNull Map<String, Object> objectMap, @NotNull String templatePath, @NotNull File outputFile) throws Exception {
Template template = velocityEngine.getTemplate(templatePath, ConstVal.UTF8);
try (FileOutputStream fos = new FileOutputStream(outputFile);
OutputStreamWriter ow = new OutputStreamWriter(fos, ConstVal.UTF8);
BufferedWriter writer = new BufferedWriter(ow)) {
template.merge(new VelocityContext(objectMap), writer);
}
}
复制代码
// objectMap 在 AbstractTemplateEngine 类中 从配置文件中生成上下文变量加入到 context 中
objectMap.put 的即在模板中可用的属性。
主要属性 PackageInfo, TableInfo
@NotNull
public Map<String, Object> getObjectMap(@NotNull ConfigBuilder config, @NotNull TableInfo tableInfo) {
GlobalConfig globalConfig = config.getGlobalConfig();
Map<String, Object> controllerData = config.getStrategyConfig().controller().renderData(tableInfo);
Map<String, Object> objectMap = new HashMap<>(controllerData);
Map<String, Object> mapperData = config.getStrategyConfig().mapper().renderData(tableInfo);
objectMap.putAll(mapperData);
Map<String, Object> serviceData = config.getStrategyConfig().service().renderData(tableInfo);
objectMap.putAll(serviceData);
Map<String, Object> entityData = config.getStrategyConfig().entity().renderData(tableInfo);
objectMap.putAll(entityData);
objectMap.put("config", config);
objectMap.put("package", config.getPackageConfig().getPackageInfo());
objectMap.put("author", globalConfig.getAuthor());
objectMap.put("kotlin", globalConfig.isKotlin());
objectMap.put("swagger", globalConfig.isSwagger());
objectMap.put("date", globalConfig.getCommentDate());
// 存在 schemaName 设置拼接 . 组合表名
String schemaName = config.getDataSourceConfig().getSchemaName();
if (StringUtils.isNotBlank(schemaName)) {
schemaName += ".";
tableInfo.setConvert(true);
} else {
schemaName = "";
}
objectMap.put("schemaName", schemaName);
objectMap.put("table", tableInfo);
objectMap.put("entity", tableInfo.getEntityName());
return objectMap;
}
复制代码
下载源码的方式扩展可以任意的 put 你想要的属性。
引用 jar 的方式可以增加全局自定义配置, 模板中使用 ${cfg.abc}
// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
@Override
public void initMap () {
// to do nothing
Map<String,Object> map = new HashMap<>();
map.put("abc","123");
setMap(map);
}
};
复制代码
Mybatis-plus-generator 在通用性、扩展性做了很多工作,如父类、驼峰、swagger、各种数据库的适配等等,是一个很优秀的工具,向作者致敬。
若依(ruoyi)框架中的 generator
本文是若依单体项目 thymeleaf 版本。module: ruoyi-generator.
生成工具通过后台管理界面的方式让用户进行设置,配置信息保存在数据库中,根据配置生成一套 CRUD 代码,很是方便。
生成代码总体配置类 GenConfig,设置了包名称规则,作者等全局信息。
入口控制类GenController
preview 预览代码, download 下载 zip 包,genCode 生成代码。
数据库查询配置信息 GenTable 加入到 VelocityContext 中,在 vm 模板中即可取值
对定义的模板进行渲染 tpl.merge(context, sw)
public Map<String, String> previewCode(Long tableId)
{
Map<String, String> dataMap = new LinkedHashMap<>();
// 查询表信息
GenTable table = genTableMapper.selectGenTableById(tableId);
// 设置主子表信息
setSubTable(table);
// 设置主键列信息
setPkColumn(table);
VelocityInitializer.initVelocity();
VelocityContext context = VelocityUtils.prepareContext(table);
// 获取模板列表
List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory());
for (String template : templates)
{
// 渲染模板
StringWriter sw = new StringWriter();
Template tpl = Velocity.getTemplate(template, Constants.UTF8);
tpl.merge(context, sw);
dataMap.put(template, sw.toString());
}
return dataMap;
}
复制代码
Put 到 VelocityContext 中的变量
public static VelocityContext prepareContext(GenTable genTable)
{
String moduleName = genTable.getModuleName();
String businessName = genTable.getBusinessName();
String packageName = genTable.getPackageName();
String tplCategory = genTable.getTplCategory();
String functionName = genTable.getFunctionName();
VelocityContext velocityContext = new VelocityContext();
velocityContext.put("tplCategory", genTable.getTplCategory());
velocityContext.put("tableName", genTable.getTableName());
velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】");
velocityContext.put("ClassName", genTable.getClassName());
velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName()));
velocityContext.put("moduleName", genTable.getModuleName());
velocityContext.put("businessName", genTable.getBusinessName());
velocityContext.put("basePackage", getPackagePrefix(packageName));
velocityContext.put("packageName", packageName);
velocityContext.put("author", genTable.getFunctionAuthor());
velocityContext.put("datetime", DateUtils.getDate());
velocityContext.put("pkColumn", genTable.getPkColumn());
velocityContext.put("importList", getImportList(genTable));
velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName));
velocityContext.put("columns", genTable.getColumns());
velocityContext.put("table", genTable);
setMenuVelocityContext(velocityContext, genTable);
if (GenConstants.TPL_TREE.equals(tplCategory))
{
setTreeVelocityContext(velocityContext, genTable);
}
if (GenConstants.TPL_SUB.equals(tplCategory))
{
setSubVelocityContext(velocityContext, genTable);
}
return velocityContext;
}
复制代码
生成代码的模板,
html: crud 的方法 add.html,edit.html,list.html,list-tree.html
Java: controller,service,domain,mapper,serviceImpl
Sql:生成菜单用
自定义的 mapper.xml
若依框架 generator 为前端页面框架 ruoyi-admin 生成了一套完美契合的快速开发的 CRUD 代码,并支持的用户的选择配置,页面的查询功能都已封装,在此学习,向大神致敬。
总结:
velocity 模板引擎分三步,1.初始化 配置模板加载地址;2.放置上下文变量 velocityContext;3.渲染模板。
有框架的项目一般会为,框架定制一个代码生成器,以使代码规范化,同时提高开发效率。
个人项目或中小项目开发应用现成框架即可,若不满足需求,要进行修改,了解模板原理,即可快速扩展。
撸文不易,感谢您的鼓励。
评论