Mybatis在開發界的地位無需多說,MyBatisPlus是站在Mybatis巨人肩膀上一個很優秀的元件,其理念是隻做增強,不做改變,在大量項目中使用,提升了開發效率。
MyBatisPlus自帶了一個代碼生成器mybatis-plus-generator,可基于資料庫庫表,結合模闆技術,自動生成程式源碼,不過預設情況下,隻支援Entity、Mapper、Service、Controller這些層次。如果想生成未預置的代碼,如vo對象、前端vue頁面等,需要做一些定制和擴充來實作。
首先,說下元件版本情況,不同的版本會有所差異,mybatis-plus-generator 3.5.1前的版本和之後的版本不相容,我這邊使用的MybatisPlus版本是3.5.3。
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.3</version>
</dependency>
代碼生成器的基本使用,請參見官網介紹資料 https://www.baomidou.com/pages/779a6e/ 。
從使用角度而言,官方資料介紹得比較簡要,要實際使用,仍然需要進行具體的嘗試和摸索,包括必要時看下源碼怎麼處理的,才能發現一些限制和限制,最終理順。
今天我重點來說說,如何在mybatis-plus-generator的基礎上,來實作生成前端vue源碼的目的。
官網關于該該部分的說明,位址如下:https://www.baomidou.com/pages/981406/#freemarker%E6%A8%A1%E7%89%88%E6%94%AF%E6%8C%81-dto-vo%E7%AD%89-%E9%85%8D%E7%BD%AE
看完後我是一頭霧水,不知道從哪下手,然後花了比較多的時間,去翻源碼,理了下整體的代碼生成邏輯,最終确認應該如何自定義實作,分享出來給大家參考。
代碼生成器的類有兩個,一是AutoGenerator,二是FastAutoGenerator,這倆啥差別官網沒提,看源碼大緻推測了下,前者是老版本,後者是新版本,後者在前者基礎上做了一些額外工作,如支援通過控制台的模式進行互動式生成,但最終進行代碼生成操作,execute方法中調用的還是AutoGenerator。
我這邊的開發平台開始實際使用的低版本,後來才更新到3.5.3新版本的,并且也不需要控制台這種互動式功能,是以還是沿用使用AutoGenerator,而沒使用FastAutoGenerator。
生成代碼前需要進行必要的配置,主體如下:
public void generateCode(String entityCode) {
//建立代碼生成器對象
AutoGenerator codeGenerator = getCodegGenerator();
//擷取實體配置資訊
Entity entity = entityService.getByCode(entityCode);
//擷取子產品配置資訊
Module module = moduleService.query(entity.getModuleId());
//配置全局資訊
configGlobal(codeGenerator,entity.getAuthor());
//配置包
configPackage(codeGenerator,module.getPackagePath(),module.getCode());
//配置模闆
configTemplate(codeGenerator);
//配置注入
configInjection(codeGenerator,entity,module.getAppCode());
//政策配置
configStrategy(codeGenerator,entity.getCode(), module.getAbbreviation());
//使用Freemarker替代預設的Velocity模闆引擎
MyFreemarkerTemplateEngine freemarkerTemplateEngine = new MyFreemarkerTemplateEngine();
//生成代碼
codeGenerator.execute(freemarkerTemplateEngine);
}
在哪個配置裡進行擴充實作自定義的前端頁面vue源碼生成呢?看名字是推測不出來的,還是通過看源碼擷取到的,實際是在InjectionConfig裡。
具體應該怎麼用呢?
1.需要使用官方提供的一個自定義檔案類,CustomFile,如下,指定生成的檔案名、模闆位置、包名(目錄名)等
//自定義清單視圖模闆
CustomFile listViewFile=new CustomFile.Builder()
.fileName("list.vue")
.templatePath("/templates/list.vue.ftl")
.enableFileOverride()
.packageName("page")
.build();
2.将自定義的檔案類,通過InjectionConfig的對象方法設定進去,可設定多次。
InjectionConfig injectionConfig = new InjectionConfig.Builder()
//自定義變量注入
.customMap(customKeyValue)
//自定義檔案輸出
.customFile(voFile)
.customFile(listViewFile)
.customFile(editViewFile)
.build();
codeGenerator.injection(injectionConfig);
以上兩步實際就完成了整體流程,涉及到一個具體的問題,即如何把模闆用的資料給注入進去。原元件會自動注入大量資料,特别是資料庫表和字段資訊,但是我們要做自定義的前端vue頁面,不可避免也需要再注入大量自定義的變量和資料,這是通過InjectionConfig的customMap對象設定進去的。
前端頁面的檔案模闆,需要放到resources目錄下templates目錄下,如下
<template>
<el-container class="list-container">
<el-main class="main">
<collapse-tab class="query">
<el-form
:inline="true"
:model="queryCondition"
label-width="80px"
@keyup.enter.native="query"
>
<!--查詢條件區 -->
<#list queryConditionList as item>
<#--根據資料類型處理-->
<#if item.dataType=="STRING">
<el-form-item label="${item.name}">
<p-input v-model="queryCondition.${item.code}" type="${item.operation}" class="form-item"/>
</el-form-item>
<#elseif item.dataType=="DATETIME">
<el-form-item label="${item.name}">
<el-date-picker
v-model="queryCondition.${item.code}BeginForQuery"
:value-format="dateFormatter.getDatetimeFormat('${item.formatPattern}')"
:type="dateFormatter.getDatetimeType('${item.formatPattern}')"
align="right"
unlink-panels
placeholder="開始"
class="form-item"
/>
</el-form-item>
<el-form-item label="至">
<el-date-picker
v-model="queryCondition.${item.code}EndForQuery"
:value-format="dateFormatter.getDatetimeFormat('${item.formatPattern}')"
:type="dateFormatter.getDatetimeType('${item.formatPattern}')"
align="right"
unlink-panels
placeholder="結束"
class="form-item"
/>
</el-form-item>
<#elseif item.dataType=="INTEGER" || item.dataType=="LONG" || item.dataType=="DOUBLE" || item.dataType=="DECIMAL" >
<el-form-item label="${item.name}">
<el-input v-model="queryCondition.${item.code}BeginForQuery" class="form-item" />
</el-form-item>
<el-form-item label="至">
<el-input v-model="queryCondition.${item.code}EndForQuery" class="form-item" />
</el-form-item>
<#elseif item.dataType=="DATA_DICTIONARY">
<p-input v-model="queryCondition.${item.code}" class="form-item" />
<#else>
<p-input v-model="queryCondition.${item.code}" class="form-item"/>
</#if>
</#list>
<el-form-item style="float:right;">
<list-button-query :page-code="pageCode"/>
</el-form-item>
<div class="clearfix"/>
</el-form>
</collapse-tab>
<el-card class="table">
<div>
<#list pageButtonList as pageButton>
<el-button
<#if pageButton.permissionFlag=="YES">
v-permission="pageCode+'${pageButton.permissionCode}'"
</#if>
type="primary"
<#if pageButton.icon??>
icon="el-icon-${pageButton.icon}"
</#if>
@click="${pageButton.functionCall}"
>${pageButton.name}</el-button>
</#list>
<div style="clear:both;"/>
</div>
<div style="margin-top:5px;">
<div>
<columns-controller v-model="columnList" :table-key="tableKey"/>
</div>
<el-table
v-loading="loading"
:data="tableData"
style="width: 100%"
highlight-current-row
border
@sort-change="handleSortChange"
@current-change="handleRowChange"
@selection-change="handleSelectionChange"
@row-dblclick="handleDoubleClickRow"
>
<el-table-column type="selection" width="55"/>
<el-table-column
v-for="(item, index) in showCols"
:key="index"
:label="item.label"
:prop="item.prop"
:show-overflow-tooltip="item.showOverflowTooltip"
:width="item.width"
:formatter="item.formatFunc"
:sortable="item.sortable"
/>
<el-table-column fixed="right" label="操作" width="250">
<template slot-scope="scope">
<#list rowButtonList as rowButton>
<el-button
<#if rowButton.permissionFlag=="YES">
v-permission="pageCode+'${rowButton.permissionCode}'"
</#if>
type="text"
@click="${rowButton.functionCall}(scope.row.id)"
>${rowButton.name}</el-button>
</#list>
<el-dropdown style="margin-left:10px;color:#1890ff">
<span class="el-dropdown-link">
更多
<i class="el-icon-arrow-down el-icon--right"/>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>
<table-button-enable :id="scope.row.id" :page-code="pageCode"/>
</el-dropdown-item>
<el-dropdown-item>
<table-button-disable :id="scope.row.id" :page-code="pageCode"/>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
</el-table-column>
</el-table>
</div>
<list-pager
:page-num="pageInfo.pageNum"
:page-size="pageInfo.pageSize"
:page-total="pageTotal"
/>
</el-card>
<detail ref="detail" @ok="handleDetailSave"/>
</el-main>
</el-container>
</template>
<script>
import {listMixin} from '@/mixin/listMixin'
import Detail from './edit'
const MODULE_CODE = '${package.ModuleName}'
const ENTITY_TYPE = '${entity?uncap_first}'
export default {
name: ENTITY_TYPE,
components: {Detail},
mixins: [listMixin],
data() {
return {
entityType: ENTITY_TYPE,
moduleCode: MODULE_CODE,
// eslint-disable-next-line no-eval
api: eval('this.$api.' + MODULE_CODE + '.' + ENTITY_TYPE),
pageCode: MODULE_CODE + ':' + ENTITY_TYPE + ':',
queryCondition: {},
columnList: [
// prop label show 必須定義, width/showOverflowTooltip/formatFunc/sortable 需要的話定義
<#list queryResultList as field>
{
prop: '${field.code}',
label: '${field.name}'
<#if field.showFlag=="YES">
, show: true
<#else>
, show: false
</#if>
<#if field.showOverflowTooltipFlag=="YES">
, showOverflowTooltip: true
<#else>
, showOverflowTooltip: false
</#if>
<#if field.width??>
, width: '${field.width}'
</#if>
<#if field.formatFunction??>
, formatFunction: '${field.formatFunction}'
</#if>
<#if field.sortableFlag=="YES">
, sortable: '${field.sortableFlag}??'
</#if>
}<#if field_has_next>, </#if>
</#list>
]
}
},
methods: {}
}
</script>
<style scoped>
</style>
最後說一個使用時發現的具體問題,原代碼生成元件,預設會在最終生成檔案名前,預設附加實體名字首,例如實體名是User,VO.java.ftl會自動生成UserVO.java,這樣沒問題。但是對于前端頁面,如list.vue,edit.vue,因為是分目錄放實體,希望最終檔案名不變,而不要附加字首變成UserList.vue、UserEdit.vue。
開始的時候,找了下CustomFile對象,發現沒有屬性進行進一步控制,然後就自己寫了個類,繼承于官方的FreemarkerTemplateEngine,重寫了outputCustomFile方法,隻對VO.java才附加實體名。
/**
* 自定義模闆引擎
* 繼承官方,覆寫自定義輸出的檔案名,控制是否附加實體名字首
* @author wqliu
* @date 2023-1-29
*/
public class MyFreemarkerTemplateEngine extends FreemarkerTemplateEngine {
/**
* 視圖對象模闆檔案名
*/
public static final String VO_JAVA = "VO.java";
/**
* 輸出自定義模闆檔案
*
* @param customFiles 自定義模闆檔案清單
* @param tableInfo 表資訊
* @param objectMap 渲染資料
* @since 3.5.3
*/
@Override
protected void outputCustomFile(@NotNull List<CustomFile> customFiles, @NotNull TableInfo tableInfo, @NotNull Map<String, Object> objectMap) {
String entityName = tableInfo.getEntityName();
String parentPath = getPathInfo(OutputFile.parent);
customFiles.forEach(file -> {
String filePath = StringUtils.isNotBlank(file.getFilePath()) ? file.getFilePath() : parentPath;
if (StringUtils.isNotBlank(file.getPackageName())) {
filePath = filePath + File.separator + file.getPackageName();
}
//隻有為視圖對象時,才附加實體名,對于其他模闆,如前端頁面,如list.vue/edit.vue,不附加實體名
String prefix= "";
if(file.getFileName().equals(VO_JAVA))
{
prefix=entityName;
}
String fileName = filePath + File.separator + prefix + file.getFileName();
outputFile(new File(fileName), objectMap, file.getTemplatePath(), file.isFileOverride());
});
}
}