基于SpringBoot的代码自动生成
条目 | 条目说明 |
---|---|
作者 | 田乐乐 |
版本号 | v1.0 |
版本描述 | 新增 |
目录
文章目录
- 基于SpringBoot的代码自动生成
-
- 目录
- 使用技术
- 一、背景
- 二、需求
- 三、优势
-
- 3.1、规范开发
- 3.2、提高开发效率
- 四、设计思路
-
- 4.1、数据库连接
- 4.2、多数据源问题
-
- 4.2.1、基本信息配置
- 4.2.2、选择配置
- 4.3、逆向工程配置
-
- 4.3.1、逆向实体类
- 4.3.2、逆向规则配置
- 4.3.3实体类信息装入模板引擎
- 五、总结
- 六、扩展
-
- 6.1表数量扩展
-
- 双表操作
- 多表操作
- 6.2测试扩展
- 6.3前端页面扩展
-
- 6.3.1前端页面扩展一
- 6.3.2前端页面扩展二
使用技术
框架:SpringBoot、Mybatis/Mybatis-plus
其他:lombok、druid、velocity
一、背景
1、项目中多人开发,使用JDK1.6,IDE编译使用JDK1.5。一人使用1.6在项目上添加注解,部分使用1.5编译报错。
2、框架搭建,使用简单工厂创建Service对象。开发项目代码中存在不使用该框架单独创建对象操作。
3、DAO实现类中,对数据库表进行CRUD,使用Date替换HiveQL中相应的占位符,使用SimpleDateFormat对时间进行parse()操作,一定概率下会导致并发问题。
当微服务项目启动,使用JDK1.8解决了上述部分问题,但是有没有能够类似想方正平台简化部分开发呢?
二、需求
基于以上背景,现需要一套解决方案从而规范开发,提高开发效率。
实现目标:spring/springboot/springCloud项目代码自动生成。
三、优势
3.1、规范开发
代码自动生成带来的第一个好处就是开发规范,本身因为考虑是否是编译错误引起的不必要麻烦。
另外,使用JDK1.8,也算是从刚开始就解决了一些并发问题。比如在JDK1.8中java.time包新增一些类就对此种并发问题进行了解决——新日期时间API。
3.2、提高开发效率
3.2.1不再需要进行繁琐的手动逆向工程
虽然使用插件可以手动进行逆向工程(数据库表、字段生成对应的POJO、mapper;生成对应的DAO、Service及其实现、生成规则配置等),比如EasyCode。但是如果使用自定义代码,生成更多对应产物,可以更加方便的完成开发,提高开发效率,比如生成单元测试的json文件(为满足所有DAO、Service单元测试100%的覆盖率和单元测试单元测试本身语句覆盖率的70%使用)、快速开发产品化需要新增的SQL(为满足产品化新增页面)等等。
四、设计思路
4.1、数据库连接
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHLzsGVNFTT650dJpHW4Z0MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLwATN4QDM0ATMyITMxkTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
pom.xml部分内容如下(依赖未做删减):
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
<druid.version>1.0.28</druid.version>
<commons.lang.version>2.6</commons.lang.version>
<commons.io.version>2.5</commons.io.version>
<commons.configuration.version>1.10</commons.configuration.version>
<fastjson.version>1.2.45</fastjson.version>
<velocity.version>1.7</velocity.version>
<pagehelper.spring.boot.version>1.2.5</pagehelper.spring.boot.version>
<mysql.version>5.1.38</mysql.version>
<mssql.version>4.0</mssql.version>
<oracle.version>11.2.0.3</oracle.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>${commons.lang.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons.io.version}</version>
</dependency>
<dependency>
<groupId>commons-configuration</groupId>
<artifactId>commons-configuration</artifactId>
<version>${commons.configuration.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<artifactId>velocity</artifactId>
<groupId>org.apache.velocity</groupId>
<version>${velocity.version}</version>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>${pagehelper.spring.boot.version}</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- oracle驱动 -->
<dependency>
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>${oracle.version}</version>
</dependency>
<!-- sqlserver驱动 -->
<dependency>
<groupId>com.microsoft.sqlserver</groupId>
<artifactId>sqljdbc4</artifactId>
<version>${mssql.version}</version>
</dependency>
<!-- postgresql驱动 -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
4.2、多数据源问题
所有逆向工程需要需要解决多数据源连接问题
比如:Oracle、MySQL
在yml配置如下:
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
#MySQL配置
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/dbname?useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: root
#oracle配置
# driverClassName: oracle.jdbc.OracleDriver
# url: jdbc:oracle:thin:@172.168.1.5:1521:orcl
# username: prtdev
# password: prtdevpw
#SQLServer配置
# driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
# url: jdbc:sqlserver://192.168.10.10:1433;DatabaseName=dbname
# username: sa
# password: 123456
#PostgreSQL配置
# driverClassName: org.postgresql.Driver
# url: jdbc:postgresql://192.168.10.10:5432/
# username: postgres
# password: 123456
#指定数据库,可选值有【mysql、oracle、sqlserver、postgresql】
tianll:
database: mysql
说明:首先在application.yml中进行数据库配置,不使用yml配置选择properties进行配置效果想同。
4.2.1、基本信息配置
spring:datasource:下进行多数据源的配置,正在使用的数据源需要解除注解并提到所有数据源配置最上方,否则SpringBoot项目启动失败。
4.2.2、选择配置
tianll:database:下可选择不同数据库配置,可自行配置,在引用的时候标记处使用哪一种数据源,方便采用不同的连接工具。如下:
在config包中DBConfig中使用变量接收yml配置信息、选择不同数据源、使用不同的数据源DAO连接。(在代码中用注解记性描述)
@Configuration
public class DbConfig {
/**
*database 用于接收yml改标签的配置
*/
@Value("${tianll.database: mysql}")
private String database;
/**
* 使用不同的数据源DAO进行选择
* 所有的具体GeneratorDao都继承GeneratorDao,
*使用不同的sql脚本配置实现对所有表结构信息的查询
*/
@Bean
@Primary
public GeneratorDao getGeneratorDao(){
if("mysql".equalsIgnoreCase(database)){
return mySQLGeneratorDao;
}else if("oracle".equalsIgnoreCase(database)){
return oracleGeneratorDao;
}else if("sqlserver".equalsIgnoreCase(database)){
return sqlServerGeneratorDao;
}else if("postgresql".equalsIgnoreCase(database)){
return postgreSQLGeneratorDao;
}else {
throw new RRException("不支持当前数据库:" + database);
}
}
mysql对表结构信息查询如下(mySQLGeneratorDao对应的mySQLGeneratorDao.xml):
<mapper namespace="对应包名+类名">
<select id="queryList" resultType="map">
select table_name tableName, engine, table_comment tableComment, create_time createTime from information_schema.tables
where table_schema = (select database())
<if test="tableName != null and tableName.trim() != ''">
and table_name like concat('%', #{tableName}, '%')
</if>
order by create_time desc
</select>
<select id="queryTable" resultType="map">
select table_name tableName, engine, table_comment tableComment, create_time createTime from information_schema.tables
where table_schema = (select database()) and table_name = #{tableName}
</select>
<select id="queryColumns" resultType="map">
select column_name columnName, data_type dataType, column_comment columnComment, column_key columnKey, extra from information_schema.columns
where table_name = #{tableName} and table_schema = (select database()) order by ordinal_position
</select>
</mapper>
4.3、逆向工程配置
4.2中已经通过SQL获取到所有的表信息和表的列信息,此时需要使用实体列进行包装。
4.3.1、逆向实体类
定义两个entity,一个表实体类Table,一个栏位实体类Column.
@Data
public class Table {
/**
* 表的名称
*/
private String tableName;
/**
* 表的备注
*/
private String comments;
/**
* 表的主键
*/
private ColumnEntity pk;
/**
* 表的列名(不包含主键)
*/
private List<ColumnEntity> columns;
/**
* 类名(第一个字母大写),如:sys_user => SysUser
*/
private String className;
/**
* 类名(第一个字母小写),如:sys_user => sysUser
*/
private String classname;
}
@Data
public class Column {
/**
* 列名
*/
private String columnName;
/**
* 列名类型
*/
private String dataType;
/**
* 列名备注
*/
private String comments;
/**
* 属性名称(第一个字母大写),如:user_name => UserName
*/
private String attrName;
/**
* 属性名称(第一个字母小写),如:user_name => userName
*/
private String attrname;
/**
* 属性类型
*/
private String attrType;
/**
* auto_increment
*/
private String extra;
}
4.3.2、逆向规则配置
自己创建一个properties进行规则配置,主要负责数据库表栏位类型逆向成实体类对应的属性的类型。
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
....=....
因为篇幅和时间精力问题,这里就不在一一列举了。
4.3.3实体类信息装入模板引擎
一切准备工作都已经完成,最后使用模板生成需要的代码
//templates为获取本地Resource下所有VM模板
List<String> templates = getTemplates();
for (String template : templates) {
//渲染模板
StringWriter sw = new StringWriter();
Template tpl = Velocity.getTemplate(template, "UTF-8" );
tpl.merge(context, sw);
}
vm模板举例如下(contorller):
package ${package}.${moduleName}.controller;
import java.util.Arrays;
import java.util.Map;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import ${package}.${moduleName}.entity.${className}Entity;
import ${package}.${moduleName}.service.${className}Service;
import ${mainPath}.common.utils.PageUtils;
import ${mainPath}.common.utils.R;
/**
* ${comments}
*
* @author ${author}
* @email ${email}
* @date ${datetime}
*/
@RestController
@RequestMapping("${moduleName}/${pathName}")
public class ${className}Controller {
@Autowired
private ${className}Service ${classname}Service;
/**
* 列表
*/
@RequestMapping("/list")
@RequiresPermissions("${moduleName}:${pathName}:list")
public Result list(@RequestParam Map<String, Object> params){
PageUtils page = ${classname}Service.queryPage(params);
return Result.ok().put("page", page);
}
/**
* 信息
*/
@RequestMapping("/info/{${pk.attrname}}")
@RequiresPermissions("${moduleName}:${pathName}:info")
public Result info(@PathVariable("${pk.attrname}") ${pk.attrType} ${pk.attrname}){
${className}Entity ${classname} = ${classname}Service.getById(${pk.attrname});
return Result.ok().put("${classname}", ${classname});
}
/**
* 保存
*/
@RequestMapping("/save")
@RequiresPermissions("${moduleName}:${pathName}:save")
public Result save(@RequestBody ${className}Entity ${classname}){
${classname}Service.save(${classname});
return Result.ok();
}
/**
* 修改
*/
@RequestMapping("/update")
@RequiresPermissions("${moduleName}:${pathName}:update")
public Result update(@RequestBody ${className}Entity ${classname}){
${classname}Service.updateById(${classname});
return Result.ok();
}
/**
* 删除
*/
@RequestMapping("/delete")
@RequiresPermissions("${moduleName}:${pathName}:delete")
public Result delete(@RequestBody ${pk.attrType}[] ${pk.attrname}s){
${classname}Service.removeByIds(Arrays.asList(${pk.attrname}s));
return Result.ok();
}
}
其中Result为自定义的关于REST风格的使用 Result 方式的返回(封装“错误码”、“错误信息”、数据总数、数据T或者T列表)。
五、总结
从第四章的讲述中可以看出,如果使用此方法。所有的单表需求已经完全可以用代码生成完成。单表的情况下,完全可以做到:
1、可移植性。针对不同版本不同框架,仅仅需要添加不同框架所需要的vm模板,做到一次添加到处使用。
2、目标多元性——满足不同客户群体数据库需求。针对客户数据库选择不同数据源进行对应数据逆向和SQL定制开发(完全写在vm中)。
3、目标多元性——满足不同群体框架使用需求。只要VM包含,可以使用不同的框架选择、比如spring或者springboot,比如mybatis、hibernate或者SpringJPA。
当然,如在JDK版本不同的情况下,比如模板生成中都是JDK1.8新特性代码,放到1.6的项目中肯定是不行的。使用1.6代码的模板即可解决。
综上,建议如下:
模板文件命名建议如下:
项目名.功能名.文件类型.vm
因为从项目名即可判断使用JDK版本,所以不再需要重复表述。
比如:
“Taiping.Dao.java.vm"适用于JDK1.6太平交付平台DAO层Interface
“Taiping.Dao.xml.vm”适用于JDK1.6太平交付平台DAO层对应的xml实现(包含针对不同数据库需要的单表SQL)
“Taiping.Dao.java.vm"适用于JDK1.8太平交付平台一体化DAO层Interface
“Taiping.Dao.xml.vm”适用于JDK1.8太平交付平台一体化DAO层对应的xml实现(包含针对不同数据库需要的单表SQL)
另外,原本的代码开发工作基本上变成了模板开发工作。
六、扩展
以上仅针对但表操作,实际项目中双表多表关联查连接查如何解决?
因为如今只实现了单表操作,双表多表的提供意见参考。
6.1表数量扩展
双表操作
vm中同样可以增加双表模板,例如:
select * from
batchinfo b,batchwork bw
where
b.batch_id=bw.batch_id
order by b.id;
select * from
batchinfo b left join batchwork bw
on
b.batch_id=bw.batch_id
order by b.id;
此时只需要在vm模板中进行关联,基于上述代码,容易实现的是根据不同栏位进行关联查询。但是实际项目中,数据库表栏位很多,关联附属表的某一栏位也不确定,虽然可以实现,但基本上实现周期比较长(vm模板修改也需要一定的学习时间),多表关联查直接常规开发比代码生成准备工作快很多,所以基本实现的有效性不高。
多表操作
由于双表操作已经存在很大不确定性,多表复杂度更高,不建议使用。
6.2测试扩展
针对SpringBoot或者基于SpringBoot的微服务项目,因测试模块最好覆盖项目DAO、Servcie层所有方法,并需要满足自动化、独立性、可重复。则代码生成测试数据等是否更加有利?答案是肯定的。
比如:所有含有@RestController标签的controller类所需要的测试json和方法返回的json是否一致,这时候,代码生成测试json会带来很大方便。
此时,只需要在json模板中循环出需要的数据库栏位并替换即可。
6.3前端页面扩展
虽然可以进行前端页面的扩展,但是和实际开发需求会有很大出入,不建议使用。但是此处提供下实现方法,vm模板如下:
6.3.1前端页面扩展一
#foreach($column in $columns)
<el-table-column
prop="${column.attrname}"
header-align="center"
align="center"
label="${column.comments}">
</el-table-column>
#end
<script>
delete (id) {
var ids = id ? [id] : this.dataListSelections.map(item => {
return item.${pk.attrname}
});
}
</script>
6.3.2前端页面扩展二
当有些项目的菜单menu通过数据库查询进行显示。每次新增一个功能需要新增若干个DML语言。此部分如没实现管理员分配出页面或者管理员分配不能完全满足的情况下,可通过定制vm完成。
比如,某一系统通过对SYS_MENU表查询父子id显示菜单。则可通过vm配置新增SQL
--父菜单
INSERT INTO `sys_menu` (`parent_id`, `name`, `url`, `perms`, `type`, `icon`, `order_num`)
VALUES ('1', '${comments}', '${moduleName}/${pathName}', NULL, '1', 'config', '6');
--子菜单
INSERT INTO `sys_menu` (`parent_id`, `name`, `url`, `perms`, `type`, `icon`, `order_num`)
SELECT @parentId, '查看', null, '${moduleName}:${pathName}:list,${moduleName}:${pathName}:info', '2', null, '6';