Java代码生成器简介、原理、开发流程和Demo

Java代码生成器简介、原理、开发流程和Demo

简介和背景

刚开始学习的时候,跟着老师写项目,一些实体类就是直接代码生成器生成的,当时就感觉好神奇,后来写毕设的时候,代码生成器又帮了我大忙,我毕设用的是mybatis plus,也有代码生成功能,直接把整体代码框架全生成了,省了很多事情,但是当时的想法就是认为代码生成器很牛,也没有研究到底咋生成的,现在工作了,组长抛给我一个任务,让我单独开发个服务,代码生成器,磨刀不误砍柴工,可以有效提高开发效率,所以就着手研究起来了。

代码生成器,我这里生成的是什么代码呢?是通过输入数据库表名,生成对应的实体类和mapper、xml和service及实现类代码,并且因为这个是单独拿出来的一个服务,所以需要适配多个项目,也就是还需要输入一个项目名,来生成对应项目目录结构的代码。这里就是简单介绍下代码生成器到底生成了啥代码,是根据什么生成的。注:我这里代码生成器接口是返回的一个符合项目目录结构的一个压缩包。

原理

在这里说一下代码生成器的原理,其实就是利用模板引擎,咱们自己写好代码的模板,然后把表信息和列信息从数据库里面查出来,然后渲染到模板里面,进而生成具体的代码文件。

啥是模板引擎呢?就是可以把数据装到模板里面的一个东西,如果学过jsp的话,也可以类比为jsp,里面有EL表达式取值,然后页面有一个整体的模板,变的只是那几个数据。

我这里用到的模板引擎是apache的velocity

开发流程

首先要知道我们的一个大体流程:

  1. 根据表名,从数据库中查询表信息,和表中的每一列的信息(列名,列备注,列类型,是否主键等等);
  2. 编写模板文件,自己想生成啥样子的代码文件就写成啥样的模板就行,这里可能会需要简单学习一下模板引擎的语法,控制语句和循环等等;
  3. 将数据渲染到模板中;
  4. 将渲染完成后的数据写入文件返回。

项目还有一个配置文件,就是描述生成类文件上面的导包路径,还有就是数据库类型和Java类型的对应关系,这个需要自己配置,下面我给一个我的参考配置:

# 类上面的注释信息
author=hc
# 项目包结构主路径
mainPath=com.hc
# 下面的子包路径
entityPackage=entity.po
mapperPackage=mapper.def
servicePackage=service
serviceImplPackage=service.impl
# 文件后缀名,这个也单独拿出来吧,或者写到常量类中都可以
generatorEntityPostfix=Po.java
generatorMapperPostfix=Mapper.java
generatorXmlPostFix=Mapper.xml
generatorServicePostFix=Service.java
generatorServiceImplPostFix=ServiceImpl.java
# xml路径
xmlPath=main/resources/mybatis/mapper/def/common
# 表的统一前缀,因为表一般都是t_开头,我们根据下划线转驼峰的时候总不能把这个没有意义的t_也转换了吧,所以配置一个统一前缀来删去,这个根据不同的表命名风格都有不同的前缀,按需配置
tablePrefix=t_
# oracle字段对应关系
FLOAT=Float
NUMBER=Integer
LONG=Long
NVARCHAR2=String
NCHAR=String
CHAR=String
VARCHAR2=String
CLOB=String
TIMESTAMP=Date
DATE=Date
# mysql字段对应关系
tinyint=Integer
smallint=Integer
mediumint=Integer
int=Integer
integer=Integer
bigint=Long
float=Float
double=Double
decimal=BigDecimal
bit=Boolean
char=String
varchar=String
tinytext=String
text=String
mediumtext=String
longtext=String
date=Date
datetime=Date
timestamp=Date

知道这个流程,大体上代码生成的原理也就都知道了,talk is cheap,现在直接上代码!

Demo

第一步肯定是导入依赖了,首先建一个空的springboot项目,然后导入以下依赖:

<!-- lombok -->
<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<optional>true</optional>
</dependency>

<!--hutool-->
<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.4.7</version>
</dependency>

<!-- 引入velocity模板引擎 -->
<dependency>
	<artifactId>velocity</artifactId>
	<groupId>org.apache.velocity</groupId>
	<version>1.7</version>
</dependency>

<!-- swagger ui -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.8.0</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.8.0</version>
</dependency>

然后下面就根据流程来!

  1. 根据表名查表信息、列信息

    首先我们需要能根据表名查出表信息,因为公司用的数据库是Oracle,所以我贴一下Oracle的查询语句,Oracle查这个比较繁琐,相比较MySQL就简单多了,这个大家可以自行查询,大家可以点击链接查看Oracle的查询语句。

    https://blog.csdn.net/hsunnyc/article/details/118306314?spm=1001.2014.3001.5501

  2. 编写模板文件,下面我展示一个实体类的模板文件,公司用了tkmapper和swagger,所以加了这两个工具需要的注解;里面的${}就是取值,if和循环的逻辑也很简单,仔细看下都能看懂。

    package ${mainPath}.${entityPackage};
    
    #if(${hasDate})
    import java.util.Date;
    #end
    import lombok.Data;
    import javax.persistence.Id;
    import javax.persistence.Column;
    import javax.persistence.Table;
    import java.io.Serializable;
    import io.swagger.annotations.ApiModel;
    import io.swagger.annotations.ApiModelProperty;
    
    /**
     * $!{table.comments} - 实体类
     * @Author: ${author}
     * @CreateTime:  ${date}
     * @Description: $!{table.comments} - 实体类
     */
    @Table( name ="${table.tableName}" )
    @Data
    @ApiModel(value = "$!{table.comments}")
    public class ${table.className}Po implements Serializable {
    
        private static final long serialVersionUID = 1L;
    #foreach ($column in $columns)
    
    #if($column.columnName == $table.primaryKey)
        @Id
    #end
        @Column(name = "$column.columnName" )
        @ApiModelProperty( value="$!column.comments")
        private $column.attrType $column.attrName;
    #end
    
    }
    
  3. 将数据渲染到模板中;我们要先获取表结构数据,我把返回的数据封装了两个po中,一个是表信息po,一个是列信息po,具体代码如下:

    // 表信息实体类
    @Data
    @ToString
    public class TablePo {
        /**
         * 表名
         */
        private String tableName;
        /**
         * 表备注
         */
        private String comments;
        /**
         * 主键名
         */
        private String primaryKey;
    
        /**
         * 类名(第一个字母大写),如:sys_user => SysUser
         */
        private String className;
    }
    
    // 列信息实体类
    @Data
    @ToString
    public class ColumnPo {
    
        /**
         * 列名
         */
        private String columnName;
        /**
         * 列名类型
         */
        private String dataType;
        /**
         * 列名备注
         */
        private String comments;
    
        /**
         * 属性名称(第一个字母小写),如:user_name => userName
         */
        private String attrName;
        /**
         * 属性类型
         */
        private String attrType;
    }
    

    封装的时候就是把实体类中的信息放到模板引擎的对象中,具体代码如下:

        /**
         * 根据表信息和列信息生成代码
         * @param table 表信息
         * @param columns 列信息
         * @param zip zip
         * @param projectName 项目名
         */
        public static void generatorCode(TablePo table, List<ColumnPo> columns, ZipOutputStream zip, String projectName) {
            // 获取配置信息
            Configuration config = getConfig();
            // 处理日期类型导包问题
            boolean hasDate = false;
            // 项目名为空会使用默认配置,项目名不为空会使用项目的特定配置
            projectName = StringUtils.isNotEmpty(projectName) ? projectName + StrUtil.DOT : "";
    
            // 表名处理
            String className = tableToJava(table.getTableName(), config.getString("tablePrefix"));
            table.setClassName(className);
    
            // 列信息处理
            for (ColumnPo columnPo : columns) {
                String attrName = columnToJava(columnPo.getColumnName());
                columnPo.setAttrName(StringUtils.uncapitalize(attrName));
                String attrType = config.getString(columnPo.getDataType(), "unKnowType");
                columnPo.setAttrType(attrType);
                if (!hasDate && "Date".equals(attrType)) {
                    hasDate = true;
                }
            }
    
            // 设置velocity资源加载器
            Properties prop = new Properties();
            prop.put("file.resource.loader.class", ClasspathResourceLoader.class.getName());
            Velocity.init(prop);
    
            // 封装模板数据
            VelocityContext context = new VelocityContext();
            context.put("table",table);
            context.put("columns", columns);
            context.put("hasDate", hasDate);
            context.put("entityPackage", getPropertyOrDefault(config, "entityPackage", projectName));
            context.put("mapperPackage", getPropertyOrDefault(config, "mapperPackage", projectName));
            context.put("servicePackage", getPropertyOrDefault(config, "servicePackage", projectName));
            context.put("serviceImplPackage", getPropertyOrDefault(config, "serviceImplPackage", projectName));
            context.put("mainPath", getPropertyOrDefault(config, "mainPath", projectName));
            context.put("author", config.getString("author"));
            context.put("date", DateUtil.now());
            // 模板渲染
            List<String> templates = getTemplates();
            for (String temp : templates) {
                Template template = Velocity.getTemplate(temp, StandardCharsets.UTF_8.name());
                StringWriter sw = new StringWriter();
                template.merge(context, sw);
                try {
                    zip.putNextEntry(new ZipEntry( StringUtils.lowerCase(table.getTableName()) + File.separator + getFileName(temp, className, config, projectName)));
                    IOUtils.write(sw.toString(), zip, StandardCharsets.UTF_8.name());
                    IOUtils.closeQuietly(sw);
                    zip.closeEntry();
                } catch (IOException e) {
                    throw new BaseException("解析表[" + table.getTableName() + "]" + "出现异常"  + e.getMessage());
                } catch (NullPointerException e) {
                    throw new BaseException("未获取到项目[" + projectName.replace(StrUtil.DOT, "") + "]" + "的配置信息,请检查输入是否正确!" + e.getMessage());
                }
            }
        }
    

    下面我解释下这个方法主要干啥了:

    1. 先是获取配置信息(主要为了从配置信息中获取我们的作者信息,包路径等等信息),然后下面是处理日期类型导包问题,因为日期类型是需要单独导包的,为了生成的java文件不报错,所以需要判断以下列信息中有没有日期类型;

      获取配置信息函数,注意:properties文件要放在resource下

      /**
       * 获取配置信息
       * @return 配置信息
       */
      private static Configuration getConfig(){
          try {
              return new PropertiesConfiguration(ProjectConstant.GENERATOR_PROPERTIES);
          } catch (ConfigurationException e) {
              throw new BaseException("获取配置文件失败:" + e.getMessage());
          }
      }
      
    2. 表名转换,去掉统一前缀,下划线命名转驼峰,具体转换函数是tableToJava,下面会把这个方法贴出来

    3. 列名转换,给列实体添加列类型,列名下划线转驼峰,转换函数是columnToJava,以上两个下划线转驼峰是如下处理

      /**
       * 列名转换成Java属性名
       * @param columnName 列名
       * @return 属性名
       */
      private static String columnToJava(String columnName) {
          return WordUtils.capitalizeFully(columnName, new char[]{ProjectConstant.UNDER_LINE.charAt(0)})
                  .replace(ProjectConstant.UNDER_LINE, "");
      }
      
      /**
       * 表名转换成Java类名
       * @param tableName 表名
       * @param tablePrefix 统一删除的表前缀
       * @return java类名
       */
      private static String tableToJava(String tableName, String tablePrefix) {
          if(StringUtils.isNotBlank(tablePrefix)){
              tableName = tableName.replace(tablePrefix, "");
          }
          return columnToJava(tableName);
      }
      
    4. 封装数据信息到模板引擎对象中,其实就是放键值对,然后在模板中使用${}这种方式取值

      // 就是new一个这个对象,然后往context中put数据
      VelocityContext context = new VelocityContext();
      
    5. 下面就是渲染模板文件了,就是我们写的entityPo.java.vm文件

      模板文件都创建在resources下的template文件夹下;

      这个是模板文件的List:

      /**
       * 获取所有模板文件
       * @return 模板文件list
       */
      private static List<String> getTemplates(){
          List<String> templates = new ArrayList<>(5);
          templates.add("template/EntityPo.java.vm");
          templates.add("template/Mapper.java.vm");
          templates.add("template/Mapper.xml.vm");
          templates.add("template/Service.java.vm");
          templates.add("template/ServiceImpl.java.vm");
          return templates;
      }
      
    6. 渲染之后打包进zip中,算是完成一次文件生成了!

    7. 最后可以写一个接口,传一个表名,然后执行查询信息的语句,执行工具类代码,就可以获取生成的文件了!

  • 5
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值