MyBatis-Plus结合Swagger实现接口代码及文档自动生成工具(进阶篇-基于数据库)
在文章MyBatis-Plus结合Swagger实现接口代码及文档自动生成工具(基础篇-基于配置文件)中,已经实现的功能有基于excel配置文件自动生成基础接口代码(增删改查、分页)以及基于Swagger风格的接口文档。唯一需要使用者做的就是按章模板编写配置文件。下面我们就来分析这种方式的优缺点:
优点:跨越任何数据库、任何平台只要你的电脑上有个excel编辑器就行。
缺点:工作量大,对于已经存在的项目来说需要重新手动配置一份配置文件。造成了重复的劳动,自动化程度不高。
分许:针对上述缺点再结合我这些年的编程经历。我发现绝大多数的项目在数据库设计之初对表字段都会添加说明,因为这样能够减少不必要的沟通成本,并且减少人员流动所带来的损失。因此我在想能不能够直接通过读取数据库表字段的注释来实现为bean对象添加Swagger风格的注释。通过网上了解,这个方案肯定是能够可行的。既然可行那么我们需要解决的问题有哪些。初步估计应该是这几个:
1:如何获取数据库连接?
2:如何读取数据库表及字段说明?
新建读取数据工具类ReadDBUtil,添加数据库连接方式
private static Connection connection = null;
private static String driver = null;
static {
driver = PropertiesUtil.getValue("FileParams.properties", "driver");
String dbUrl = PropertiesUtil.getValue("FileParams.properties", "url");
String username = PropertiesUtil.getValue("FileParams.properties", "username");
String password = PropertiesUtil.getValue("FileParams.properties", "password");
// 加载驱动
try {
Class.forName(driver);
Properties props = new Properties();
props.put("user", username);
props.put("password", password);
connection = DriverManager.getConnection(dbUrl, props);
// 获得数据库连接
// connection = DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException | SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
在配置文件FileParams.properties 中新增关于数据的属性:
#driver=com.mysql.jdbc.Driver
#url=jdbc:mysql://1.1.1.1:3306/lyhtest?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8&useSSL=false&allowMultiQueries=true&useAffectedRows=true
#username=root11
#password=1111
上述配置文件仅限参考,情义实际项目为准。
添加读取数据库表名及备注字段方法:
/**
* @return
* @throws SQLException
* @throws ClassNotFoundException
*/
public static Map<String, Map<String, String>> paraseOtherDb() throws SQLException, ClassNotFoundException {
Map<String, Map<String, String>> maps = new HashMap<>();
// 获得元数据
DatabaseMetaData metaData = connection.getMetaData();
// 获得表信息
ResultSet tables = metaData.getTables(null, null, null, new String[] { "TABLE" });
while (tables.next()) {
// 获得表名
String table_name = tables.getString("TABLE_NAME");
// 通过表名获得所有字段名
ResultSet columns = metaData.getColumns(null, null, table_name, "%");
List<String> tableNames = new ArrayList<String>();
Map<String, String> map = null;
int count = 0;
// 获得所有字段名
while (columns.next()) {
// 获得字段名
String column_name = columns.getString("COLUMN_NAME");
// 获得字段注释 注意: 对于此列,SQL Server 始终会返回 Null。
String remarks = columns.getString("REMARKS");
// https://docs.microsoft.com/zh-cn/sql/connect/jdbc/reference/getcolumns-method-sqlserverdatabasemetadata?view=sql-server-2017
if (tableNames.contains(table_name)) {
map.put(column_name.replaceAll("_", "").toLowerCase(), remarks);
} else {
if (count > 0) {
maps.put(tableNames.get(tableNames.size() - 1), map);
}
maps.put(table_name, map);
map = new HashMap<>();
tableNames.add(table_name);
map.put(column_name.replaceAll("_", "").toLowerCase(), remarks);
}
count++;
}
}
return maps;
}
该方法返回 Map<String, Map<String, String>>结果对象,该map对象的key对应的是表名,value又是一个map对象,
这个对象的key是表中的字段,value是字段对应的字段说明,为什么这样返回,那是因为上文中编写的自动生成bean
注释方法需要这样结构的对象才能自动生成bean注释,为了最小程度的修改代码,因此在该方法做到兼任。
运行测试,发现程序运行良好,达到了预期的目的。
可是可是事情到这就完美无缺了吗?NO!
目前为止,我们的工具能很好地完成这个项目的需求可是既然将它定义成一个工具,就要兼容不同的场景。比如下次换成了
oracle数据库抑或是sqlserver数据库我们的工具能兼容吗?
用事实说话,经过我的测试上述两种数据库都不能达到预期要求。(均不能读取到字段的备注信息)
经过阅读源码以及网上查找资料发现Oracle数据需要手动的开启读取备注字段的开关,具体的做法是在数据库连接代码加上
props.put("remarksReporting", "true");配置。
private static Connection connection = null;
private static String driver = null;
static {
driver = PropertiesUtil.getValue("FileParams.properties", "driver");
String dbUrl = PropertiesUtil.getValue("FileParams.properties", "url");
String username = PropertiesUtil.getValue("FileParams.properties", "username");
String password = PropertiesUtil.getValue("FileParams.properties", "password");
// 加载驱动
try {
Class.forName(driver);
Properties props = new Properties();
props.put("user", username);
props.put("password", password);
props.put("remarksReporting", "true");
connection = DriverManager.getConnection(dbUrl, props);
// 获得数据库连接
// connection = DriverManager.getConnection(url, username, password);
} catch (ClassNotFoundException | SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
加上这行配置最终达到自动生成注释的目的。
SqlServer数据库属性读取代码:
/**
* @return
* @throws SQLException
*/
public static Map<String, Map<String, String>> paraseSqlServer() throws SQLException {
Map<String, Map<String, String>> columns = new HashMap<>(32);
try {
String sql = "SELECT \r\n" + "t=cast(d.name as varchar(500)), \r\n" +
" t1=cast(a.name as varchar(500)), \r\n" +
" t2=isnull(cast(g.[value] as varchar(500)),'') \r\n" + " FROM syscolumns a \r\n" +
" inner join sysobjects d on a.id=d.id and d.xtype='U' and d.name<>'dtproperties' \r\n"
+
" left join sys.extended_properties g on a.id=g.major_id and a.colid=g.minor_id \r\n";
ResultSet rs = connection.createStatement().executeQuery(sql);
String tableName = null;
int count = 0;
List<String> tableNames = new ArrayList<String>();
Map<String, String> map = null;
while (rs.next()) {
tableName = rs.getString("t").replaceAll("_", "").toLowerCase();
if (tableNames.contains(tableName)) {
map.put(rs.getString("t1").replaceAll("_", "").toLowerCase(), rs.getString("t2"));
} else {
if (count > 0) {
columns.put(tableNames.get(tableNames.size() - 1), map);
}
tableNames.add(tableName);
map = new HashMap<>();
map.put(rs.getString("t1").replaceAll("_", "").toLowerCase(), rs.getString("t2"));
}
count++;
}
columns.put(tableName, map);
} catch (Exception e) {
e.printStackTrace();
}
return columns;
}
由于存在数据库上的差异,因此在最初的读取数据库属性方法上需要根据数据的类型走不同的分支,因此最终的代码是这样的
public static Map<String, Map<String, String>> paraseDBInfo2Map(PluginParamsConfig p)
throws SQLException, ClassNotFoundException {
initDB(p);
Map<String, Map<String, String>> tableNameMap = new HashMap<>();
if (driver.toLowerCase().contains("sqlserver")) {
tableNameMap = paraseSqlServer();
} else {
tableNameMap = paraseOtherDb();
}
return tableNameMap;
}
不足:目前该方法只支持三种数据的属性读取,
如果读者在实际项目中遇到其他数据库,如果兼容的话可以给我留言,不兼容的话也可以给我留言,不过最好的是自己改写源码,然后在我的gitgub上修正我这部分代码。
到这里我们可以通过配置和通过数据库配置两种方式来生成自动化代码,那么我们该如何选择呢?
1:如果你是一个崭新的项目,我建议你选择数据库配置的方式,因为这样你不必额外维护一份配置文件。
2:如果是一个老项目并且数据库注释几乎为零,那么我建议你选择配置文件的方式。
项目到这里我们还有不足吗?答案是肯定的,不知道读者是否注意到,我们的工具是整个数据库或者整个配置文件全量的生成代码以及注释。那么问题来了,如果我们是一个老项目,新增了几张表要利用这个工具生成代码会发生什么情况。情况就是所有的代码都会生成一次。
解决方案:在配置扫描bean、rest路径的时候新建一个独立于项目bean和rest的包,把需要新增的bean放在新建的bean路径下,这样就只会生成该路径下bean的自动化代码和文档。
项目到了这里,读者可能会问了我的项目中逻辑不仅只有简单的增删改查及分页操作,因此在rest接口中直接操作mapper对象缺乏灵活度,在复杂需求的时候还是需要自己创建相应的service类,因此是否可以将service接口一并生成,答案是肯定的。
如果读过我上篇文章的读者应该有了思路,没错还是用模板,在template文件中新建一个service模板。
以下我只给出rest和service最终的模板,代码的修改还是请读者自行完成,如果需要借鉴请参考上文的源码链接。
controller.fpl
package com.zte.rest;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.zte.service.${service};
import com.zte.model.${model};
import com.zte.common.util.JsonResult;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
@RestController
@RequestMapping("/${mapping}")
@Api(tags = "${tags}")
public class ${class} {
Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
${service} ${service1};
@ApiOperation(value = "查询列表接口", notes = "")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "path", name = "pageNum", value = "页码", required = true, dataType = "Integer"),
@ApiImplicitParam(paramType = "path", name = "pageSize", value = "每页条数", required = true, dataType = "Integer"),
${ApiImplicitParam}})
@GetMapping(value = "list/{pageNum}/{pageSize}", produces = MediaType.APPLICATION_JSON_VALUE)
public JsonResult list(@PathVariable("pageNum") int pageNum, @PathVariable("pageSize") int pageSize,
${model} item) {
return JsonResult.getSuccess(${service1}.selectList(pageNum, pageSize, item));
}
@ApiOperation(value = "查询单个接口", notes = "")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "path", name = "id", value = "id", required = true, dataType = "Long") })
@GetMapping(value = "item/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public JsonResult item(@PathVariable("id") long id) {
return JsonResult.getSuccess(${service1}.selectOne(id));
}
@ApiOperation(value = "修改接口", notes = "")
@PostMapping(value = "update", produces = MediaType.APPLICATION_JSON_VALUE)
public JsonResult update (@RequestBody ${model} item) {
return JsonResult.getSuccess(${service1}.update(item));
}
@ApiOperation(value = "删除接口", notes = "")
@ApiImplicitParams({
@ApiImplicitParam(paramType = "path", name = "id", value = "id", required = true, dataType = "Long"), })
@GetMapping(value = "delete/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public JsonResult delete(@PathVariable("id") long id) {
return JsonResult.getSuccess(${service1}.delete(id));
}
@ApiOperation(value = "新增接口", notes = "")
@PostMapping(value = "insert", produces = MediaType.APPLICATION_JSON_VALUE)
public JsonResult insert(@RequestBody ${model} item) {
return JsonResult.getSuccess(${service1}.insert(item));
}
}
service.fpl
package com.zte.service;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.zte.mapper.${mapper};
import com.zte.model.${model};
import com.zte.model.${example};
import com.zte.param.PageInfo;
@Service
public class ${serviceClass} {
@Autowired
private ${mapper} ${mapper1};
/**
* 条件查询、满足所有条件组合查询
* @param pageNum 页码
* @param pageSize 分页大小
* @param item 查询条件对象
* @return 分页对象PageInfo
*/
public PageInfo selectList(int pageNum, int pageSize, ${model} item) {
${example} example = null;
// 补充查询参数开始
if (item != null) {
example = new ${example}();
${example}.Criteria criteria = example.createCriteria();
${params}
}
// 补充查询参数结束
Page<List<${model}>> page = PageHelper.startPage(pageNum, pageSize);
List<${model}> list = ${mapper1}.selectByExample(example);
PageInfo info = new PageInfo(page.getPageNum(), page.getPageSize(), page.getTotal(), page.getPages(), list);
return info;
}
/**
* 通过主键查询
* @param id 主键id
* @return
*/
public ${model} selectOne(Long id)
{
return ${mapper1}.selectByPrimaryKey(id);
}
/**
* 通过主键删除
* @param id 主键id
* @return
*/
public int delete(Long id) {
return ${mapper1}.deleteByPrimaryKey(id);
}
/**
* 更新对象
* @param item
* @return
*/
public int update(${model} item)
{
return ${mapper1}.updateByPrimaryKeySelective(item);
}
/**
* 插入对象
* @param item
* @return
*/
public int insert(${model} item)
{
return ${mapper1}.insertSelective(item);
}
}