扩展MyBatisPlus代码生成器实现自定义前端页面源码生成

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 lang="scss" 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());
        });
    }

}

猜你喜欢

转载自blog.csdn.net/seawaving/article/details/128865708