共计 10705 个字符,预计需要花费 27 分钟才能阅读完成。
为了提高开发效率,通常会想办法把一些模式固定的重复性的劳动抽取出来,以后再使用的时候,拿来主义就可以了。这样既可以提高开发效率,又降低了出错的风险。
这一思想在我们的日常工作中可以说随处可见,我们完成一项复杂的工程,并不需要面面俱到什么都自己写,我们完全可以利用第三方的 jar 包让我们达到事半功倍的效果,比如经常使用的 apche 的 commons-lang3 包。再比如 java 中的继承、我们自己封装的工具类等等。另外一方面,对于源码文件,如果公司有成熟的框架,我们的开发都是遵循着框架制定的约定来进行开发的,我们在创建某一个业务的控制层、业务层、持久层的时候,实际上有相当一部分的工作是重复的。
那么对于源码文件的编写我们能否偷偷懒呢?答案肯定是可以的,我们可以利用模板引擎技术,将不变的部分写在模板文件中,将可变的部分作为变量传递到模板引擎的上下文中,最终生成我们想要的源码文件。
模板引擎的产品有很多,比如前端模板 artTemplate、后端模板 Velocity、FreeMarker 等 本文以 Velocity 为例,总结一下它在实战中的应用
1. 基础知识
搭建过程涉及到的基础知识包括:Maven、Velocity、工厂模式、建造者模式、单元测试
对于基础不熟悉的同学,建议看一下下面的两篇文章
Velocity 基础
Velocity 语法摘要
2. 搭建工程
2.1 模块目录
代码生成功能,在我设计的后台框架中,作为一个独立的模块存在,使用 Maven 构建。
builder 目录:建造者模式应用。由于代表表结构的 Table 实体稍显复杂,因此使用了建造者模式构建 Table 对象。其实不用也可以,因为 Table 不是很复杂,只是为了复习一下所学过的设计模式知识
factory 目录:工厂模式应用。在构建源码文件的时候,由于涉及到了 Controller、Service、Dao、Domain 这几种类型的文件,因此针对不同类型的文件,要使用其对应的处理类,因此使用了工厂模式
handler 目录:生成源文件的核心代码
model 目录:在生成 domain 的时候,由于字段需要从数据库中的表中读取,因此构造了与表对应的实体类方便处理
utils 目录:工具类
Generator.java:程序主文件,调用入口
test 目录:单元测试
. | |
├── generator.iml | |
├── pom.xml | |
└── src | |
├── main | |
│ ├── java | |
│ │ └── com | |
│ │ └── wt | |
│ │ └── master | |
│ │ └── generator | |
│ │ ├── Generator.java | |
│ │ ├── builder | |
│ │ │ ├── MySqlTableBuilder.java | |
│ │ │ └── TableBuilder.java | |
│ │ ├── factory | |
│ │ │ └── GeneratorFactory.java | |
│ │ ├── handler | |
│ │ │ ├── BaseGenerator.java | |
│ │ │ ├── ControllerGeneratorHandler.java | |
│ │ │ ├── DomainGeneratorHandler.java | |
│ │ │ ├── MapperGeneratorHandler.java | |
│ │ │ └── ServiceGeneratorHandler.java | |
│ │ ├── model | |
│ │ │ └── Table.java | |
│ │ └── util | |
│ │ ├── JdbcUtils.java | |
│ │ ├── SpringContextUtils.java | |
│ │ ├── TableColumnUtils.java | |
│ │ └── TableInfoUtils.java | |
│ └── resources | |
│ ├── config | |
│ │ ├── applicationContext.xml | |
│ │ └── db.properties | |
│ └── template | |
│ ├── controller.java.vm | |
│ ├── dao.java.vm | |
│ ├── domain.java.vm | |
│ ├── service.java.vm | |
│ └── serviceimpl.java.vm | |
└── test | |
└── com.wt.master.generator | |
└── GeneratorTest.java |
2.2 引入依赖
<?xml version="1.0" encoding="UTF-8"?> | |
<project xmlns="http://maven.apache.org/POM/4.0.0" | |
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | |
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | |
<parent> | |
<artifactId>j2ee</artifactId> | |
<groupId>com.wt.master</groupId> | |
<version>1.0-SNAPSHOT</version> | |
<relativePath>../version/</relativePath> | |
</parent> | |
<modelVersion>4.0.0</modelVersion> | |
<artifactId>generator</artifactId> | |
<dependencies> | |
<!-- 模板引擎 --> | |
<dependency> | |
<groupId>org.apache.velocity</groupId> | |
<artifactId>velocity</artifactId> | |
<version>1.7</version> | |
</dependency> | |
<!-- https://mvnrepository.com/artifact/junit/junit --> | |
<dependency> | |
<groupId>junit</groupId> | |
<artifactId>junit</artifactId> | |
<version>4.12</version> | |
<scope>test</scope> | |
</dependency> | |
<dependency> | |
<groupId>com.wt.master</groupId> | |
<artifactId>core</artifactId> | |
<version>1.0-SNAPSHOT</version> | |
</dependency> | |
<!-- https://mvnrepository.com/artifact/com.mchange/c3p0 --> | |
<dependency> | |
<groupId>com.mchange</groupId> | |
<artifactId>c3p0</artifactId> | |
<version>0.9.5.4</version> | |
</dependency> | |
</dependencies> | |
</project> |
3. 核心代码
3.1 模板文件的定义
以 controller 层生成模板为例
将不变的部分直接写到.vm 文件中
将模板文件中,有可能发生变化的部分,抽取为变量,变量的值从 VelocityContext 中获取
在 Velocity 架构中,有一个上下文的定义,通过上下文,程序将变量放入上下文对象中。而模板从上下文中获取对应变量的值,获取的方式是 ${变量名},关于 Velocity 模板文件中的语法,参见上文提到的两篇文章
package ${packagePath}.controller; | |
import ${packagePath}.domain.${moduleName}; | |
import ${packagePath}.service.${moduleName}Service; | |
import com.wt.master.core.base.BaseController; | |
import com.wt.master.core.helper.QueryHelper; | |
import com.wt.master.core.request.HttpResultEntity; | |
import io.swagger.annotations.Api; | |
import lombok.extern.slf4j.Slf4j; | |
import org.apache.commons.lang.StringUtils; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.web.bind.annotation.PutMapping; | |
import org.springframework.web.bind.annotation.RequestBody; | |
import org.springframework.web.bind.annotation.RequestMapping; | |
import org.springframework.web.bind.annotation.RestController; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
/** | |
* ${moduleNameCN}控制器 | |
* | |
* @author | |
* @date | |
*/ | |
@Api(value = "${moduleNameCN}控制器", tags = "${moduleName}Controller", description = "${moduleNameCN}控制器" ) | |
@RestController | |
@RequestMapping("/${moduleName}" ) | |
@Slf4j | |
public class ${moduleName}Controller extends BaseController<${moduleName}, ${moduleName}Service> { | |
@Autowired | |
private ${moduleName}Service ${lowerModuleName}Service; | |
@Override | |
protected ${moduleName}Service getService() {return ${lowerModuleName}Service; | |
} | |
} |
3.2 工厂类定义
根据源码文件类型的不同,定义了不同的处理类,通过工厂模式返回对应的处理类
package com.wt.master.generator.factory; | |
import com.wt.master.generator.Generator; | |
import com.wt.master.generator.handler.*; | |
/** | |
* 生成器工厂 | |
* | |
* @author lichking2019@aliyun.com | |
* @date Jun 18, 2019 at 4:02:23 PM | |
*/ | |
public class GeneratorFactory {public static BaseGenerator create(Generator.GenerateItem item) { | |
BaseGenerator baseGenerator = null; | |
switch (item) { | |
case service: | |
baseGenerator = new ServiceGeneratorHandler(); | |
break; | |
case controller: | |
baseGenerator = new ControllerGeneratorHandler(); | |
break; | |
case mapper: | |
baseGenerator = new MapperGeneratorHandler(); | |
break; | |
case domain: | |
baseGenerator = new DomainGeneratorHandler(); | |
break; | |
default: | |
baseGenerator = new ControllerGeneratorHandler();} | |
return baseGenerator; | |
} | |
} |
3.3 源码生成处理类定义
以 controller 处理类为例
定义抽象类,作为基类
定义了抽象方法 generate,生成源码文件的处理方法
定义了抽象方法 getFilePath,获取生成文件的路径
方法的实现由具体的实现类来实现
package com.wt.master.generator.handler; | |
import org.apache.velocity.Template; | |
import org.apache.velocity.VelocityContext; | |
import org.apache.velocity.app.VelocityEngine; | |
import org.apache.velocity.runtime.RuntimeConstants; | |
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader; | |
import java.io.File; | |
import java.io.FileNotFoundException; | |
import java.io.PrintWriter; | |
/** | |
* 生成器抽象 | |
* | |
* @author lichking2019@aliyun.com | |
* @date May 12, 2019 at 10:44:53 AM | |
*/ | |
public abstract class BaseGenerator { | |
/** | |
* 生成代码 | |
* | |
* @param tableName 表名 | |
* @param moduleName 模块英文名 | |
* @param moduleNameCN 模块中文名 | |
* @param packagePath 包路径 | |
* @return | |
*/ | |
public abstract BaseGenerator generate(String tableName, String moduleName, String moduleNameCN, | |
String packagePath); | |
/** | |
* 生成文件路径 | |
* @param packagePath | |
* @return | |
*/ | |
public abstract String getFilePath(String packagePath,String moduleName); | |
/** | |
* 获取 模板 | |
* | |
* @param templateName 模板文件名称 | |
* @return | |
*/ | |
Template getTemplate(String templateName) {VelocityEngine ve = new VelocityEngine(); | |
ve.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath"); | |
ve.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName()); | |
ve.setProperty("input.encoding","utf-8"); | |
ve.setProperty("output.encoding","utf-8"); | |
ve.init(); | |
Template t = ve.getTemplate("/template/" + templateName); | |
return t; | |
} | |
protected void merge(Template template, VelocityContext ctx, String path) {File file = new File(path); | |
if(!file.exists()){new File(file.getParent()).mkdirs();}else{System.out.println("替换文件"+file.getAbsolutePath()); | |
} | |
PrintWriter writer = null; | |
try {writer = new PrintWriter(path); | |
template.merge(ctx, writer); | |
writer.flush();} catch (FileNotFoundException e) {e.printStackTrace(); | |
} finally {writer.close(); | |
} | |
} | |
/** | |
* 获得根目录 | |
* @return | |
*/ | |
protected String getRootPath(){ | |
String rootPath = ""; | |
try {File file = new File(BaseGenerator.class.getResource("/").getFile()); | |
rootPath = file.getParent(); | |
rootPath = java.net.URLDecoder.decode(rootPath.substring(0, rootPath.indexOf("target") - 1), "utf-8"); | |
return rootPath+"/src/main/java"; | |
} catch (Exception e) {e.printStackTrace(); | |
} | |
return rootPath; | |
} | |
/** | |
* 转换包路径为文件路径 | |
* @param packagePath | |
* @return | |
*/ | |
protected String convertPackagePathToFilePath(String packagePath){StringBuilder path = new StringBuilder(); | |
path.append("/"); | |
path.append(packagePath.replace(".", "/")); | |
path.append("/"); | |
return path.toString();} | |
} |
3.4 工具类的定义
该类主要是获取表的信息及对应的字段信息
package com.wt.master.generator.util; | |
import com.wt.master.generator.model.Table; | |
import org.springframework.jdbc.core.BeanPropertyRowMapper; | |
import org.springframework.jdbc.core.JdbcTemplate; | |
import org.springframework.jdbc.core.RowMapper; | |
import org.springframework.util.CollectionUtils; | |
import java.util.HashMap; | |
import java.util.List; | |
import java.util.Map; | |
/** | |
* 表操作类 | |
* | |
* @author lichking2019@aliyun.com | |
* @date Apr 23, 2019 at 11:36:30 PM | |
*/ | |
public class TableInfoUtils { | |
public static final String JDBC_TEMPLATE = "jdbcTemplate"; | |
public static Table getTableColumnList(String tableName) {JdbcTemplate jdbcTemplate = (JdbcTemplate) SpringContextUtils.getBean(JDBC_TEMPLATE); | |
List<Map<String,Object>> tableInfo = jdbcTemplate.queryForList(getTableStructureSql(tableName)); | |
if (CollectionUtils.isEmpty(tableInfo)) {throw new RuntimeException("表:" + tableName + "不存在"); | |
} | |
List<Map<String,Object>> columns = jdbcTemplate.queryForList(getColumnStructureSql(tableName)); | |
return TableColumnUtils.convertToColumn(columns, tableInfo.get(0)); | |
} | |
/** | |
* 获取查询表字段属性的 SQL | |
* | |
* @param tableName 表名 | |
* @return | |
*/ | |
private static String getColumnStructureSql(String tableName) {StringBuilder sql = new StringBuilder(); | |
sql.append("select column_name, data_type,column_comment,column_key"); | |
sql.append("from information_schema.columns"); | |
sql.append("where table_name ='" + tableName + "'"); | |
return sql.toString();} | |
/** | |
* 获取表的信息 | |
* @param tableName | |
* @return | |
*/ | |
private static String getTableStructureSql(String tableName) {StringBuilder sql = new StringBuilder(); | |
sql.append("select table_name,table_comment"); | |
sql.append("from information_schema.tables"); | |
sql.append("where table_name='" + tableName + "'"); | |
return sql.toString();} | |
} |
3.5 应用入口
代用 create 方法来生成源码文件
package com.wt.master.generator; | |
import com.wt.master.generator.builder.MySqlTableBuilder; | |
import com.wt.master.generator.builder.TableBuilder; | |
import com.wt.master.generator.factory.GeneratorFactory; | |
import com.wt.master.generator.handler.BaseGenerator; | |
import com.wt.master.generator.model.Table; | |
import com.wt.master.generator.util.TableInfoUtils; | |
import org.apache.commons.lang3.StringUtils; | |
import org.springframework.context.ApplicationContext; | |
import org.springframework.context.support.ClassPathXmlApplicationContext; | |
import org.springframework.jdbc.core.JdbcTemplate; | |
import org.springframework.util.Assert; | |
import java.util.List; | |
/** | |
* 代码生成工具 | |
* | |
* @author lichking2019@aliyun.com | |
* @date Apr 23, 2019 at 10:41:51 PM | |
*/ | |
public class Generator { | |
/** | |
* 生成代码入口 | |
* | |
* @param tableName 表名 | |
* @param moduleName 模块英文名 | |
* @param moduleNameCN 模块中文名 | |
* @param packagePath 打包路径 | |
* @param item 生成项目 | |
*/ | |
public static void create(String tableName, String moduleName, String moduleNameCN, String packagePath, | |
GenerateItem... item) {if (StringUtils.isBlank(tableName) || StringUtils.isBlank(moduleName) || StringUtils.isBlank(moduleNameCN) || StringUtils.isBlank(packagePath)) {throw new IllegalArgumentException("参数非法!"); | |
} | |
for (GenerateItem generateItem : item) {BaseGenerator baseGenerator = GeneratorFactory.create(generateItem); | |
baseGenerator.generate(tableName, moduleName, moduleNameCN, packagePath); | |
} | |
} | |
public enum GenerateItem {controller, service, mapper, domain} | |
} |
4. 单元测试
package com.wt.master.generator; | |
import org.junit.Test; | |
import org.junit.Before; | |
import org.junit.After; | |
/** | |
* Generator Tester. | |
* | |
* @author <Authors name> | |
* @version 1.0 | |
* @since <pre>Jun 18, 2019</pre> | |
*/ | |
public class GeneratorTest { | |
@Test | |
public void testCreate() throws Exception { | |
//TODO: Test goes here... | |
Generator.create("SecurityRoleT", "SecurityRole", "角色管理", "com.wt.common.security", | |
Generator.GenerateItem.controller, Generator.GenerateItem.service, Generator.GenerateItem.mapper, | |
Generator.GenerateItem.domain); | |
} | |
} |
源码 github 地址