代码生成器实现
实现封装元数据的工具类实现代码生成器的代码编写掌握模板创建的
构造数据模型
需求分析
借助Freemarker机制可以方便的根据模板生成文件,同时也是组成代码生成器的核心部分。对于Freemarker而
言,其强调 数据模型 + 模板 = 文件 的思想,所以代码生成器最重要的一个部分之一就是数据模型。在这里数据
模型共有两种形式组成:
- 数据库中表、字段等信息
针对这部分内容,可以使用元数据读取并封装到java实体类中
- 用户自定义的数据
为了代码生成器匹配多样的使用环境,可以让用户自定义的数据,并且以key-value的形式配置到properties文件中
接下来针对这两方面的数据进行处理
PropertiesUtils工具类自定义数据
通过PropertiesUtils工具类,统一对properties文件夹下的所有.properties文件进行加载,并存入内存中
package cn.itcast.generate.utils;import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.*;/*** 需要将自定义的配置信息写入到properties文件中* 配置到相对于工程的properties文件夹下*/
public class PropertiesUtils {public static Map<String,String> customMap = new HashMap<>();static {File dir = new File("properties");try {List<File> files = FileUtils.searchAllFile(new File(dir.getAbsolutePath()));for (File file : files) {if(file.getName().endsWith(".properties")) {Properties prop = new Properties();prop.load(new FileInputStream(file));customMap.putAll((Map) prop);}}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) {PropertiesUtils.customMap.forEach((k, v)->{System.out.println(k+"--"+v);});}
}
骚戴理解:这里的PropertiesUtils工具类的作用就是把properties文件夹下的所有.properties文件读取处理,主要就是用于用户自定义的数据,可能有的数据不是从数据库里面读取的,所以就可以定义在properties配置文件里,然后读取配置文件获取数据,再加载到模板里面
导入代码生成器依赖的配置文件
#SQL类型和java类型替换规则
VARCHAR=String
BIGINT=Long
INT=Integer
DATE=java.util.Date
DATETIME=java.util.Date
DOUBLE=Double
TEXT=String
VARCHAR2=String
NVARCHAR2=String
NUMBER=Long
CHAR=String
MEDIUMTEXT=String
TINYINT=Integer
LONGTEXT=String#Table的前缀或者后缀
tableRemovePrefixes=tb_,co_,pe_,bs_
元数据处理
加载指定数据库表,将表信息转化为实体类对象(Table)
package cn.itcast.generate.utils;import cn.itcast.generate.entity.Column;
import cn.itcast.generate.entity.DataBase;
import cn.itcast.generate.entity.Table;import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;public class DataBaseUtils {//获取到mysql中所有的数据库名称//获取数据库连接public static Connection getConnection(DataBase db) throws Exception {Properties props = new Properties();props.put("remarksReporting","true");//获取数据库的备注信息props.put("user",db.getUserName());props.put("password",db.getPassWord());Class.forName(db.getDriver());//注册驱动return DriverManager.getConnection(db.getUrl(),props);}//获取数据库列表public static List<String> getSchemas(DataBase db) throws Exception {//1.获取元数据Connection connection = getConnection(db);DatabaseMetaData metaData = connection.getMetaData();//2.获取所有数据库列表ResultSet rs = metaData.getCatalogs();List<String> list = new ArrayList<>();while (rs.next()) {list.add(rs.getString(1));}rs.close();connection.close();return list;}/*** 获取数据库中的表和字段构造实体类* Table对象** 1.参数* DataBase 数据库对象* 2.操作步骤* 1.获取连接* 2.获取databasemetaData* 3.获取当前数据库中的所有表* 4.获取每个表中的所有字段* 5.封装到java对象中即可*/public static List<Table> getDbInfo(DataBase db) throws Exception {//1.获取连接Connection connection = getConnection(db);//2.获取元数据DatabaseMetaData metaData = connection.getMetaData();//3.获取当前数据库中的所有表ResultSet tables = metaData.getTables(null, null, "pe_permission", new String[]{"TABLE"});List<Table> list = new ArrayList<>();while (tables.next()) {Table tab = new Table();//i.表名String tableName = tables.getString("TABLE_NAME"); //bs_user User//ii.类名String className = removePrefix(tableName);//iii.描述String remarks = tables.getString("REMARKS");//iiii.主键(主键可能是组合主键,有多个,所以primaryKeys是多个主键的set集合)ResultSet primaryKeys = metaData.getPrimaryKeys(null, null, tableName);String keys = "";while (primaryKeys.next()) {String keyname = primaryKeys.getString("COLUMN_NAME");keys += keyname+",";}tab.setName(tableName);tab.setName2(className);tab.setComment(remarks);tab.setKey(keys);//处理表中的所有字段ResultSet columns = metaData.getColumns(null, null, tableName, null);List <Column> columnList = new ArrayList<>();while (columns.next()) {Column cn = new Column();//构造Column对象//列名称String columnName = columns.getString("COLUMN_NAME"); //user_id userId , create_time createTimecn.setColumnName(columnName);//属性名String attName = StringUtils.toJavaVariableName(columnName);cn.setColumnName2(attName);//java类型和数据库类型String dbType = columns.getString("TYPE_NAME");//VARCHAR,DATETIMEcn.setColumnDbType(dbType);String javaType = PropertiesUtils.customMap.get(dbType);cn.setColumnType(javaType);//备注String columnRemark = columns.getString("REMARKS");//VARCHAR,DATETIMEcn.setColumnComment(columnRemark);//是否主键String pri = null;if(StringUtils.contains(columnName ,keys.split(","))) {pri = "PRI";}cn.setColumnKey(pri);columnList.add(cn);}columns.close();tab.setColumns(columnList);list.add(tab);}tables.close();connection.close();return list;}public static String removePrefix(String tableName) {String prefix = PropertiesUtils.customMap.get("tableRemovePrefixes");//bs_, tb_ , co_ ,String temp = tableName; //bs_userfor(String pf : prefix.split(",")) {temp = StringUtils.removePrefix(temp,pf,true);}//temp = userreturn StringUtils.makeAllWordFirstLetterUpperCase(temp);}public static void main(String[] args) throws Exception {DataBase db = new DataBase("MYSQL","ihrm");db.setUserName("root");db.setPassWord("111111");List<Table> dbInfo = DataBaseUtils.getDbInfo(db);for (Table table : dbInfo) {List<Column> columns = table.getColumns();for (Column column : columns) {System.out.println(column);}}}
}
骚戴理解:removePrefix方法就是把数据库表名的前缀去掉,然后再转成首字母大写的实体类名称,表名前缀都写在了配置文件里面,所以先通过PropertiesUtils获取配置文件的前缀配置信息,如下所示,然后调用StringUtils的removePrefix方法去掉前缀,最后调用StringUtils的makeAllWordFirstLetterUpperCase方法转成首字母大写
#Table的前缀或者后缀
tableRemovePrefixes=tb_,co_,pe_,bs_
实现代码生成
需求分析
为了代码更加直观和易于调用,实现代码生成共有两个类组成:
- UI界面统一调用的入口类:GeneratorFacade
方便多种界面调用,主要完成数据模型获取,调用核心代码处理类完成代码生成
- 代码生成核心处理类:Generator
根据数据模型和模板文件路径,统一生成文件到指定的输出路径
模板生成
- 配置统一调用入口类GeneratorFacade
package cn.itcast.generate.core;import cn.itcast.generate.entity.DataBase;
import cn.itcast.generate.entity.Settings;
import cn.itcast.generate.entity.Table;
import cn.itcast.generate.utils.DataBaseUtils;
import cn.itcast.generate.utils.PropertiesUtils;import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** 1.采集用户UI界面输入的数据* 模板位置* 代码生成路径* 工程配置对象 setting* 数据库对象 DataBase* 2.准备数据模型* 1.自定义配置* 2.元数据* 3.setting* 3.调用核心处理类完成代码生成工作* 方法:Generator*/
public class GeneratorFacade {private String templatePath;private String outPath;private Settings settings;private DataBase db;private Generator generator;public GeneratorFacade(String templatePath, String outPath, Settings settings, DataBase db) throws Exception {this.templatePath = templatePath;this.outPath = outPath;this.settings = settings;this.db = db;this.generator = new Generator(templatePath,outPath);}/*** 1.准备数据模型* 2.调用核心处理类完成代码生成工作*/public void generatorByDataBase() throws Exception {List<Table> tables = DataBaseUtils.getDbInfo(db);for (Table table : tables) {//对每一个Table对象进行代码生成/*** 数据模型* 调用Generator核心处理类*/Map<String,Object> dataModel = getDataModel(table);
//
// for(Map.Entry<String,Object> entry:dataModel.entrySet()) {
// System.out.println(entry.getKey() + "--" + entry.getValue());
// }
// System.out.println("------------------------");generator.scanAndGenerator(dataModel);}}/*** 根据table对象获取数据模型*/private Map<String,Object> getDataModel(Table table) {Map<String,Object> dataModel = new HashMap<>();//1.自定义配置dataModel.putAll(PropertiesUtils.customMap);//2.元数据dataModel.put("table",table); //table.name2//3.settingdataModel.putAll(this.settings.getSettingMap());//4.类型dataModel.put("ClassName",table.getName2());return dataModel;}
}
骚戴理解:通过DataBaseUtils.getDbInfo(db);获取到的是数据库表数据的元数据信息,getDataModel方法的作用是为了封装数据模型,数据模型结合模板生成代码,数据模型包括自定义配置、元数据、setting里面的数据,而ClassName只是为了方便后面拿这个类名而已,所以也封装在其中
- 处理模板代码生成的核心类Generator
package cn.itcast.generate.core;import cn.itcast.generate.utils.FileUtils;
import freemarker.cache.FileTemplateLoader;
import freemarker.cache.StringTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;import java.io.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** 代码生成器的核心处理类* 使用Freemarker完成文件生成* 数据模型 + 模板* 数据:* 数据模型* 模板的位置* 生成文件的路径**/
public class Generator {private String templatePath;//模板路径private String outPath;//代码生成路径private Configuration cfg;public Generator(String templatePath, String outPath) throws Exception {this.templatePath = templatePath;this.outPath = outPath;//实例化Configuration对象cfg = new Configuration();//指定模板加载器FileTemplateLoader ftl = new FileTemplateLoader(new File(templatePath));cfg.setTemplateLoader(ftl);}/*** 代码生成* 1.扫描模板路径下的所有模板* 2.对每个模板进行文件生成(数据模型)*/public void scanAndGenerator(Map<String,Object> dataModel) throws Exception {//1.根据模板路径找到此路径下的所有模板文件List<File> fileList = FileUtils.searchAllFile(new File(templatePath));//2.对每个模板进行文件生成for (File file : fileList) {executeGenertor(dataModel,file);}}/*** 对模板进行文件生成* @param dataModel : 数据模型* @param file : 模板文件* 模板文件:c:com.ihrm.system.abc.java*/private void executeGenertor(Map<String,Object> dataModel,File file) throws Exception {//1.文件路径处理 (E:\模板\${path1}\${path2}\${path3}\${ClassName}.java)//templatePath : E:\模板\String templateFileName = file.getAbsolutePath().replace(this.templatePath,"");String outFileName = processTemplateString(templateFileName,dataModel);//2.读取文件模板Template template = cfg.getTemplate(templateFileName);template.setOutputEncoding("utf-8");//指定生成文件的字符集编码//3.创建文件File file1 = FileUtils.mkdir(outPath, outFileName);//4.模板处理(文件生成)FileWriter fw = new FileWriter(file1);template.process(dataModel,fw);fw.close();}public String processTemplateString(String templateString,Map dataModel) throws Exception {StringWriter out = new StringWriter();Template template = new Template("ts",new StringReader(templateString),cfg);template.process(dataModel,out);return out.toString();}public static void main(String[] args) throws Exception {String templatePath = "C:\\Users\\ThinkPad\\Desktop\\ihrm\\day13\\资源\\测试\\模板";String outPath = "C:\\Users\\ThinkPad\\Desktop\\ihrm\\day13\\资源\\测试\\生成代码路径";Generator generator = new Generator(templatePath, outPath);Map <String,Object> dataModel = new HashMap<>();dataModel.put("username","张三");generator.scanAndGenerator(dataModel);}}
骚戴理解:executeGenertor方法这段代码的作用是根据模板文件和数据模型生成代码文件。
1. String templateFileName = file.getAbsolutePath().replace(this.templatePath,"");
这行代码的作用是将模板文件的绝对路径中的模板根路径(即templatePath)替换为空字符串,得到模板文件名(不包含根路径)。
2. String outFileName = processTemplateString(templateFileName,dataModel);
这行代码的作用是将模板文件名中的变量替换为具体的值,得到输出文件的文件名。
3. Template template = cfg.getTemplate(templateFileName);
这行代码的作用是根据模板文件名,从Configuration对象中获取对应的Template对象。
4. template.setOutputEncoding("utf-8");
这行代码的作用是指定生成文件的字符集编码为UTF-8。
5. File file1 = FileUtils.mkdir(outPath, outFileName);
这行代码的作用是根据输出路径和输出文件名,创建输出文件。
6. FileWriter fw = new FileWriter(file1);
这行代码的作用是创建一个FileWriter对象,用于将模板处理后的结果输出到指定的文件中。
7. template.process(dataModel,fw);
这行代码的作用是将数据模型和FileWriter对象作为参数传入Template的process方法中,根据数据模型中的值,将模板文件中的变量替换为具体的值,并将输出结果写入FileWriter对象中。
8. fw.close();
这行代码的作用是关闭FileWriter对象,释放资源。
路径处理
使用字符串模板对文件生成路径进行统一处理
public String processTemplateString(String templateString,Map dataModel) throws Exception {StringWriter out = new StringWriter();Template template = new Template("ts",new StringReader(templateString),cfg);template.process(dataModel,out);return out.toString();}
骚戴理解:这段代码是使用FreeMarker模板引擎来处理模板字符串,将模板字符串中的变量替换为具体的值,最终返回替换后的字符串。
1.StringWriter out = new StringWriter();
这行代码的作用是创建一个StringWriter对象,用于接收模板处理后的输出结果。
2.Template template = new Template("ts",new StringReader(templateString),cfg);
这行代码的作用是创建一个Template对象,用于解析模板字符串并进行变量替换。其中,第一个参数是模板名称(随意取名),第二个参数是StringReader对象,用于读取模板字符串,第三个参数是Configuration对象,用于配置模板引擎的相关参数。
3.template.process(dataModel,out);
这行代码的作用是将数据模型和输出流作为参数传入Template的process方法中,根据数据模型中的值,将模板字符串中的变量替换为具体的值,并将输出结果写入输出流。
4.return out.toString();
这行代码的作用是将输出流中的内容转换为字符串,并返回该字符串。
制作模板
模板制作的约定
- 模板位置
模板统一放置到相对于当前路径的模板文件夹下
- 自定义数据
自定义的数据以 .propeties 文件(key-value)的形式存放入相对于当前路径的 properties 文件夹下
- 数据格式
名称 | 说明 |
author | 作者 |
project | 工程名 |
path1 | 包名1 |
path2 | 包名2 |
path3 | 包名3 |
pPackage | 完整包名 |
projectComment | 工程描述 |
ClassName | 类名 |
table | 数据库信息 |
table中数据内容:
name | 表名 |
comment | 表注释 |
key | 表主键 |
columns | 所有列信息 |
columnName | 字段列名 |
columnName2 | 属性名 |
columnType | java类型 |
columnDbType | 数据库类型 |
columnComment | 注释 |
columnKey | 是否主键 |
需求分析
制作通用的SpringBoot程序的通用模板
- 实体类
类路径,类名,属性列表(getter,setter方法)
- 持久化层
类路径,类名,引用实体类
- 业务逻辑层
类路径,类名,引用实体类,引用持久化层代码
- 视图层
类路径,类名,引用实体类,引用业务逻辑层代码,请求路径
- 配置文件
pom文件,springboot配置文件
SpringBoot通用模板
实体类
package ${pPackage}.pojo;import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.io.Serializable;@Entity
@Table(name = "${table.name}")
public class ${ClassName} implements Serializable {//这里table.columns是因为上面数据模型里面放的是 dataModel.put("table",table);<#list table.columns as column><#if column.columnKey??>@Id</#if>private ${column.columnType} ${column.columnName2};</#list><#list table.columns as column>public void set${column.columnName2?cap_first}(${column.columnType} value) {this.${column.columnName2} = value;}public ${column.columnType} get${column.columnName2?cap_first}() {return this.${column.columnName2};}</#list>
}
骚戴理解:这段代码使用FreeMarker模板语言生成Java类的属性和getter/setter方法。
具体来说,这段代码包含了以下两个部分:
1. 属性生成部分
-<#list table.columns as column>:这行代码使用FreeMarker的list指令,遍历数据模型中的table.columns字段,将每个元素赋值给变量column。
-<#if column.columnKey??>:这行代码使用FreeMarker的if指令,判断当前列是否为主键列。
-@Id:如果当前列是主键列,则生成@Id注解。
-<#if>:if指令的结束标签。
-private ${column.columnType} ${column.columnName2};:生成Java类的私有属性,属性名和属性类型分别对应数据模型中的column.columnName2和column.columnType。
-</#list>:list指令的结束标签。
2. getter/setter方法生成部分
-<#list table.columns as column>:这行代码使用FreeMarker的list指令,遍历数据模型中的table.columns字段,将每个元素赋值给变量column。
-public void set${column.columnName2?cap_first}(${column.columnType} value) {:生成Java类的setter方法,方法名为set+属性名(首字母大写),方法参数类型和属性类型相同,方法体为将参数值赋值给属性,其中?cap_first是使用内置函数,可以把column.columnName2变成首字母大写。
-public ${column.columnType} get${column.columnName2?cap_first}() {:生成Java类的getter方法,方法名为get+属性名(首字母大写),方法返回值类型为属性类型,方法体为返回属性值。
-</#list>:list指令的结束标签。
总之,这段代码的作用是根据数据模型中的表结构信息,生成Java类的属性和getter/setter方法。
Service层
<#assign classNameLower = ClassName ? uncap_first>
package ${pPackage}.service;import com.ihrm.common.utils.IdWorker;
import ${pPackage}.dao.${ClassName}Dao;
import ${pPackage}.pojo.${ClassName};
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;@Service
public class ${ClassName}Service {@Autowiredprivate ${ClassName}Dao ${classNameLower}Dao;@Autowiredprivate IdWorker idWorker;/*** 保存*/public void add(${ClassName} ${classNameLower}) {//基本属性的设置String id = idWorker.nextId()+"";${classNameLower}.setId(id);${classNameLower}Dao.save(${classNameLower});}/*** 更新*/public void update(${ClassName} ${classNameLower}) {${classNameLower}Dao.save(${classNameLower});}/*** 删除*/public void deleteById(String id) {${classNameLower}Dao.deleteById(id);}/*** 根据id查询*/public ${ClassName} findById(String id) {return ${classNameLower}Dao.findById(id).get();}/*** 查询列表*/public List<${ClassName}> findAll() {return ${classNameLower}Dao.findAll();}
}
Controller层
<#assign classNameLower = ClassName ? uncap_first>
package ${pPackage}.controller;import com.ihrm.common.entity.Result;
import com.ihrm.common.entity.ResultCode;
import com.ihrm.common.exception.CommonException;import ${pPackage}.service.${ClassName}Service;
import ${pPackage}.pojo.${ClassName};
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;import java.util.List;//解决跨域问题
@CrossOrigin
@RestController
@RequestMapping(value="/${classNameLower}")
public class ${ClassName}Controller {@Autowiredprivate ${ClassName}Service ${classNameLower}Service;//保存@RequestMapping(value="",method = RequestMethod.POST)public Result save(@RequestBody ${ClassName} ${classNameLower}) {//业务操作${classNameLower}Service.add(${classNameLower});return new Result(ResultCode.SUCCESS);}//根据id更新@RequestMapping(value = "/{id}",method = RequestMethod.PUT)public Result update(@PathVariable(value="id") String id, @RequestBody ${ClassName} ${classNameLower} ) {//业务操作${classNameLower}.setId(id);${classNameLower}Service.update(${classNameLower});return new Result(ResultCode.SUCCESS);}//根据id删除@RequestMapping(value="/{id}",method = RequestMethod.DELETE)public Result delete(@PathVariable(value="id") String id) {${classNameLower}Service.deleteById(id);return new Result(ResultCode.SUCCESS);}//根据id查询@RequestMapping(value="/{id}",method = RequestMethod.GET)public Result findById(@PathVariable(value="id") String id) throws CommonException {${ClassName} ${classNameLower} = ${classNameLower}Service.findById(id);return new Result(ResultCode.SUCCESS,${classNameLower});}//查询全部@RequestMapping(value="",method = RequestMethod.GET)public Result findAll() {List<${ClassName}> list = ${classNameLower}Service.findAll();Result result = new Result(ResultCode.SUCCESS);result.setData(list);return result;}
}
Dao层
package ${pPackage}.dao;import ${pPackage}.pojo.${ClassName};
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;public interface ${ClassName}Dao extends JpaRepository<${ClassName},String> ,JpaSpecificationExecutor<${ClassName}> {
}
配置文件
- application.yml
server: port: 9001
spring: application: name: ${project}-${path3} #指定服务名datasource: driverClassName: ${driverName}url: ${url}username: ${dbuser}password: ${dbpassword}jpa: database: MySQLshow-sql: true
- pom.xml
<?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>${path2}_parent</artifactId><groupId>${path1}.${path2}</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>${path2}_${project}</artifactId><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>${path1}.${path2}</groupId><artifactId>${path2}_common</artifactId><version>1.0-SNAPSHOT</version></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-client</artifactId></dependency></dependencies>
</project>