在我们开发一个新的功能的时候,会根据表创建Entity,Controller,Service,Repository等代码,其中很多步骤都是重复的,并且特别繁琐。这个时候就需要一个代码生成器帮助我们解决这个问题从而提高工作效率,让我们更致力于业务逻辑。
设计原理 在我们安装数据库后会有几个默认的数据库,其中information_schema这个数据库中保存了MySQL服务器所有数据库的信息,如:数据库名、数据库表、表的数据信息与访问权限等。
1 2 information_schema的表tables记录了所有数据库的表的信息 information_schema的表columns记录了所有数据库的表字段详细的信息
我们代码中可以可以通过Sql语句查询出当前数据库中所有表的信息,这里已 eladmin 为例。
1 2 3 # 显示部分数据:表名称、数据库引擎、编码、表备注、创建时间 select table_name ,create_time , engine, table_collation, table_comment from information_schema.tables where table_schema = (select database());
知道表的数据后,可以查询出表字段的详细数据,这里用 job 表为例
sql语句如下:
1 2 3 # 显示部分数据:字段名称、字段类型、字段注释、字段键类型等 select column_name, is_nullable, data_type, column_comment, column_key, extra from information_schema.columns where table_schema = (select database()) and table_name = "job";
有了表字段信息的数据后,通过程序将数据库表字段类型转换成Java语言的字段类型,再通过FreeMarker创建模板,将数据写入到模板,输出成文件即可实现代码生成功能。
代码实现 这里只贴出核心代码,源码可查询文末地址,首先创建一个新的spring boot 项目,选择如下依赖 Maven完整依赖如下
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 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency> <!-- 配置管理工具 --> <dependency> <groupId>commons-configuration</groupId> <artifactId>commons-configuration</artifactId> <version>1.9 </version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true </optional> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies>
项目结构如下
教程开始 修改Spring boot 配置文件 application.yml,如下
1 2 3 4 5 6 7 8 9 10 service: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/eladmin?serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver jpa: show-sql: true
在 resources 目录下创建 Mysql 字段与 Java字段对应关系的配置文件 generator.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 tinyint=Integer smallint=Integer mediumint=Integer int=Integer integer =Integerbigint=Long float =Floatdouble=Double decimal=BigDecimal bit=Boolean char=String varchar=String tinytext=String text=String mediumtext=String longtext=String date =Timestampdatetime=Timestamp timestamp=Timestamp
在 vo 包下创建临时 Vo 类 ColumnInfo,该类的功能用于接收Mysql字段详细信息
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 import lombok.AllArgsConstructor;import lombok.Data;@Data @AllArgsConstructor public class ColumnInfo { private Object columnName; private Object isNullable; private Object columnType; private Object columnComment; private Object columnKey; private Object extra; }
在 util 包下创建字段工具类 ColumnUtil,该类的功能用于转换mysql类型为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 import org.apache.commons.configuration.Configuration;import org.apache.commons.configuration.ConfigurationException;import org.apache.commons.configuration.PropertiesConfiguration;public class ColumnUtil { private static final char SEPARATOR = '_' ; public static PropertiesConfiguration getConfig () { try { return new PropertiesConfiguration ("generator.properties" ); } catch (ConfigurationException e) { e.printStackTrace(); } return null ; } public static String cloToJava (String type) { Configuration config = getConfig(); return config.getString(type,null ); } public static String toCamelCase (String s) { if (s == null ) { return null ; } s = s.toLowerCase(); StringBuilder sb = new StringBuilder (s.length()); boolean upperCase = false ; for (int i = 0 ; i < s.length(); i++) { char c = s.charAt(i); if (c == SEPARATOR) { upperCase = true ; } else if (upperCase) { sb.append(Character.toUpperCase(c)); upperCase = false ; } else { sb.append(c); } } return sb.toString(); } public static String toCapitalizeCamelCase (String s) { if (s == null ) { return null ; } s = toCamelCase(s); return s.substring(0 , 1 ).toUpperCase() + s.substring(1 ); } }
在 util 包下创建代码生成工具类 GeneratorUtil,该类用于将获取到的Mysql字段信息转出Java字段类型,并且获取代码生成的路径,读取 Template,并且输出成文件,代码如下:
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 import freemarker.template.Configuration;import freemarker.template.Template;import freemarker.template.TemplateException;import lombok.extern.slf4j.Slf4j;import org.springframework.util.ObjectUtils;import java.io.*;import java.time.LocalDate;import java.util.*;@Slf4j public class GeneratorUtil { private static final String TIMESTAMP = "Timestamp" ; private static final String BIGDECIMAL = "BigDecimal" ; private static final String PK = "PRI" ; private static final String EXTRA = "auto_increment" ; public static void generatorCode (List<ColumnInfo> columnInfos, String pack, String author, String tableName) throws IOException { Map<String, Object> map = new HashMap <>(); map.put("package" , pack); map.put("author" , author); map.put("date" , LocalDate.now().toString()); map.put("tableName" , tableName); String className = ColumnUtil.toCapitalizeCamelCase(tableName); String changeClassName = ColumnUtil.toCamelCase(tableName); map.put("className" , className); map.put("changeClassName" , changeClassName); map.put("hasTimestamp" , false ); map.put("hasBigDecimal" , false ); map.put("auto" , false ); List<Map<String, Object>> columns = new ArrayList <>(); for (ColumnInfo column : columnInfos) { Map<String, Object> listMap = new HashMap <>(); listMap.put("columnComment" , column.getColumnComment()); listMap.put("columnKey" , column.getColumnKey()); String colType = ColumnUtil.cloToJava(column.getColumnType().toString()); String changeColumnName = ColumnUtil.toCamelCase(column.getColumnName().toString()); if (PK.equals(column.getColumnKey())) { map.put("pkColumnType" , colType); map.put("pkChangeColName" , changeColumnName); } if (TIMESTAMP.equals(colType)) { map.put("hasTimestamp" , true ); } if (BIGDECIMAL.equals(colType)) { map.put("hasBigDecimal" , true ); } if (EXTRA.equals(column.getExtra())) { map.put("auto" , true ); } listMap.put("columnType" , colType); listMap.put("columnName" , column.getColumnName()); listMap.put("isNullable" , column.getIsNullable()); listMap.put("changeColumnName" , changeColumnName); columns.add(listMap); } map.put("columns" , columns); Configuration configuration = new Configuration (Configuration.VERSION_2_3_23); configuration.setClassForTemplateLoading(GeneratorUtil.class, "/template" ); Template template = configuration.getTemplate("Entity.ftl" ); String filePath = getAdminFilePath(pack, className); File file = new File (filePath); genFile(file, template, map); } private static String getAdminFilePath (String pack, String className) { String ProjectPath = System.getProperty("user.dir" ) + File.separator; String packagePath = ProjectPath + File.separator + "src" + File.separator + "main" + File.separator + "java" + File.separator; if (!ObjectUtils.isEmpty(pack)) { packagePath += pack.replace("." , File.separator) + File.separator; } return packagePath + "entity" + File.separator + className + ".java" ; } private static void genFile (File file, Template template, Map<String, Object> params) throws IOException { File parentFile = file.getParentFile(); if (null != parentFile && !parentFile.exists()) { parentFile.mkdirs(); } Writer writer = new BufferedWriter (new OutputStreamWriter (new FileOutputStream (file), "UTF-8" )); try { template.process(params, writer); } catch (TemplateException e) { e.printStackTrace(); } } }
在 resources 的 template 目录下创建 framework 模板 Entity.ftl,代码如下:
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 package NULL.entity;import lombok.Data;import javax.persistence.*;<#if hasTimestamp> import java.sql.Timestamp;</#if > <#if hasBigDecimal> import java.math.BigDecimal;</#if > import java.io.Serializable;@Entity @Data @Table(name="NULL") public class NULL implements Serializable {<#if columns??> <#list columns as column> <#if column.columnComment != '' > </#if > <#if column.columnKey = 'PRI' > @Id <#if auto> @GeneratedValue(strategy = GenerationType.IDENTITY) </#if > </#if > @Column(name = "NULL"<#if column.columnKey = 'UNI'>,unique = true</#if><#if column.isNullable = 'NO' && column.columnKey != 'PRI'>,nullable = false</#if>) private NULL NULL; </#list> </#if > }
创建服务类 GeneratorService,该类用于获取数据库表的源数据
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 import org.springframework.stereotype.Service;import org.springframework.util.ObjectUtils;import javax.persistence.EntityManager;import javax.persistence.PersistenceContext;import javax.persistence.Query;import java.io.IOException;import java.util.ArrayList;import java.util.List;@Service public class GeneratorService { @PersistenceContext private EntityManager em; public List<ColumnInfo> getColumns (String tableName) { StringBuilder sql = new StringBuilder ("select column_name, is_nullable, data_type, column_comment, column_key, extra from information_schema.columns where " ); if (!ObjectUtils.isEmpty(tableName)){ sql.append("table_name = '" ).append(tableName).append("' " ); } sql.append("and table_schema = (select database()) order by ordinal_position" ); Query query = em.createNativeQuery(sql.toString()); List result = query.getResultList(); List<ColumnInfo> columnInfos = new ArrayList <>(); for (Object o : result) { Object[] obj = (Object[])o; columnInfos.add(new ColumnInfo (obj[0 ],obj[1 ],obj[2 ],obj[3 ],obj[4 ],obj[5 ])); } return columnInfos; } }
由于没有前端页面,所以只能在测试类中演示代码生成功能,GeneratorDomeApplicationTests 修改如下
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 import com.ydyno.util.GeneratorUtil;import com.ydyno.vo.ColumnInfo;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import java.io.IOException;import java.util.List;@SpringBootTest class GeneratorDomeApplicationTests { @Autowired private GeneratorService generatorService; @Test void genTest () throws IOException { String tableName = "job" ; String pack = "com.ydyno" ; String author = "Zheng Jie" ; List<ColumnInfo> columnInfos = generatorService.getColumns(tableName); GeneratorUtil.generatorCode(columnInfos,pack,author,tableName); } }
执行后,查看创建好的Entity