从mybatis-plus-generator看如何编写代码生成器

举报
琴岛蛏子 发表于 2022/03/21 00:11:49 2022/03/21
【摘要】 项目中常常用到代码生成器生成代码,下面介绍velocity代码生成原理,及如何编写代码生成器。 Velocity介绍​ Velocity是一个基于Java的模板引擎,基于MVC模型实现,其提供了一个Context容器(相当于Spring的Model),在java代码里面我们可以往容器中存值,然后在vm文件中使用特定的语法获取(相当于Spring页面中取值如freemarker、thymel...

项目中常常用到代码生成器生成代码,下面介绍velocity代码生成原理,及如何编写代码生成器。

Velocity介绍

​ Velocity是一个基于Java的模板引擎,基于MVC模型实现,其提供了一个Context容器(相当于Spring的Model),在java代码里面我们可以往容器中存值,然后在vm文件中使用特定的语法获取(相当于Spring页面中取值如freemarker、thymeleaf)。

官网:http://velocity.apache.org/

maven引入

<!-- https://mvnrepository.com/artifact/org.apache.velocity/velocity -->
<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity</artifactId>
    <version>1.7</version>
</dependency>

velocity 基本语法

变量

设置变量 #set($foo =“hello”) 取值 $foo

访问对象属性 $user.name ${user.name}

使用 v a r i 获取变量时,如果变量不存在, V e l o c i t y 引擎会将其原样输出,通过使用 vari获取变量时,如果变量不存在,Velocity引擎会将其原样输出,通过使用 !{}的形式可以将不存在的变量变成空白输出. 见示例 ${notExist} $!{notExistEmpty}

velocity中大小写敏感

循环

#foreach($i in $list)
    $i
#end

velocity 只会替换变量,所以velocity的语句一般顶行写,以保持文件格式

如上 $i前的空格将会原样输出

条件

#if(condition)
...dosonmething...
#elseif(condition)
...dosomething...
#else
...dosomething...
#end

hello world generator

  1. 初始化了VelocityEngine这个模板引擎,对其设置参数进行初始化,指定使用ClasspathResourceLoader来加载vm文件。

  2. VelocityContext这个Velocity容器中存放对象了。

  3. .vm文件中我们可以取出这些变量,

  4. Template模板输出 template.merge(ctx,sw)

public class HelloWorldVelocity {

  public static void main(String[] args) {
    // 初始化模板引擎
    VelocityEngine velocityEngine = new VelocityEngine();
    velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADER, "classpath");
    velocityEngine.setProperty("classpath.resource.loader.class", ClasspathResourceLoader.class.getName());
    velocityEngine.init();
    // 获取模板文件
    Template template = velocityEngine.getTemplate("helloVelocity.vm");
    // 设置变量
    VelocityContext ctx = new VelocityContext();
    ctx.put("name", "Velocity");
    User user = new User();
    user.setName("zhang san");
    user.setPhone("18612345678");
    ctx.put("user", user);
    List list = new ArrayList();
    list.add("1");
    list.add("2");
    ctx.put("list", list);
    // 输出
    StringWriter sw = new StringWriter();
    template.merge(ctx,sw);
    System.out.println(sw.toString());
  }

resouces目录下的模板文件helloVelocity.vm

#set($foo = 'hello')
$foo $name
${notExist}
$!{notExistEmpty}
$user.name
${user.name}
#foreach($i in $list)
    $i
#end

Gitee: https://gitee.com/tg_seahorse/paw-demos/tree/master/paw-generator

mybatis-plus-generator

Mybatsi-plus官网文档

官网源码Git

generator使用

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-generator</artifactId>
    <version>3.4.1</version>
</dependency>

MyBatis-Plus 支持 Velocity(默认)、Freemarker、Beetl模板引擎

<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.3</version>
</dependency>

参照官网修改的生成代码类

  1. 配置信息写在了main方法的开头,项目路径、包名、要生成的表、表前缀

  2. 配置数据源 mysql 引入依赖mysql-connector-java

  3. cfg.setFileCreate 文件生成策略,return true 会生成文件,覆盖原有文件。

  4. 开启swaggergc.setSwagger2(true);代码使用时需引入swagger依赖

    <dependency>
      <groupId>com.github.xiaoymin</groupId>
      <artifactId>knife4j-spring-boot-starter</artifactId>
    </dependency>
    

代码生成类

public class CodeGenerator {

  public static void main(String[] args) {

    String projectPath = "/Users/rubble/workSpace/paw/paw-demos/paw-generator";
    String author = "Rubble";
    String packageParent = "com.paw.generator";
    String module = "system";
    // 多个用,分隔
    String tables = "sys_user";
    String tablePrefix = "sys_";

    // 代码生成器
    AutoGenerator mpg = new AutoGenerator();

    // 全局配置
    GlobalConfig gc = new GlobalConfig();
    gc.setOutputDir(projectPath + "/src/main/java");
    gc.setAuthor(author);
    gc.setOpen(false);
    // gc.setSwagger2(true); 实体属性 Swagger2 注解
    mpg.setGlobalConfig(gc);

    // 数据源配置
    DataSourceConfig dsc = new DataSourceConfig();
    dsc.setUrl("jdbc:mysql://localhost:3306/ry?useUnicode=true&useSSL=false&characterEncoding=utf8");
    // dsc.setSchemaName("public");
    dsc.setDriverName("com.mysql.cj.jdbc.Driver");
    dsc.setUsername("root");
    dsc.setPassword("123456");
    mpg.setDataSource(dsc);

    // 包配置
    PackageConfig pc = new PackageConfig();
    pc.setModuleName(module);
    pc.setParent(packageParent);
    mpg.setPackageInfo(pc);

    // 自定义配置
    InjectionConfig cfg = new InjectionConfig() {
      @Override
      public void initMap() {
        // to do nothing
      }
    };

    // 如果模板引擎是 freemarker
//    String templatePath = "/templates/mapper.xml.ftl";
    // 如果模板引擎是 velocity
     String templatePath = "/templates/mapper.xml.vm";

    // 自定义输出配置
    List<FileOutConfig> focList = new ArrayList<>();
    // 自定义配置会被优先输出
    focList.add(new FileOutConfig(templatePath) {
      @Override
      public String outputFile(TableInfo tableInfo) {
        // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
        return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
            + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
      }
    });
        /* 允许生成模板文件 */
        cfg.setFileCreate(new IFileCreate() {
            @Override
            public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
                // 判断自定义文件夹是否需要创建
                checkDir(projectPath);
                if (fileType == FileType.MAPPER) {
                    // 已经生成 mapper 文件判断存在,不想重新生成返回 false
                    return !new File(filePath).exists();
                }
                // 允许生成模板文件
                return true;
            }
        });

    cfg.setFileOutConfigList(focList);
    mpg.setCfg(cfg);

    // 配置模板
    TemplateConfig templateConfig = new TemplateConfig();

    // 配置自定义输出模板
    //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
    // templateConfig.setEntity("templates/entity2.java");
    // templateConfig.setService();
    // templateConfig.setController();

    templateConfig.setXml(null);
    mpg.setTemplate(templateConfig);

    // 策略配置
    StrategyConfig strategy = new StrategyConfig();
    strategy.setNaming(NamingStrategy.underline_to_camel);
    strategy.setColumnNaming(NamingStrategy.underline_to_camel);
//    strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
    strategy.setEntityLombokModel(true);
    strategy.setRestControllerStyle(true);
    // 公共父类
//    strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");
    // 写于父类中的公共字段
    strategy.setSuperEntityColumns("id");
    strategy.setInclude(tables.split(","));
    strategy.setControllerMappingHyphenStyle(true);
    strategy.setTablePrefix(tablePrefix);
    mpg.setStrategy(strategy);
    mpg.setTemplateEngine(new VelocityTemplateEngine());
    mpg.execute();
  }

}

CodeGenerator可以用于日常基于mybatis-plus项目的开发中。

自定义模板

  1. 扩展control模板

    Mybatis-plus 模板默认位置resources/templates下,可在配置中进行修改templatePath

    从git项目或jar包中复制controller.java.vm 增加CRUD的方法,只写了简单的add、list方法,可自行就行扩展,如增加分页查找。

    package ${package.Controller};
    
    import java.util.List;
    import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
    import com.paw.generator.system.entity.User;
    import com.paw.generator.system.service.IUserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    #if(${restControllerStyle})
    import org.springframework.web.bind.annotation.RestController;
    #else
    import org.springframework.stereotype.Controller;
    #end
    #if(${superControllerClassPackage})
    import ${superControllerClassPackage};
    #end
    
    /**
     * <p>
     * $!{table.comment} 前端控制器
     * </p>
     *
     * @author ${author}
     * @since ${date}
     */
    #if(${restControllerStyle})
    @RestController
    #else
    @Controller
    #end
    @RequestMapping("#if(${package.ModuleName})/${package.ModuleName}#end/#if(${controllerMappingHyphenStyle})${controllerMappingHyphen}#else${table.entityPath}#end")
    #if(${kotlin})
    class ${table.controllerName}#if(${superControllerClass}) : ${superControllerClass}()#end
    
    #else
    #if(${superControllerClass})
    public class ${table.controllerName} extends ${superControllerClass} {
    #else
    public class ${table.controllerName} {
    #end
    
        @Autowired
        private ${table.serviceName} service;
    
    
        @GetMapping("add")
        public Object add(${entity} entity){
            boolean saved = service.save(entity);
            return entity;
        }
    
        @GetMapping("list")
        public List<${entity}> list(${entity} entity){
            return service.list(new QueryWrapper<>(entity));
        }
    }
    
    #end
    

    生成代码

    /**
     * <p>
     * 用户信息表 前端控制器
     * </p>
     *
     * @author Rubble
     * @since 2021-07-07
     */
    @RestController
    @RequestMapping("/system/user")
    public class UserController {
    
        @Autowired
        private IUserService service;
    
    
        @GetMapping("add")
        public Object add(User entity){
            boolean saved = service.save(entity);
            return entity;
        }
    
        @GetMapping("list")
        public List<User> list(User entity){
            return service.list(new QueryWrapper<>(entity));
        }
    }
    
  2. 自定义模板hello

    配置中增加模板hello.java.vm,定义文件输出位置

    String helloTemplatePath = "/templates/hello.java.vm";
    focList.add(new FileOutConfig(helloTemplatePath) {
      @Override
      public String outputFile (TableInfo tableInfo) {
        return projectPath + "/src/main/java/" + packageParent.replace(".", File.separator) + File.separator + pc.getModuleName() + File.separator + "entity"
            + File.separator + "Hello" + tableInfo.getEntityName() + StringPool.DOT_JAVA;
      }
    });
    

    最简单的模板

    package ${package.Entity};
    
    public class Hello${entity}{
    
    }
    

    执行输出

    package com.paw.generator.system.entity;
    
    public class HelloUser{
    
    }
    

mybatis-plus-generator解析

git下载项目,用jdk8, gradle 6.3 编译通过。

自动配置类AutoGenerator, 除datasource外其他均可默认设置。

/**
 * 配置信息
 */
protected ConfigBuilder config;
/**
 * 注入配置
 */
protected InjectionConfig injection;
/**
 * 数据源配置
 */
private DataSourceConfig dataSource;
/**
 * 数据库表配置
 */
private StrategyConfig strategy;
/**
 * 包 相关配置
 */
private PackageConfig packageInfo;
/**
 * 模板 相关配置
 */
private TemplateConfig template;
/**
 * 全局 相关配置
 */
private GlobalConfig globalConfig;

模板引擎

image-20210707174422127.png

AbstractTemplateEngine 实现了文件的输出controller、service、 entity、mapper。

VelocityTemplateEngine模板引擎

init()初始化VelocityEngine指定文件位置、编码等;

writer引擎模板的渲染 template.merge(new VelocityContext(objectMap), writer);

Map<String, Object> objectMap = this.getObjectMap(config, tableInfo);

@Override
public @NotNull VelocityTemplateEngine init(@NotNull ConfigBuilder configBuilder) {
    if (null == velocityEngine) {
        Properties p = new Properties();
        p.setProperty(ConstVal.VM_LOAD_PATH_KEY, ConstVal.VM_LOAD_PATH_VALUE);
        p.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, StringPool.EMPTY);
        p.setProperty(Velocity.ENCODING_DEFAULT, ConstVal.UTF8);
        p.setProperty(Velocity.INPUT_ENCODING, ConstVal.UTF8);
        p.setProperty("file.resource.loader.unicode", StringPool.TRUE);
        velocityEngine = new VelocityEngine(p);
    }
    return this;
}


@Override
public void writer(@NotNull Map<String, Object> objectMap, @NotNull String templatePath, @NotNull File outputFile) throws Exception {
    Template template = velocityEngine.getTemplate(templatePath, ConstVal.UTF8);
    try (FileOutputStream fos = new FileOutputStream(outputFile);
         OutputStreamWriter ow = new OutputStreamWriter(fos, ConstVal.UTF8);
         BufferedWriter writer = new BufferedWriter(ow)) {
        template.merge(new VelocityContext(objectMap), writer);
    }
}

// objectMap在 AbstractTemplateEngine类中 从配置文件中生成上下文变量加入到context中

objectMap.put的即在模板中可用的属性。

主要属性 PackageInfo, TableInfo

@NotNull
public Map<String, Object> getObjectMap(@NotNull ConfigBuilder config, @NotNull TableInfo tableInfo) {
    GlobalConfig globalConfig = config.getGlobalConfig();
    Map<String, Object> controllerData = config.getStrategyConfig().controller().renderData(tableInfo);
    Map<String, Object> objectMap = new HashMap<>(controllerData);
    Map<String, Object> mapperData = config.getStrategyConfig().mapper().renderData(tableInfo);
    objectMap.putAll(mapperData);
    Map<String, Object> serviceData = config.getStrategyConfig().service().renderData(tableInfo);
    objectMap.putAll(serviceData);
    Map<String, Object> entityData = config.getStrategyConfig().entity().renderData(tableInfo);
    objectMap.putAll(entityData);
    objectMap.put("config", config);
    objectMap.put("package", config.getPackageConfig().getPackageInfo());
    objectMap.put("author", globalConfig.getAuthor());
    objectMap.put("kotlin", globalConfig.isKotlin());
    objectMap.put("swagger", globalConfig.isSwagger());
    objectMap.put("date", globalConfig.getCommentDate());
    // 存在 schemaName 设置拼接 . 组合表名
    String schemaName = config.getDataSourceConfig().getSchemaName();
    if (StringUtils.isNotBlank(schemaName)) {
        schemaName += ".";
        tableInfo.setConvert(true);
    } else {
        schemaName = "";
    }
    objectMap.put("schemaName", schemaName);
    objectMap.put("table", tableInfo);
    objectMap.put("entity", tableInfo.getEntityName());
    return objectMap;
}

下载源码的方式扩展可以任意的put你想要的属性。

引用jar的方式可以增加全局自定义配置, 模板中使用 ${cfg.abc}

// 自定义配置
InjectionConfig cfg = new InjectionConfig() {
  @Override
  public void initMap () {
    // to do nothing
    Map<String,Object> map = new HashMap<>();
    map.put("abc","123");
    setMap(map);
  }
};

Mybatis-plus-generator 在通用性、扩展性做了很多工作,如父类、驼峰、swagger、各种数据库的适配等等,是一个很优秀的工具,向作者致敬。

若依(ruoyi)框架中的generator

本文是若依单体项目thymeleaf版本。module: ruoyi-generator.

生成工具通过后台管理界面的方式让用户进行设置,配置信息保存在数据库中,根据配置生成一套CRUD代码,很是方便。

生成代码总体配置类 GenConfig,设置了包名称规则,作者等全局信息。

入口控制类GenController preview 预览代码, download 下载zip包,genCode生成代码。

  1. 数据库查询配置信息GenTable 加入到VelocityContext中,在vm模板中即可取值
  2. 对定义的模板进行渲染 tpl.merge(context, sw)
public Map<String, String> previewCode(Long tableId)
{
    Map<String, String> dataMap = new LinkedHashMap<>();
    // 查询表信息
    GenTable table = genTableMapper.selectGenTableById(tableId);
    // 设置主子表信息
    setSubTable(table);
    // 设置主键列信息
    setPkColumn(table);
    VelocityInitializer.initVelocity();

    VelocityContext context = VelocityUtils.prepareContext(table);

    // 获取模板列表
    List<String> templates = VelocityUtils.getTemplateList(table.getTplCategory());
    for (String template : templates)
    {
        // 渲染模板
        StringWriter sw = new StringWriter();
        Template tpl = Velocity.getTemplate(template, Constants.UTF8);
        tpl.merge(context, sw);
        dataMap.put(template, sw.toString());
    }
    return dataMap;
}

Put到VelocityContext中的变量

public static VelocityContext prepareContext(GenTable genTable)
{
    String moduleName = genTable.getModuleName();
    String businessName = genTable.getBusinessName();
    String packageName = genTable.getPackageName();
    String tplCategory = genTable.getTplCategory();
    String functionName = genTable.getFunctionName();

    VelocityContext velocityContext = new VelocityContext();
    velocityContext.put("tplCategory", genTable.getTplCategory());
    velocityContext.put("tableName", genTable.getTableName());
    velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】");
    velocityContext.put("ClassName", genTable.getClassName());
    velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName()));
    velocityContext.put("moduleName", genTable.getModuleName());
    velocityContext.put("businessName", genTable.getBusinessName());
    velocityContext.put("basePackage", getPackagePrefix(packageName));
    velocityContext.put("packageName", packageName);
    velocityContext.put("author", genTable.getFunctionAuthor());
    velocityContext.put("datetime", DateUtils.getDate());
    velocityContext.put("pkColumn", genTable.getPkColumn());
    velocityContext.put("importList", getImportList(genTable));
    velocityContext.put("permissionPrefix", getPermissionPrefix(moduleName, businessName));
    velocityContext.put("columns", genTable.getColumns());
    velocityContext.put("table", genTable);
    setMenuVelocityContext(velocityContext, genTable);
    if (GenConstants.TPL_TREE.equals(tplCategory))
    {
        setTreeVelocityContext(velocityContext, genTable);
    }
    if (GenConstants.TPL_SUB.equals(tplCategory))
    {
        setSubVelocityContext(velocityContext, genTable);
    }
    return velocityContext;
}

生成代码的模板,

html: crud的方法add.html,edit.html,list.html,list-tree.html

Java: controller,service,domain,mapper,serviceImpl

Sql:生成菜单用

自定义的mapper.xml

若依框架generator为前端页面框架ruoyi-admin生成了一套完美契合的快速开发的CRUD代码,并支持的用户的选择配置,页面的查询功能都已封装,在此学习,向大神致敬。

总结:

velocity模板引擎分三步,1.初始化 配置模板加载地址;2.放置上下文变量velocityContext;3.渲染模板。

有框架的项目一般会为,框架定制一个代码生成器,以使代码规范化,同时提高开发效率。

个人项目或中小项目开发应用现成框架即可,若不满足需求,要进行修改,了解模板原理,即可快速扩展。

撸文不易,感谢您的鼓励。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。