为了提高开发效率,通常会想办法把一些模式固定的重复性的劳动抽取出来,以后再使用的时候,拿来主义就可以了。这样既可以提高开发效率,又降低了出错的风险。
这一思想在我们的日常工作中可以说随处可见,我们完成一项复杂的工程,并不需要面面俱到什么都自己写,我们完全可以利用第三方的jar包让我们达到事半功倍的效果,比如经常使用的apche的commons-lang3包。再比如java中的继承、我们自己封装的工具类等等。 另外一方面,对于源码文件,如果公司有成熟的框架,我们的开发都是遵循着框架制定的约定来进行开发的,我们在创建某一个业务的控制层、业务层、持久层的时候,实际上有相当一部分的工作是重复的。
那么对于源码文件的编写我们能否偷偷懒呢?答案肯定是可以的,我们可以利用模板引擎技术,将不变的部分写在模板文件中,将可变的部分作为变量传递到模板引擎的上下文中,最终生成我们想要的源码文件。
模板引擎的产品有很多,比如前端模板artTemplate、后端模板Velocity、FreeMarker等 本文以Velocity为例,总结一下它在实战中的应用
搭建过程涉及到的基础知识包括:Maven、Velocity、工厂模式、建造者模式、单元测试
对于基础不熟悉的同学,建议看一下下面的两篇文章
Velocity基础
Velocity语法摘要代码生成功能,在我设计的后台框架中,作为一个独立的模块存在,使用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
<?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>
以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; } }
根据源码文件类型的不同,定义了不同的处理类,通过工厂模式返回对应的处理类
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; } }
以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(); } }
该类主要是获取表的信息及对应的字段信息
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(); } }
代用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 } }
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地址