代码生成器 代码生成器概述 背景 基于MVC
架构的传统进行模块开发的步骤:
创建数据库表
根据表字段设计对应的实体类(PO)
编写增删改查持久层(dao)代码
根据业务逻辑编写service层代码
controller层代码和前端页面
通常只要知道了一个表的结构,增删改查的前后台代码格式基本上就是固定的,比如controller
、service
、dao
、实体类Po
、前端jsp
等模块的代码在不同数据库表基础上都具有类似的结构,所以正对这部分代码,可以让计算机自动帮我们生成,让精力聚焦于更复杂业务代码的编写上
需求分析 代码生成器依据公共的模板和数据库表中的信息自动生成代码
传统开发中,由于不同数据库基础增删改查代码结构相似,程序员通常都是将一份已经写好的代码进行复制和修改,再根据不同业务需求在此基础上进行代码的完善,可以将这份代码称之为代码模板
生成的基础代码与对应数据库密切相关,所以除代码模板之外还需要数据库表的信息来对模板相关内容进行填充
基本思路
代码生成自动生成代码的几个重点需要解决的问题
数据库以及表的解析,用于生成实体类以及其他代码
从数据库中解析出数据库中表的名称以及表字段等属性:可以根据表名确定实体类名称,根据字段确定实体类中的属性
模板开发生成代码文件
模板中定义公共的基础代码和需要替换的占位符内容,然后根据解析好的数据库信息进行数据替换并生成对应代码文件
基础框架模板代码的抽取
FreeMaker的使用 FreeMaker概述 FreeMaker
是一款模板引擎:一种基于模板的,用来输出文本的通用工具。
它是为Java程序员提供的一个开发包或者类库,它并不面向最终用户,而是面向程序员提供的嵌入他们开发产品过程中的一款应用程序。
FreeMaker
最早是用来设计生成HTML网页的。使用 MVC 模式的动态网页的构思使得你可以将前端设计者(编写 HTML)从 程序员中分离出来。所有人各司其职,发挥其擅长的一面。网页设计师可以改写页面的显示效果而不受程序员编译 代码的影响,因为应用程序的逻辑(Java 程序)和页面设计(FreeMarker 模板)已经分开了。页面模板代码不会受到复 杂的程序代码影响。这种分离的思想即便对一个程序员和页面设计师是同一个人的项目来说都是非常有用的,因为 分离使得代码保持简洁而且便于维护。
FreeMaker的应用场景 动态页面 基于模板配置和表达式生成页面文件,可以像jsp一样被客户端访问
页面静态化 对于系统中频繁使用数据库进行查询但是内容更新很小的应用,都可以用FreeMarker将网页静态化,这样就避免 了大量的数据库访问请求,从而提高网站的性能
代码生成器 可以自动根据后台配置生成页面或者代码
FreeMaker的优势
强大的模板语言:有条件的块,迭代,赋值,字符串和算术运算和格式化,宏和函数,编码等更多的功能;
多用途且轻量:零依赖,输出任何格式,可以从任何地方加载模板(可插拔),配置选项丰富;
智能的国际化和本地化:对区域设置和日期/时间格式敏感;
XML处理功能:将dom-s放入到XML数据模型并遍历它们,甚至处理他们的声明;
通用的数据模型:通过可插拔适配器将java对象暴露于模板作为变量树;
FreeMaker的基本使用 使用FreeMaker
的重点
编写模板文件
使用FreeMaker解析模板,并将模板中的对应内容替换掉
导入pom依赖 1 2 3 4 5 <dependency > <groupId > org.freemarker</groupId > <artifactId > freemarker</artifactId > <version > 2.3.20</version > </dependency >
FreeMaker使用步骤 编写对应的模板文件 FreeMaker
的模板文件一般以.ftl
结尾,以什么后缀结尾并不影响最终的处理
模板数据填充替换 解析模板文件,将数据替换到模板中对应位置,并生成需要的文件,此过程一般需要经历以下5个步骤
创建FreeMaker的配置类
指定模板加载器,将模板存入缓存
获取模板
构造数据模型
输出生成的文件
入门案例:文件模板 创建模板文件template01.ftl 在工程src
同级目录下新建文件templates/template01.ftl
使用FreeMaker进行数据填充,生成指定文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public class FreeMakerParse { public static void main (String[] args) throws IOException, TemplateException { Configuration cfg = new Configuration (); String templateFolder = "templates" ; FileTemplateLoader loader = new FileTemplateLoader (new File (templateFolder)); cfg.setTemplateLoader(loader); String templatePath = "template01.ftl" ; Template template = cfg.getTemplate(templatePath); Map<String, Object> dataMap = new HashMap <>(); dataMap.put("username" ,"李白" ); String outPath = "Files/out1.txt" ; template.process(dataMap,new FileWriter (new File (outPath))); } }
项目目录结构
入门案例:字符模板 模板通过字符串的形式在代码中直接定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class FreeMakerStringParse { public static void main (String[] args) throws IOException, TemplateException { Configuration cfg = new Configuration (); cfg.setTemplateLoader(new StringTemplateLoader ()); String templateStr = "你来自于哪个城市:${cityName}" ; Template template = new Template ("strYemplate" , new StringReader (templateStr), cfg); Map<String, Object> dataMap = new HashMap <>(); dataMap.put("cityName" ,"安徽" ); template.process(dataMap,new PrintWriter (System.out)); } }
FreeMaker模板 经过前面案例,明显感觉到FreeMaker
的Java
代码部分编写的步骤比较固定,FreeMaker
的重点部分在于模板的创建编写
概述 FreeMaker
模板文件主要由以下5个部分组成:
数据模型:模板中用到的数据
文本:直接输出的部分
注释:模板中的注释格式<#-- 注释内容 -->
插值:即${}
或者#{}
部分,将会使用数据模型中的部分来对这部分进行替换
FTL指令:FreeMaker
指令,和HTML标记类似,名字前面加#
区分,不会输出
数据模型 FreeMarker(还有模板开发者)并不关心数据是如何计算的,FreeMarker 只是知道真实的数据是什么。模板能用 的所有数据被包装成 data-model 数据模型
数据模型结构
比如要获取user数据,模板中应该写为:${user}
,要获取url数据,模板中应该写为:${latestProduct.url}
模板常用标签
${}
,插值(占位符),FreeMaker
在输出时,会用数据模型中的实际值进行替代
<#..>
: FTL标记,类似于HTML标记,#
主要是为了与HTML进行区分
<@>
:宏,自定义标签
<#-- -->
注释符
模板中常用的指令(FTL标记) if指令 条件分支控制语句
在<#
内写的占位符,无需用#{}
包裹
1 2 3 4 5 6 7 <# if flag=1> 传入的数据是1 <# elseif flag=2> 传入的数据据是2 <# else> 传入的数据是其他 </# if>
对应的FreeMaker解析的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public class FreeMakerParse { public static void main (String[] args) throws IOException, TemplateException { Configuration cfg = new Configuration (); String templateFolder = "templates" ; FileTemplateLoader loader = new FileTemplateLoader (new File (templateFolder)); cfg.setTemplateLoader(loader); String templatePath = "template01.ftl" ; Template template = cfg.getTemplate(templatePath); Map<String, Object> dataMap = new HashMap <>(); dataMap.put("username" ,"李白" ); dataMap.put("flag" ,3 ); template.process(dataMap,new PrintWriter (System.out)); } }
list指令 list
指令是迭代输出指令,用于迭代输出数据模型中的集合数据
1 2 3 <# list userList as user> 当前登录用户:$ {user} </# list>
对应的FreeMaker解析的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public class FreeMakerParse { public static void main (String[] args) throws IOException, TemplateException { Configuration cfg = new Configuration (); String templateFolder = "templates" ; FileTemplateLoader loader = new FileTemplateLoader (new File (templateFolder)); cfg.setTemplateLoader(loader); String templatePath = "template01.ftl" ; Template template = cfg.getTemplate(templatePath); Map<String, Object> dataMap = new HashMap <>(); dataMap.put("username" ,"李白" ); dataMap.put("flag" ,3 ); ArrayList<String> list = new ArrayList <>(); list.add("曹阿满" ); list.add("刘大耳" ); list.add("孙十万" ); list.add("诸葛村夫" ); dataMap.put("userList" ,list); template.process(dataMap,new PrintWriter (System.out)); } }
执行结果
list结合break指令使用 1 2 3 4 5 6 <# list userList as user> 当前登录用户:$ {user},当前用户编号:$ {user_ index+1} <# if user_ has_ next>=========</# if> <# if user="孙十万"><# break></# if> </# list>
xxx_index
:当前变量的索引值
xxx_has_next
:是否存在下一个对象
<#break>
:跳出循环迭代
运行结果
include指令 模板包含,一个模板中包含另一个模板内容
1 <# include "需要导入包含模板路径,支持相对路径的写法,只是要用引号括起来">
assign指令 assign
指令用于在模板中自定义个变量,可以存储到数据模型中,在模板其他部分引用
模板其他位置可以通过${age}
来引用该变量
内置函数 FreeMarker还提供了一些内建函数来转换输出,可以在任何变量后紧跟?,?后紧跟内建函数,就可通过内建函 数来转换输出变量。
使用语法:${username?upper_case}
下面是常用的内建的字符串函数:
?html:html字符转义
?cap_first: 字符串的第一个字母变为大写形式
?lower_case :字符串的小写形式
?upper_case :字符串的大写形式
?trim:去掉字符串首尾的空格
?substring:截字符串
?lenth: 取长度
?size: 序列中元素的个数
?int : 数字的整数部分(比如- 1.9?int 就是- 1)
?replace:字符串替换
数据库元数据 背景 什么是数据库元数据 元数据(meta data
)是指定义数据结构的数据,数据库元数据是指定义数据库各类对象结构的数据,例如数据库中的数据库名、表名、列名、用户名、版本名以及从SQL语句得到的结果中大部分字符串都是元数据
数据库元数据作用
在应用设计时,能够充分利用数据库元数据
深入理解了数据库组织结构,再去理解数据访问相关框架的实现原理会更加容易
如何获取数据库元数据 JDBC来处理数据库的接口主要有三个,即Connection
,PreparedStatement
和ResultSet
这三个, 而对于这三个接口,还可以获取不同类型的元数据,通过这些元数据类获得一些数据库的信息。
数据库元数据 概述 数据库元数据(DatabaseMetaData
):是由Connection
对象通过getMetaData
方法获取而来,主要封装了是对 数据库本身的一些整体综合信息,例如数据库的产品名称,数据库的版本号,数据库的URL,是否支持事务等等
下面是getMetaData
的一些常用方法:
getDatabaseProductName:获取数据库的产品名称
getDatabaseProductVersion:获取数据库的版本号
getUserName:获取数据库的用户名
getURL:获取数据库连接的URL
getDriverName:获取数据库的驱动名称
driverVersion:获取数据库的驱动版本号
isReadOnly:查看数据库是否只允许读操作
supportsTransactions:查看数据库是否支持事务
使用案例 引入pom依赖 1 2 3 4 5 <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 5.1.6</version > </dependency >
获取数据库连接 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private Connection conn; @Before public void init () throws Exception { Class.forName("com.mysql.jdbc.Driver" ); String url = "jdbc:mysql://127.0.0.1:3306?useUnicode=true&characterEncoding=utf8" ; String userName = "train_member" ; String passWd = "wu123456" ; Properties props = new Properties (); props.put("user" ,userName); props.put("password" ,passWd); props.put("remarksReporting" ,"true" ); conn = (Connection) DriverManager.getConnection(url, props); }
获取数据库综合信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test public void test01 () throws SQLException { DatabaseMetaData metaData = conn.getMetaData(); System.out.println(metaData.getDatabaseProductName()); System.out.println(metaData.getDatabaseProductVersion()); System.out.println(metaData.getUserName()); System.out.println(metaData.getURL()); System.out.println(metaData.getDriverName()); System.out.println(metaData.getDriverVersion()); System.out.println(metaData.isReadOnly()); System.out.println(metaData.supportsTransactions()); }
1 2 3 4 5 6 7 8 9 # 运行结果MySQL 5.7.35-log train_ member@localhost jdbc:mysql://127.0.0.1:3306?useUnicode=true& characterEncoding=utf8 MySQL-AB JDBC Driver mysql-connector-java-5.1.6 ( Revision: $ {svn.Revision} ) false true
获取数据库列表 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void test2 () throws SQLException { DatabaseMetaData metaData = conn.getMetaData(); ResultSet catalogs = metaData.getCatalogs(); while (catalogs.next()){ System.out.println(catalogs.getString(1 )); } catalogs.close(); conn.close(); }
1 2 3 # 运行结果information_ schema train_ member
获取数据库中表信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Test public void test03 () throws SQLException { DatabaseMetaData metaData = conn.getMetaData(); ResultSet rs = metaData.getTables("train_member" , null , null , new String []{"TABLE" }); while (rs.next()){ System.out.println(rs.getString(1 )); System.out.println(rs.getString(2 )); System.out.println(rs.getString(3 )); System.out.println(rs.getString(4 )); System.out.println(rs.getString(5 )); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 运行结果train_ member null member TABLE train_ member null passenger TABLE train_ member null ticket TABLE
获取指定表中的字段信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test public void test04 () throws SQLException { DatabaseMetaData metaData = conn.getMetaData(); ResultSet rs = metaData.getColumns("train_member" , null , "passenger" , null ); while (rs.next()){ System.out.println(rs.getString("COLUMN_NAME" )); } rs.close(); conn.close(); }
具体详细用法,可以参考JDK官方文档
参数元数据 参数元数据(ParameterMetaData
):是由PreparedStatement
对象通过getParameterMetaData
方法获取而 来,主要是针对PreparedStatement
对象和其预编译的SQL命令语句提供一些信息,ParameterMetaData
能提供 占位符参数的个数,获取指定位置占位符的SQL类型等等
参数元数据在实际应用场景中一般很少用到,只需了解即可,主要常用方法:
getParameterCount:获取预编译SQL语句中占位符参数的个数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @Test public void test05 () throws Exception { Class.forName("com.mysql.jdbc.Driver" ); String url = "jdbc:mysql://127.0.0.1:3306/train_member?useUnicode=true&characterEncoding=utf8" ; String userName = "train_member" ; String passWd = "wu123456" ; Properties props = new Properties (); props.put("user" ,userName); props.put("password" ,passWd); props.put("remarksReporting" ,"true" ); conn = (Connection) DriverManager.getConnection(url, props); String sql = "select * from passenger where id=?" ; PreparedStatement pstm = conn.prepareStatement(sql); pstm.setString(1 ,"1733754970974588928" ); ParameterMetaData parameterMetaData = pstm.getParameterMetaData(); int count = parameterMetaData.getParameterCount(); System.out.println(count); }
结果集元数据 结果集元数据(ResultSetMetaData
):是由ResultSet
对象通过getMetaData
方法获取而来,主要是针对由数据 库执行的SQL脚本命令获取的结果集对象ResultSet
中提供的一些信息,比如结果集中的列数、指定列的名称、指定 列的SQL类型等等,可以说这个是对于框架来说非常重要的一个对象
关于ResultSetMetaData的常用方法:
getColumnCount:获取结果集中列项的个数
.getColumnName: 获取列名
getColumnTypeName:getColumnTypeName:获取指定列的SQL数据类型
getColumnClassName:获取指定列SQL类型对应于Java中的数据类型(包名加类名)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Test public void test06 () throws Exception { Class.forName("com.mysql.jdbc.Driver" ); String url = "jdbc:mysql://127.0.0.1:3306/train_member?useUnicode=true&characterEncoding=utf8" ; String userName = "train_member" ; String passWd = "wu123456" ; Properties props = new Properties (); props.put("user" ,userName); props.put("password" ,passWd); props.put("remarksReporting" ,"true" ); conn = (Connection) DriverManager.getConnection(url, props); String sql = "select * from passenger where id=?" ; PreparedStatement pstm = conn.prepareStatement(sql); pstm.setString(1 ,"1733754970974588928" ); ResultSet rs = pstm.executeQuery(); ResultSetMetaData metaData = rs.getMetaData(); int columnCount = metaData.getColumnCount(); for (int i=1 ;i<=columnCount;i++){ System.out.println(metaData.getColumnName(i)); System.out.println(metaData.getColumnTypeName(i)); System.out.println(metaData.getColumnClassName(i)); System.out.println("================================" ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 # 运行结果id BIGINT java.lang.Long ================================ member_ id BIGINT java.lang.Long ================================ name VARCHAR java.lang.String ================================ id_ card VARCHAR java.lang.String ================================ type CHAR java.lang.String ================================ create_ time DATETIME java.sql.Timestamp ================================ update_ time DATETIME java.sql.Timestamp ================================
代码生成器制作 思路分析
完成代码生成器需要以下几个步骤:
用户填写数据库相关信息,相关信息封装到实体类,便于后续操作
借助元数据,将数据库表信息、数据库字段信息封装到对应实体类
构建FreeMaker数据模型,将数据库表对象和相关配置存入Map集合
利用FreeMaker完成代码生成
自定义公共代码模板
项目环境搭建 引入pom依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 <dependency > <groupId > org.freemarker</groupId > <artifactId > freemarker</artifactId > <version > 2.3.20</version > </dependency > <dependency > <groupId > junit</groupId > <artifactId > junit</artifactId > <version > 4.12</version > <scope > test</scope > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 5.1.6</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.30</version > </dependency > <dependency > <groupId > org.projectlombok</groupId > <artifactId > lombok</artifactId > <version > 1.18.30</version > </dependency > <dependency > <groupId > cn.hutool</groupId > <artifactId > hutool-all</artifactId > <version > 4.5.15</version > </dependency >
创建对应实体类 在整个项目中设计如下实体类的封装
数据库信息实体类 在代码生成过程中,用户需要提供数据库类型、数据名、用户名、密码、数据库地址等数据信息,为便于后续操作,可以将这些信息统一封装成一个对应的实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @Data @AllArgsConstructor @NoArgsConstructor public class DataBase { private static String mysqlUrl = "jdbc:mysql://[ip]:[port]/[db]?useUnicode=true&characterEncoding=UTF8" ; private static String oracleUrl = "jdbc:oracle:thin:@[ip]:[port]:[db]" ; private String dbType="MYSQL" ; private String dbName; private String driver="com.mysql.jdbc.Driver" ; private String userName; private String passWord; private String ip="127.0.0.1" ; private String port="3306" ; private String url; public DataBase (String dbType,String dbName,String userName,String passWord,String ip,String port) { this .dbType = dbType; this .dbName = dbName; this .ip=ip; this .port=port; this .userName = userName; this .passWord = passWord; if ("MYSQL" .equals(dbType.toUpperCase())){ this .driver = "com.mysql.jdbc.Driver" ; this .url = mysqlUrl.replace("[ip]" ,this .ip).replace("[port]" ,port).replace("[db]" ,this .dbName); }else { this .driver = "oracle.jdbc.driver.OracleDriver" ; this .url=oracleUrl.replace("[ip]" ,ip).replace("[port]" ,port).replace("[db]" ,dbName); } } }
配置信息实体类 用户需要输入项目的项目名称、全包名、项目的中文名等,为便于使用,也将这些数据封装成一个实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Data @AllArgsConstructor @NoArgsConstructor public class Settings { private String project="example" ; private String pPackage="com.example.demo" ; private String projectComment; private String author; private String path1="com" ; private String path2="example" ; private String path3="demo" ; private String pathAll; public Settings (String project, String pPackage, String projectComment, String author) { if (StrUtil.isNotBlank(project)) { this .project = project; } if (StrUtil.isNotBlank(pPackage)) { this .pPackage = pPackage; String[] paths = pPackage.split("\\." ); this .path1 = paths[0 ]; this .path2 = paths.length>1 ?paths[1 ]:path2; this .path3 = paths.length>2 ?paths[2 ]:path3; this .pathAll = pPackage.replaceAll("." ,"/" ); } this .projectComment = projectComment; this .author = author; } public Map<String, Object> getSettingMap () { Map<String, Object> map = new HashMap <>(); Field[] declaredFields = Settings.class.getDeclaredFields(); for (Field field : declaredFields) { field.setAccessible(true ); try { map.put(field.getName(), field.get(this )); }catch (Exception e){} } return map; } }
数据库表信息实体类 数据某个表的SQL表名、处理后的Java表名,以及数据库表的注释信息、字段列表等数据
1 2 3 4 5 6 7 8 9 10 11 12 13 @Data @AllArgsConstructor @NoArgsConstructor public class Table { private String tableName; private String poName; private String comment; private String key; private List<Column> columnsList; }
数据库字段实体类 数据库字段SQL名称、SQL数据类型、列备注信息,是否主键,以及对应Java实体类属性名、Java数据类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Data @AllArgsConstructor @NoArgsConstructor public class Column { private String columnName; private String fieldName; private String columnType; private String fieldType; private String columnComment; private String columnKey; }
数据库相关工具类的创建 将获取数据库连接,数据库资源关闭以及获取数据库表信息等功能封装成对应的工具类方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class DataBaseUtils { public static Connection getConnection (DataBase dataBase) throws Exception { Connection conn = null ; Class.forName(dataBase.getDriver()); String url = dataBase.getUrl(); Properties props = new Properties (); props.put("user" ,dataBase.getUserName()); props.put("password" ,dataBase.getPassWord()); props.put("remarksReporting" ,"true" ); conn = (Connection) DriverManager.getConnection(url, props); return conn; } public static List<String> getSchema (DataBase db) throws Exception { Connection conn = getConnection(db); DatabaseMetaData metaData = conn.getMetaData(); ResultSet catalogs = metaData.getCatalogs(); List<String> schemaList = new ArrayList <>(); while (catalogs.next()){ schemaList.add(catalogs.getString(1 )); } catalogs.close(); conn.close(); return schemaList; } }
字符串相关工具类创建 在整个代码编写中,涉及到大量字符串处理操作,比如数据库表名、字段名与Java实体类名、属性名的映射
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 package com.bang.generator.utils;import java.util.*;import java.util.regex.Matcher;import java.util.regex.Pattern;public class StringUtils { public static String removeCrlf (String str) { if (str == null ) return null ; return StringUtils.join(StringUtils.tokenizeToStringArray(str,"\t\n\r\f" )," " ); } private static final Map<String,String> XML = new HashMap <String,String>(); static { XML.put("apos" , "'" ); XML.put("quot" , "\"" ); XML.put("amp" , "&" ); XML.put("lt" , "<" ); XML.put("gt" , ">" ); } public static String unescapeXml (String str) { if (str == null ) return null ; for (String key : XML.keySet()) { String value = XML.get(key); str = StringUtils.replace(str, "&" +key+";" , value); } return str; } public static String removePrefix (String str,String prefix) { return removePrefix(str,prefix,false ); } public static String removePrefix (String str,String prefix,boolean ignoreCase) { if (str == null ) return null ; if (prefix == null ) return str; if (ignoreCase) { if (str.toLowerCase().startsWith(prefix.toLowerCase())) { return str.substring(prefix.length()); } }else { if (str.startsWith(prefix)) { return str.substring(prefix.length()); } } return str; } public static boolean isBlank (String str) { return str == null || str.trim().length() == 0 ; } public static boolean isNotBlank (String str) { return !isBlank(str); } public static String getExtension (String str) { if (str == null ) return null ; int i = str.lastIndexOf('.' ); if (i >= 0 ) { return str.substring(i+1 ); } return null ; } public static int countOccurrencesOf (String str, String sub) { if (str == null || sub == null || str.length() == 0 || sub.length() == 0 ) { return 0 ; } int count = 0 ; int pos = 0 ; int idx; while ((idx = str.indexOf(sub, pos)) != -1 ) { ++count; pos = idx + sub.length(); } return count; } public static boolean contains (String str,String... keywords) { if (str == null ) return false ; if (keywords == null ) throw new IllegalArgumentException ("'keywords' must be not null" ); for (String keyword : keywords) { if (str.equals(keyword)) { return true ; } } return false ; } public static String defaultString (Object value) { if (value == null ) { return "" ; } return value.toString(); } public static String defaultIfEmpty (Object value,String defaultValue) { if (value == null || "" .equals(value)) { return defaultValue; } return value.toString(); } public static String makeAllWordFirstLetterUpperCase (String sqlName) { String[] strs = sqlName.toLowerCase().split("_" ); String result = "" ; String preStr = "" ; for (int i = 0 ; i < strs.length; i++) { if (preStr.length() == 1 ) { result += strs[i]; }else { result += capitalize(strs[i]); } preStr = strs[i]; } return result; } public static int indexOfByRegex (String input,String regex) { Pattern p = Pattern.compile(regex); Matcher m = p.matcher(input); if (m.find()) { return m.start(); } return -1 ; } public static String toJavaVariableName (String str) { return uncapitalize(toJavaClassName(str)); } public static String toJavaClassName (String str) { return makeAllWordFirstLetterUpperCase(StringUtils.toUnderscoreName(str)); } public static String removeMany (String inString, String... keywords) { if (inString == null ) { return null ; } for (String k : keywords) { inString = replace(inString, k, "" ); } return inString; } public static String replace (String inString, String oldPattern, String newPattern) { if (inString == null ) { return null ; } if (oldPattern == null || newPattern == null ) { return inString; } StringBuffer sbuf = new StringBuffer (); int pos = 0 ; int index = inString.indexOf(oldPattern); int patLen = oldPattern.length(); while (index >= 0 ) { sbuf.append(inString.substring(pos, index)); sbuf.append(newPattern); pos = index + patLen; index = inString.indexOf(oldPattern, pos); } sbuf.append(inString.substring(pos)); return sbuf.toString(); } public static String capitalize (String str) { return changeFirstCharacterCase(str, true ); } public static String uncapitalize (String str) { return changeFirstCharacterCase(str, false ); } private static String changeFirstCharacterCase (String str, boolean capitalize) { if (str == null || str.length() == 0 ) { return str; } StringBuffer buf = new StringBuffer (str.length()); if (capitalize) { buf.append(Character.toUpperCase(str.charAt(0 ))); } else { buf.append(Character.toLowerCase(str.charAt(0 ))); } buf.append(str.substring(1 )); return buf.toString(); } private static final Random RANDOM = new Random (); public static String randomNumeric (int count) { return random(count, false , true ); } public static String random (int count, boolean letters, boolean numbers) { return random(count, 0 , 0 , letters, numbers); } public static String random (int count, int start, int end, boolean letters, boolean numbers) { return random(count, start, end, letters, numbers, null , RANDOM); } public static String random (int count, int start, int end, boolean letters, boolean numbers, char [] chars, Random random) { if (count == 0 ) { return "" ; } else if (count < 0 ) { throw new IllegalArgumentException ( "Requested random string length " + count + " is less than 0." ); } if ((start == 0 ) && (end == 0 )) { end = 'z' + 1 ; start = ' ' ; if (!letters && !numbers) { start = 0 ; end = Integer.MAX_VALUE; } } char [] buffer = new char [count]; int gap = end - start; while (count-- != 0 ) { char ch; if (chars == null ) { ch = (char ) (random.nextInt(gap) + start); } else { ch = chars[random.nextInt(gap) + start]; } if ((letters && Character.isLetter(ch)) || (numbers && Character.isDigit(ch)) || (!letters && !numbers)) { if (ch >= 56320 && ch <= 57343 ) { if (count == 0 ) { count++; } else { buffer[count] = ch; count--; buffer[count] = (char ) (55296 + random.nextInt(128 )); } } else if (ch >= 55296 && ch <= 56191 ) { if (count == 0 ) { count++; } else { buffer[count] = (char ) (56320 + random.nextInt(128 )); count--; buffer[count] = ch; } } else if (ch >= 56192 && ch <= 56319 ) { count++; } else { buffer[count] = ch; } } else { count++; } } return new String (buffer); } public static String toUnderscoreName (String name) { if (name == null ) return null ; String filteredName = name; if (filteredName.indexOf("_" ) >= 0 && filteredName.equals(filteredName.toUpperCase())) { filteredName = filteredName.toLowerCase(); } if (filteredName.indexOf("_" ) == -1 && filteredName.equals(filteredName.toUpperCase())) { filteredName = filteredName.toLowerCase(); } StringBuffer result = new StringBuffer (); if (filteredName != null && filteredName.length() > 0 ) { result.append(filteredName.substring(0 , 1 ).toLowerCase()); for (int i = 1 ; i < filteredName.length(); i++) { String preChart = filteredName.substring(i - 1 , i); String c = filteredName.substring(i, i + 1 ); if (c.equals("_" )) { result.append("_" ); continue ; } if (preChart.equals("_" )){ result.append(c.toLowerCase()); continue ; } if (c.matches("\\d" )) { result.append(c); }else if (c.equals(c.toUpperCase())) { result.append("_" ); result.append(c.toLowerCase()); } else { result.append(c); } } } return result.toString(); } public static String removeEndWiths (String inputString,String... endWiths) { for (String endWith : endWiths) { if (inputString.endsWith(endWith)) { return inputString.substring(0 ,inputString.length() - endWith.length()); } } return inputString; } static Pattern three = Pattern.compile("(.*)\\((.*),(.*)\\)" ); static Pattern two = Pattern.compile("(.*)\\((.*)\\)" ); public static boolean substringMatch (CharSequence str, int index, CharSequence substring) { for (int j = 0 ; j < substring.length(); j++) { int i = index + j; if (i >= str.length() || str.charAt(i) != substring.charAt(j)) { return false ; } } return true ; } public static String[] tokenizeToStringArray(String str,String seperators) { if (str == null ) return new String [0 ]; StringTokenizer tokenlizer = new StringTokenizer (str,seperators); List result = new ArrayList (); while (tokenlizer.hasMoreElements()) { Object s = tokenlizer.nextElement(); result.add(s); } return (String[])result.toArray(new String [result.size()]); } public static String join (List list, String seperator) { return join(list.toArray(new Object [0 ]),seperator); } public static String replace (int start, int end, String str,String replacement) { String before = str.substring(0 ,start); String after = str.substring(end); return before + replacement + after; } public static String join (Object[] array, String seperator) { if (array == null ) return null ; StringBuffer result = new StringBuffer (); for (int i = 0 ; i < array.length; i++) { result.append(array[i]); if (i != array.length - 1 ) { result.append(seperator); } } return result.toString(); } public static int containsCount (String string, String keyword) { if (string == null ) return 0 ; int count = 0 ; for (int i = 0 ; i < string.length(); i++ ) { int indexOf = string.indexOf(keyword,i); if (indexOf < 0 ) { break ; } count ++; i = indexOf; } return count; } }
构造数据模型 需求分析 借助FreeMaker
可以方便的根据模板生成文件,对于FreeMaker
而言,其强调数据模型+模板=文件
的思想,所以代码生成器最重要的一个部分就是数据模型
数据模型一共以两种形式组成:
数据库中表、字段等信息
用户自定义数据
为使得代码生成器匹配多样的使用环境,可以让用户自定义数据,并且以key-value的形式配置到propertities
文件中
自定义数据 规定用户自定义数据存放在properties
目录下,构建PropertiesUtils
工具类,统一对properties
文件夹下的所有.properties
文件进行加载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class PropertiesUtils { public static Map<String,String> customMap = new HashMap <>(); static { File dir = new File ("properties" ); try { List<File> files = 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 List<File> searchAllFile (File dir) throws IOException { ArrayList arrayList = new ArrayList (); searchFiles(dir,arrayList); return arrayList; } public static void searchFiles (File dir,List<File> collector) throws IOException { if (dir.isDirectory()) { File[] subFiles = dir.listFiles(); for (int i = 0 ; i < subFiles.length; i++) { searchFiles(subFiles[i],collector); } }else { collector.add(dir); } } }
测试
1 2 3 4 5 6 @Test public void test03 () { PropertiesUtils.customMap.forEach((k,v)->{ System.out.println(k+"->" +v); }); }
元数据处理 将元数据读取到对应数据库表和字段的信息封装到前面定义的Table
和Column
实体类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 public static List<Table> getDbInfo (DataBase db) throws Exception{ Connection conn = getConnection(db); DatabaseMetaData metaData = conn.getMetaData(); List<Table> tableList = new ArrayList <>(); ResultSet tables = metaData.getTables(null , null , null , new String []{"TABLE" }); while (tables.next()){ Table tab = new Table (); String tableName = tables.getString("TABLE_NAME" ); tab.setTableName(tableName); String className = removePrefix(tableName); tab.setPoName(className); String remarks = tables.getString("REMARKS" ); tab.setComment(remarks); ResultSet primaryKeys = metaData.getPrimaryKeys(null , null , tableName); String primaryKesName="" ; while (primaryKeys.next()){ String keyName = primaryKeys.getString("COLUMN_NAME" ); primaryKesName+=keyName+"," ; } tab.setKey(primaryKesName); List<Column> columnList = new ArrayList <>(); ResultSet columns = metaData.getColumns(null , null , tableName, null ); while (columns.next()){ Column column = new Column (); String columnName = columns.getString("COLUMN_NAME" ); column.setColumnName(columnName); String fieldName = StringUtils.toJavaVariableName(columnName); column.setFieldName(fieldName); String columnType = columns.getString("TYPE_NAME" ); column.setColumnType(columnType); String filedType = PropertiesUtils.customMap.get(columnType); column.setFieldType(filedType); String comment = columns.getString("REMARKS" ); column.setColumnComment(comment); String pri = "" ; if (StringUtils.contains(columnName,primaryKesName.split("," ))){ pri = "PRI" ; } column.setColumnKey(pri); columnList.add(column); } tab.setColumnsList(columnList); tableList.add(tab); columns.close(); } tables.close(); conn.close(); return tableList; } public static String removePrefix (String tableName) { String prefix = PropertiesUtils.customMap.get("tableRemovePrefixes" ); String newTableName = tableName; for (String pf:prefix.split("," )){ newTableName = newTableName.replace(pf,"" ); } return StrUtil.upperFirst(newTableName); }
测试代码
1 2 3 4 5 6 7 8 @Test public void test01 () throws Exception { DataBase dataBase = new DataBase ("MYSQL" , "train_member" , "train_member" , "wu123456" , "127.0.0.1" ,"3306" ); List<Table> dbInfo = DataBaseUtils.getDbInfo(dataBase); for (Table table : dbInfo) { System.out.println(table); } }
代码生成器实现 需求分析 用户需要提供的信息
数据库相关信息
工程配置信息
模板路径
模板文件根目录,程序会获取目录下的所有模板文件,集合数据模型生成对应代码文件
输出路径
中间需要生成的数据模型
用户自定义数据对应的数据模型
数据库元数据对应的数据模型
工程配置信息对应的数据模型
数据模型构建类创建 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 @SuppressWarnings("all") public class GeneratorFacade { private HashMap<String, Object> map; private Settings settings; private DataBase db; private Generator generator; public GeneratorFacade (HashMap<String, Object> map, Settings settings, DataBase db) throws Exception { this .map = map; this .settings = settings; this .db = db; this .generator = new Generator (map); } public void generatorByDataBase () throws Exception { List<String> tables = (List<String>) map.get("tables" ); List<Table> allTable = DataBaseUtils.getDbInfo(db); for (int i = 0 ; i < allTable.size(); i++) { for (int j = 0 ; j < tables.size(); j++) { if (tables.get(j).equals(allTable.get(i).getTableName())) { Map<String, Object> dataModel = getDataModel(allTable.get(i)); generator.scanAndGenerator(dataModel); } } } } private Map<String, Object> getDataModel (Table table) { Map<String, Object> dataModel = new HashMap <>(); dataModel.putAll(PropertiesUtils.customMap); dataModel.put("table" , table); dataModel.putAll(this .settings.getSettingMap()); dataModel.put("ClassName" , table.getPoName()); dataModel.put("username" , db.getUserName()); dataModel.put("password" , db.getPassWord()); dataModel.put("url" , db.getUrl()); dataModel.put("driver" , db.getDriver()); dataModel.put("author" , settings.getAuthor()); Date date = new Date (); SimpleDateFormat simpleDateFormat = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); String str = simpleDateFormat.format(date); dataModel.put("createTime" , str); return dataModel; } }
FreeMaker解析输出代码文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 @SuppressWarnings("all") public class Generator { private List<Configuration> cfgs; private HashMap<String, Object> map; private List<String> template; public Generator (HashMap<String, Object> map) throws Exception { this .map = map; cfgs = new ArrayList <>(); template = (List<String>) map.get("template" ); for (int i = 0 ; i < template.size(); i++) { cfgs.add(i, new Configuration ()); FileTemplateLoader ftl = new FileTemplateLoader (new File (template.get(i))); cfgs.get(i).setTemplateLoader(ftl); } } public void scanAndGenerator (Map<String, Object> dataModel) throws Exception { List<String> template = (List<String>) map.get("template" ); for (int i = 0 ; i < template.size(); i++) { List<File> fileList = FileUtils.searchAllFile(new File (template.get(i))); for (File file : fileList) { executeGenerator(dataModel, file, i); } } } private void executeGenerator (Map<String, Object> dataModel, File file, int index) throws Exception { String templateFileName = file.getAbsolutePath().replace(template.get(index), "" ); String outFileName = processTemplateString(templateFileName, dataModel,index); Template template = cfgs.get(index).getTemplate(templateFileName); template.setOutputEncoding("utf-8" ); File file1 = FileUtils.mkdir((String) map.get("templatePath" ), outFileName); FileWriter fw = new FileWriter (file1); template.process(dataModel, fw); fw.close(); } public String processTemplateString (String templateString,Map dataModel,int index) throws Exception { StringWriter out = new StringWriter (); Template template = new Template ("ts" , new StringReader (templateString), cfgs.get(index)); template.process(dataModel,out); return out.toString(); } }
模板文件的编写 重点在于模板文件的编写,根据需求编写不同模板