Java代码生成器简介、原理、开发流程和Demo
Java代码生成器简介、原理、开发流程和Demo简介和背景刚开始学习的时候,跟着老师写项目,一些实体类就是直接代码生成器生成的,当时就感觉好神奇,后来写毕设的时候,代码生成器又帮了我大忙,我毕设用的是mybatisplus,也有代码生成功能,直接把整体代码框架全生成了,省了很多事情,但是当时的想法就是认为代码生成器很牛,也没有研究到底咋生成的,现在工作了,组长抛给我一个任务,让我单独开发个服务,代码生成器,磨刀不误砍柴工,可以有效提高开发效率,所以就着手研究起来了。
代码生成器,我这里生成的是什么代码呢?是通过输入数据库表名,生成对应的实体类和mapper、xml和service及实现类代码,并且因为这个是单独拿出来的一个服务,所以需要适配多个项目,也就是还需要输入一个项目名,来生成对应项目目录结构的代码。这里就是简单介绍下代码生成器到底生成了啥代码,是根据什么生成的。注:我这里代码生成器接口是返回的一个符合项目目录结构的一个压缩包。
原理在这里说一下代码生成器的原理,其实就是利用模板引擎,咱们自己写好代码的模板,然后把表信息和列信息从数据库里面查出来,然后渲染到模板里面,进而生成具体的代码文件。
啥是模板引擎呢?就是可以把数据装到模板里面的一个东西,如果学过jsp的话,也可以类比为jsp,里面有EL表达式取值,然后页面有一个整体的模板,变的只是那几个数据。
我这里用到的模板引擎是apache的velocity
开发流程首先要知道我们的一个大体流程:
根据表名,从数据库中查询表信息,和表中的每一列的信息(列名,列备注,列类型,是否主键等等);编写模板文件,自己想生成啥样子的代码文件就写成啥样的模板就行,这里可能会需要简单学习一下模板引擎的语法,控制语句和循环等等;将数据渲染到模板中;将渲染完成后的数据写入文件返回。项目还有一个配置文件,就是描述生成类文件上面的导包路径,还有就是数据库类型和Java类型的对应关系,这个需要自己配置,下面我给一个我的参考配置:
#类上面的注释信息author=hc#项目包结构主路径mainPath=com.hc#下面的子包路径entityPackage=entity.pomapperPackage=mapper.defservicePackage=serviceserviceImplPackage=service.impl#文件后缀名,这个也单独拿出来吧,或者写到常量类中都可以generatorEntityPostfix=Po.javageneratorMapperPostfix=Mapper.javageneratorXmlPostFix=Mapper.xmlgeneratorServicePostFix=Service.javageneratorServiceImplPostFix=ServiceImpl.java#xml路径xmlPath=main/resources/mybatis/mapper/def/common#表的统一前缀,因为表一般都是t_开头,我们根据下划线转驼峰的时候总不能把这个没有意义的t_也转换了吧,所以配置一个统一前缀来删去,这个根据不同的表命名风格都有不同的前缀,按需配置tablePrefix=t_#oracle字段对应关系FLOAT=FloatNUMBER=IntegerLONG=LongNVARCHAR2=StringNCHAR=StringCHAR=StringVARCHAR2=StringCLOB=StringTIMESTAMP=DateDATE=Date#mysql字段对应关系tinyint=Integersmallint=Integermediumint=Integerint=Integerinteger=Integerbigint=Longfloat=Floatdouble=Doubledecimal=BigDecimalbit=Booleanchar=Stringvarchar=Stringtinytext=Stringtext=Stringmediumtext=Stringlongtext=Stringdate=Datedatetime=Datetimestamp=Date知道这个流程,大体上代码生成的原理也就都知道了,talkischeap,现在直接上代码!
Demo第一步肯定是导入依赖了,首先建一个空的springboot项目,然后导入以下依赖:
org.projectlomboklomboktruecn.hutoolhutool-all5.4.7velocityorg.apache.velocity1.7io.springfoxspringfox-swagger22.8.0io.springfoxspringfox-swagger-ui2.8.0然后下面就根据流程来!
根据表名查表信息、列信息
首先我们需要能根据表名查出表信息,因为公司用的数据库是Oracle,所以我贴一下Oracle的查询语句,Oracle查这个比较繁琐,相比较MySQL就简单多了,这个大家可以自行查询,大家可以点击链接查看Oracle的查询语句。
https://blog.csdn.net/hsunnyc/article/details/118306314?spm=1001.2014.3001.5501
编写模板文件,下面我展示一个实体类的模板文件,公司用了tkmapper和swagger,所以加了这两个工具需要的注解;里面的${}就是取值,if和循环的逻辑也很简单,仔细看下都能看懂。
package${mainPath}.${entityPackage};#if(${hasDate})importjava.util.Date;#endimportlombok.Data;importjavax.persistence.Id;importjavax.persistence.Column;importjavax.persistence.Table;importjava.io.Serializable;importio.swagger.annotations.ApiModel;importio.swagger.annotations.ApiModelProperty;/***$!{table.comments}-实体类*@Author:${author}*@CreateTime:${date}*@Description:$!{table.comments}-实体类*/@Table(name="${table.tableName}")@Data@ApiModel(value="$!{table.comments}")publicclass${table.className}PoimplementsSerializable{privatestaticfinallongserialVersionUID=1L;#foreach($columnin$columns)#if($column.columnName==$table.primaryKey)@Id#end@Column(name="$column.columnName")@ApiModelProperty(value="$!column.comments")private$column.attrType$column.attrName;#end}将数据渲染到模板中;我们要先获取表结构数据,我把返回的数据封装了两个po中,一个是表信息po,一个是列信息po,具体代码如下:
//表信息实体类@Data@ToStringpublicclassTablePo{/***表名*/privateStringtableName;/***表备注*/privateStringcomments;/***主键名*/privateStringprimaryKey;/***类名(第一个字母大写),如:sys_user=>SysUser*/privateStringclassName;}//列信息实体类@Data@ToStringpublicclassColumnPo{/***列名*/privateStringcolumnName;/***列名类型*/privateStringdataType;/***列名备注*/privateStringcomments;/***属性名称(第一个字母小写),如:user_name=>userName*/privateStringattrName;/***属性类型*/privateStringattrType;}封装的时候就是把实体类中的信息放到模板引擎的对象中,具体代码如下:
/***根据表信息和列信息生成代码*@paramtable表信息*@paramcolumns列信息*@paramzipzip*@paramprojectName项目名*/publicstaticvoidgeneratorCode(TablePotable,Listcolumns,ZipOutputStreamzip,StringprojectName){//获取配置信息Configurationconfig=getConfig();//处理日期类型导包问题booleanhasDate=false;//项目名为空会使用默认配置,项目名不为空会使用项目的特定配置projectName=StringUtils.isNotEmpty(projectName)?projectName+StrUtil.DOT:"";//表名处理StringclassName=tableToJava(table.getTableName(),config.getString("tablePrefix"));table.setClassName(className);//列信息处理for(ColumnPocolumnPo:columns){StringattrName=columnToJava(columnPo.getColumnName());columnPo.setAttrName(StringUtils.uncapitalize(attrName));StringattrType=config.getString(columnPo.getDataType(),"unKnowType");columnPo.setAttrType(attrType);if(!hasDate&&"Date".equals(attrType)){hasDate=true;}}//设置velocity资源加载器Propertiesprop=newProperties();prop.put("file.resource.loader.class",ClasspathResourceLoader.class.getName());Velocity.init(prop);//封装模板数据VelocityContextcontext=newVelocityContext();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());//模板渲染Listtemplates=getTemplates();for(Stringtemp:templates){Templatetemplate=Velocity.getTemplate(temp,StandardCharsets.UTF_8.name());StringWritersw=newStringWriter();template.merge(context,sw);try{zip.putNextEntry(newZipEntry(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(IOExceptione){thrownewBaseException("解析表["+table.getTableName()+"]"+"出现异常"+e.getMessage());}catch(NullPointerExceptione){thrownewBaseException("未获取到项目["+projectName.replace(StrUtil.DOT,"")+"]"+"的配置信息,请检查输入是否正确!"+e.getMessage());}}}下面我解释下这个方法主要干啥了:
先是获取配置信息(主要为了从配置信息中获取我们的作者信息,包路径等等信息),然后下面是处理日期类型导包问题,因为日期类型是需要单独导包的,为了生成的java文件不报错,所以需要判断以下列信息中有没有日期类型;
获取配置信息函数,注意:properties文件要放在resource下
/***获取配置信息*@return配置信息*/privatestaticConfigurationgetConfig(){try{returnnewPropertiesConfiguration(ProjectConstant.GENERATOR_PROPERTIES);}catch(ConfigurationExceptione){thrownewBaseException("获取配置文件失败:"+e.getMessage());}}表名转换,去掉统一前缀,下划线命名转驼峰,具体转换函数是tableToJava,下面会把这个方法贴出来
列名转换,给列实体添加列类型,列名下划线转驼峰,转换函数是columnToJava,以上两个下划线转驼峰是如下处理
/***列名转换成Java属性名*@paramcolumnName列名*@return属性名*/privatestaticStringcolumnToJava(StringcolumnName){returnWordUtils.capitalizeFully(columnName,newchar[]{ProjectConstant.UNDER_LINE.charAt(0)}).replace(ProjectConstant.UNDER_LINE,"");}/***表名转换成Java类名*@paramtableName表名*@paramtablePrefix统一删除的表前缀*@returnjava类名*/privatestaticStringtableToJava(StringtableName,StringtablePrefix){if(StringUtils.isNotBlank(tablePrefix)){tableName=tableName.replace(tablePrefix,"");}returncolumnToJava(tableName);}封装数据信息到模板引擎对象中,其实就是放键值对,然后在模板中使用${}这种方式取值
//就是new一个这个对象,然后往context中put数据VelocityContextcontext=newVelocityContext();下面就是渲染模板文件了,就是我们写的entityPo.java.vm文件
模板文件都创建在resources下的template文件夹下;
这个是模板文件的List:
/***获取所有模板文件*@return模板文件list*/privatestaticListgetTemplates(){Listtemplates=newArrayList(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");returntemplates;}渲染之后打包进zip中,算是完成一次文件生成了!
最后可以写一个接口,传一个表名,然后执行查询信息的语句,执行工具类代码,就可以获取生成的文件了!