Mybatis-Plus Generate 源码分析

如果写一个和Mybatis-Plus类似的代码生成框架,思路比较容易想到。核心的几个步骤就是:

  • 获取数据库表、字段信息;
  • 添加相应的规则,补充额外的信心。
  • 根据表、字段信息、新加的规则生成对应的代码;

Mybatis-Plus整个框架依赖于Spring、Mybatis、模板引擎(freemaker或者velocity)和日志框架slf4j。

整个架构如下:


如上所示:

  • core:是整个框架的核心。包含了对数据库实体的反射提取,分析数据表的字段的信息,数据的CRUD
  • support:定义了相关的接口
  • generate:赋值对相关代码的生成。

本篇只针对Generate部分进行分析。

Generate (代码生成)

其使用方式为首先创建一个AutoGenerator对象,此对象里面包含了所有的相关的配置信息,按照配置的不同类型组织为:

  • 总配置ConfigBuilder,会对下面的各个配置汇总。
  • 注入配置InjectionConfig
  • 数据源配置DataSourceConfig
  • 数据表配置StrategyConfig
  • 包配置PackageConfig
  • 模板配置TemplateConfig
  • 全局配置GlobalConfig

使用AutoGenerator生成代码的时候,外界设定好相关的配置类,然后赋值给AutoGenerator相关属性,最后调用execute即可,非常简单。但是各种各样的配置非常多,如果没有深入的去了解,很有可能不能充分利用这个框架。

Config(配置详解)

ConfigBuilder

ConfigBuilder会对所有的配置再一次封装,比如对某些为null的配置设定为默认值,过参数进行过滤、验证等等。各个配置都有了之后,调用对应的handler执行处理。整个处理过程一定要注意配置初始化的顺序,不能打乱,比如最终的表生成策略依赖于模板配置、数据源配置等等。

具体来讲对应代码如下:

public ConfigBuilder(PackageConfig packageConfig,
                     DataSourceConfig dataSourceConfig, 
                     StrategyConfig strategyConfig,
                     TemplateConfig template, 
                     GlobalConfig globalConfig) {
        // 全局配置
        if (null == globalConfig) {
            this.globalConfig = new GlobalConfig();
        } else {
            this.globalConfig = globalConfig;
        }
        // 模板配置
        if (null == template) {
            this.template = new TemplateConfig();
        } else {
            this.template = template;
        }
        // 包配置
        if (null == packageConfig) {
            handlerPackage(this.template, this.globalConfig.getOutputDir(), new PackageConfig());
        } else {
            handlerPackage(this.template, this.globalConfig.getOutputDir(), packageConfig);
        }
        this.dataSourceConfig = dataSourceConfig;
        handlerDataSource(dataSourceConfig);
        // 策略配置
        if (null == strategyConfig) {
            this.strategyConfig = new StrategyConfig();
        } else {
            this.strategyConfig = strategyConfig;
        }
        handlerStrategy(this.strategyConfig);
}

这个类比较重要,对于各个配置的handle也是在其进行。比如获取表属性。

GlobalConfig

全局配置主要是对于整个自定生成环境的配置。如目录,开发人员名称,是否使用基类,文件命名等。具体来讲包含有如下配置:

     /**
     * 生成文件的输出目录【默认 D 盘根目录】
     */
    private String outputDir = "D://";

    /**
     * 是否覆盖已有文件
     */
    private boolean fileOverride = false;

    /**
     * 是否打开输出目录
     */
    private boolean open = true;

    /**
     * 是否在xml中添加二级缓存配置
     */
    private boolean enableCache = true;

    /**
     * 开发人员
     */
    private String author;

    /**
     * 开启 Kotlin 模式
     */
    private boolean kotlin = false;

    /**
     * 开启 ActiveRecord 模式
     */
    private boolean activeRecord = true;

    /**
     * 开启 BaseResultMap
     */
    private boolean baseResultMap = false;

    /**
     * 开启 baseColumnList
     */
    private boolean baseColumnList = false;
    /**
     * 各层文件名称方式,例如: %Action 生成 UserAction
     */
    private String mapperName;
    private String xmlName;
    private String serviceName;
    private String serviceImplName;
    private String controllerName;
    /**
     * 指定生成的主键的ID类型
     */
    private IdType idType;

需要注意的几个点:

  • 生成文件的输出目录因操作系统不同而不同。默认是Windows的D盘

  • 生成的主键的ID类型有多种。

  •     AUTO(0, "数据库ID自增"),
        INPUT(1, "用户输入ID"),
        ID_WORKER(2, "全局唯一ID"),
        UUID(3, "全局唯一ID"),
        NONE(4, "该类型为未设置主键类型"),
        ID_WORKER_STR(5, "字符串全局唯一ID");
    

PackageConfig

包相关的配置,这个配置比较简单。具体来讲:

    /**
     * 父包名。如果为空,将下面子包名必须写全部, 否则就只需写子包名
     */
    private String parent = "com.baomidou";

    /**
     * 父包模块名。
     */
    private String moduleName = null;
    
    /**
     * Entity包名
     */
    private String entity = "entity";
    
    /**
     * Service包名
     */
    private String service = "service";
    
    /**
     * Service Impl包名
     */
    private String serviceImpl = "service.impl";
    /**
     * Mapper包名
     */
    private String mapper = "mapper";
    
    /**
     * Mapper XML包名
     */
    private String xml = "mapper.xml";
    
    /**
     * Controller包名
     */
    private String controller = "web";

其实也就是对最终生成的目录结构的设定。一个简单的例子:


TemplateConfig

模板配置类,主要是对生成代码文件格式的配置。我们生成不用层次的类对应的模板是不同的,虽然可以通过字符串的方式来实现具体的自动生成。但是使用模板技术更加简单。对于每个层采用不同的模板:

    private String entity = ConstVal.TEMPLATE_ENTITY_JAVA;

    private String service = ConstVal.TEMPLATE_SERVICE;
    
    private String serviceImpl = ConstVal.TEMPLATE_SERVICEIMPL;
    
    private String mapper = ConstVal.TEMPLATE_MAPPER;
    
    private String xml = ConstVal.TEMPLATE_XML;
    
    private String controller = ConstVal.TEMPLATE_CONTROLLER;

这里的ConstVal里面定义全局常量。

以模板文件mapper.java.vm为例:

package ${package.Mapper};

import ${package.Entity}.${entity};
import ${superMapperClassPackage};

/**
 * <p>
 * $!{table.comment} Mapper 接口
 * </p>
 *
 * @author ${author}
 * @since ${date}
 */
#if(${kotlin})
interface ${table.mapperName} : ${superMapperClass}<${entity}>
#else
public interface ${table.mapperName} extends ${superMapperClass}<${entity}> {

}
#end

其对应生成的代码

package com.baomidou.test.mapper;

import com.baomidou.test.entity.Permission;
import com.baomidou.mybatisplus.mapper.BaseMapper;

/**
 * <p>
 * 权限表 Mapper 接口
 * </p>
 *
 * @author Yanghu
 * @since 2018-06-08
 */
public interface PermissionMapper extends BaseMapper<Permission> {

}

DataSourceConfig

数据库配置主要就是定义好相关数据库,用户名,密码等,便于连接到数据库读到想的表字段。具体来讲包含对如下信息的配置:

    /**
     * 数据库信息查询
     */
    private IDbQuery dbQuery;
    /**
     * 数据库类型
     */
    private DbType dbType;
    /**
     * PostgreSQL schemaname
     */
    private String schemaname = "public";
    /**
     * 类型转换
     */
    private ITypeConvert typeConvert;
    /**
     * 驱动连接的URL
     */
    private String url;
    /**
     * 驱动名称
     */
    private String driverName;
    /**
     * 数据库连接用户名
     */
    private String username;
    /**
     * 数据库连接密码
     */
    private String password;

需要说明一下IDbQuery、ITypeConvert。

  • IDbQuery是一个接口里面对查询数据库表、字段、注释信息的封装。因为需要满足多种数据库的自动生成,所以需要正对不同数据库实现IDbQuery不同的类。比如MySqlQuery就是对IDbQuery一种实现
   @Override
    public DbType dbType() {
        return DbType.MYSQL;
    }


    @Override
    public String tablesSql() {
        return "show table status";
    }


    @Override
    public String tableFieldsSql() {
        return "show full fields from `%s`";
    }


    @Override
    public String tableName() {
        return "NAME";
    }


    @Override
    public String tableComment() {
        return "COMMENT";
    }


    @Override
    public String fieldName() {
        return "FIELD";
    }


    @Override
    public String fieldType() {
        return "TYPE";
    }


    @Override
    public String fieldComment() {
        return "COMMENT";
    }


    @Override
    public String fieldKey() {
        return "KEY";
    }


    @Override
    public boolean isKeyIdentity(ResultSet results) throws SQLException {
        return "auto_increment".equals(results.getString("Extra"));
    }
  • ITypeConvert接口是把数据库中filed中的类型转为java中对应的数据类型。具体来讲MySqlTypeConvert实现如下:
    @Override
      public DbColumnType processTypeConvert(String fieldType) {
          String t = fieldType.toLowerCase();
          if (t.contains("char") || t.contains("text")) {
              return DbColumnType.STRING;
          } else if (t.contains("bigint")) {
              return DbColumnType.LONG;
          } else if (t.contains("int")) {
              return DbColumnType.INTEGER;
          } else if (t.contains("date") || t.contains("time") || t.contains("year")) {
              return DbColumnType.DATE;
          } else if (t.contains("text")) {
              return DbColumnType.STRING;
          } else if (t.contains("bit")) {
              return DbColumnType.BOOLEAN;
          } else if (t.contains("decimal")) {
              return DbColumnType.BIG_DECIMAL;
          } else if (t.contains("clob")) {
              return DbColumnType.CLOB;
          } else if (t.contains("blob")) {
              return DbColumnType.BLOB;
          } else if (t.contains("binary")) {
              return DbColumnType.BYTE_ARRAY;
          } else if (t.contains("float")) {
              return DbColumnType.FLOAT;
          } else if (t.contains("double")) {
              return DbColumnType.DOUBLE;
          } else if (t.contains("json") || t.contains("enum")) {
              return DbColumnType.STRING;
          }
          return DbColumnType.STRING;
      }

注意这里为什么用contains作为判断,因为mysql中可以设置数据类型具体大小。虽然大小不固定但是,其前缀是固定。

  • 其次还在DataSourceConfig中创建了数据库连接对象提供给外界使用。
 public Connection getConn() {
        Connection conn = null;
        try {
            Class.forName(driverName);
            conn = DriverManager.getConnection(url, username, password);
        } catch (ClassNotFoundException | SQLException e) {
            e.printStackTrace();
        }
        return conn;
    }

StrategyConfig

对具体生成的表字段进行配置。比如去掉哪些表前缀,字段前缀;定义生成entity的公告字段及相关基类;排除对哪些表自动生成,或者对哪些表自动生成;是否根据表生成对应的注释。具体来讲有如下设置:

/**
 * 表名、字段名、是否使用下划线命名(默认 false)
 */
public static boolean DB_COLUMN_UNDERLINE = false;

/**
 * 是否大写命名
 */
private boolean isCapitalMode = false;

/**
 * 是否跳过视图
 */
private boolean skipView = false;

/**
 * 数据库表映射到实体的命名策略
 */
private NamingStrategy naming = NamingStrategy.nochange;
/**
 * 数据库表字段映射到实体的命名策略<br/>
 * 未指定按照 naming 执行
 */
private NamingStrategy columnNaming = null;

/**
 * 表前缀
 */
private String[] tablePrefix;

/**
 * 字段前缀
 */
private String[] fieldPrefix;

/**
 * 自定义继承的Entity类全称,带包名
 */
private String superEntityClass;

/**
 * 自定义基础的Entity类,公共字段
 */
private String[] superEntityColumns;

/**
 * 自定义继承的Mapper类全称,带包名
 */
private String superMapperClass = ConstVal.SUPERD_MAPPER_CLASS;

/**
 * 自定义继承的Service类全称,带包名
 */
private String superServiceClass = ConstVal.SUPERD_SERVICE_CLASS;

/**
 * 自定义继承的ServiceImpl类全称,带包名
 */
private String superServiceImplClass = ConstVal.SUPERD_SERVICEIMPL_CLASS;

/**
 * 自定义继承的Controller类全称,带包名
 */
private String superControllerClass;

/**
 * 需要包含的表名(与exclude二选一配置)
 */
private String[] include = null;

/**
 * 需要排除的表名
 */
private String[] exclude = null;
/**
 * 【实体】是否生成字段常量(默认 false)<br>
 * -----------------------------------<br>
 * public static final String ID = "test_id";
 */
private boolean entityColumnConstant = false;

/**
 * 【实体】是否为构建者模型(默认 false)<br>
 * -----------------------------------<br>
 * public User setName(String name) { this.name = name; return this; }
 */
private boolean entityBuilderModel = false;

/**
 * 【实体】是否为lombok模型(默认 false)<br>
 * <a href="https://projectlombok.org/">document</a>
 */
private boolean entityLombokModel = false;

/**
 * Boolean类型字段是否移除is前缀(默认 false)<br>
 * 比如 : 数据库字段名称 : 'is_xxx',类型为 : tinyint. 在映射实体的时候则会去掉is,在实体类中映射最终结果为 xxx
 */
private boolean entityBooleanColumnRemoveIsPrefix = false;
/**
 * 生成 <code>@RestController</code> 控制器
 * <pre>
 *      <code>@Controller</code> -> <code>@RestController</code>
 * </pre>
 */
private boolean restControllerStyle = false;
/**
 * 驼峰转连字符
 * <pre>
 *      <code>@RequestMapping("/managerUserActionHistory")</code> -> <code>@RequestMapping("/manager-user-action-history")</code>
 * </pre>
 */
private boolean controllerMappingHyphenStyle = false;

/**
 * 是否生成实体时,生成字段注解
 */
private boolean entityTableFieldAnnotationEnable = false;
/**
 * 乐观锁属性名称
 */
private String versionFieldName;

/**
 * 逻辑删除属性名称
 */
private String logicDeleteFieldName;

/**
 * 表填充字段
 */
private List<TableFill> tableFillList = null;

小结

相关配置介绍完了,可以看到每一个配置对应一个具体的层面。这样的好处在于职责清晰。从代码层面上讲也使用到了诸如观面模式,策略模式等。

Handle(处理配置)

各个配置都设置好了之后就开始进行处理了。其实就调用了一个方法而已

 public void execute() {
        logger.debug("==========================准备生成文件...==========================");
        if(null == this.config) {
            this.config = new ConfigBuilder(this.packageInfo, this.dataSource, this.strategy, this.template, this.globalConfig);
            if(null != this.injectionConfig) {
                this.injectionConfig.setConfig(this.config);
            }
        }

        if(null == this.templateEngine) {
            this.templateEngine = new VelocityTemplateEngine();
        }

        this.templateEngine.init(this.pretreatmentConfigBuilder(this.config)).mkdirs().batchOutput().open();
        logger.debug("==========================文件生成完成!!!==========================");
    }

上面代理主要分为两步。

  1. 第一步:根据配置信息,实例化一个ConfigBuilder。它屏蔽了对配置如何处理的细节,初始化完成之后,ConfigBuilder就完成了对所有配置的加载,以及对应数据库表、字段的提取。
  2. 第二步:调用模板引擎,传入ConfigBuilder。模板引擎根据ConfigBuilder填充对应的模板。最终生成代码。

构造ConfigBuilder

在构造ConfigBuilder中,会一一相关的配置进行handle。这里主要讲一下handlerStrategy,因为这个方法包含了对数据库信息的提取过程,并且将数据库表信息映射为实体。

最终会走到一个名叫getTablesInfo方法。里面涉及到两个基本的、对数据表抽象的实体TableFieldTableInfo

基础实体

TableField

TableField的内容:

    /**
     * 是否需要进行转换
     */
    private boolean convert;
    /**
     * 是否为主键
     */
    private boolean keyFlag;
    /**
     * 主键是否为自增类型
     */
    private boolean keyIdentityFlag;
    /**
     * 对应数据表的名称
     */
    private String name;
    /**
     * 转换之后的类型
     */
    private String type;
    /**
     * 转换之后的属性名
     */
    private String propertyName;
    /**
     * 对应数据表的类型
     */
    private DbColumnType columnType;
    /**
     * 该字段的注释信息
     */
    private String comment;
    /**
     * 填充信息
     */
    private String fill;
    /**
     * 自定义查询字段列表
     */
    private Map<String, Object> customMap;

注意在设置setConvert的时候是传入的一个StrategyConfig,根据StrategyConfig的某些配置确定是否需要转换。

TableInfo

TableInfo是对数据表的抽象,具体来讲:

    /**
     * 是否转换
     */     
    private boolean convert;
    /**
     * 表名
     */
    private String name;
    /**
     * 表注释
     */
    private String comment;
    /**
     * 表所对应的实体名
     */
    private String entityName;
    /**
     * 表所对应的mapper名
     */
    private String mapperName;
    /**
     * 表所对应的xml名
     */
    private String xmlName;
    /**
     * 表所对应的service名
     */
    private String serviceName;
    /**
     * 表所对应的serviceimpl名
     */
    private String serviceImplName;
    /**
     * 表所对应的controller名
     */
    private String controllerName;
    /**
     * 表所包含的所有field集合
     */
    private List<TableField> fields;
    /**
     * 公共字段
     */
    private List<TableField> commonFields;
    /**
     * 所依赖的包名
     */
    private List<String> importPackages = new ArrayList<>();
    /**
     * 说有字段连在一起的字符串,用于日志信息
     */
    private String fieldNames;

这需要注意一点的就是在设置fields的时候需要根据fields的数据类型引入相应的包名。做法就是在设置fields的时候根据field类型引入。具体来讲:

public void setFields(List<TableField> fields) {
        if (CollectionUtils.isNotEmpty(fields)) {
            this.fields = fields;
            // 收集导入包信息,注意为什么用HashSet。因为HashSetk可以自动去除重复的key
            Set<String> pkgSet = new HashSet<>();
            for (TableField field : fields) {
                if (null != field.getColumnType() && null != field.getColumnType().getPkg()) {
                    pkgSet.add(field.getColumnType().getPkg());
                }
                if (field.isKeyFlag()) {
                    // 主键
                    if (field.isConvert() || field.isKeyIdentityFlag()) {
                        pkgSet.add("com.baomidou.mybatisplus.annotations.TableId");
                    }
                    // 自增
                    if (field.isKeyIdentityFlag()) {
                        pkgSet.add("com.baomidou.mybatisplus.enums.IdType");
                    }
                } else if (field.isConvert()) {
                    // 普通字段
                    pkgSet.add("com.baomidou.mybatisplus.annotations.TableField");
                }
                if (null != field.getFill()) {
                    // 填充字段
                    pkgSet.add("com.baomidou.mybatisplus.annotations.TableField");
                    pkgSet.add("com.baomidou.mybatisplus.enums.FieldFill");
                }
            }
            if (!pkgSet.isEmpty()) {
                this.importPackages = new ArrayList<>(Arrays.asList(pkgSet.toArray(new String[]{})));
            }
        }
    }

getTablesInfo

先介绍一下其中的处理逻辑:

  1. 判断是否设置了同时设置了include和exclude

  2. 保存所有表的信息,包含排除的表,需要生成的表。

  3. 根据sql查询表的信息,然后依次将数据映射到对应的基础实体上(TableInfo,FiledInfo)。

    1. 如果设置了include或者exclude,再进一步删选。比如过滤用户输入不存在的表。
    2. 最终得到includeTableList表,礼包包含了需要转换的表名称
    3. 调用convertTableFields将表中的filed转为基础实体。
    4. 最后调用processTable将表、字段信息、其他信息填充到TableInfo中。完成TableInfo基础实体的构造。

其中用到的几条sql语句如下:

  • show table status:获取表信息,比如表名、创建(修改)时间、表注释,数量条数等
  • show full fields from xxx:从特定表中获取表所有字段的信息,比如字段名、字段类型,字段注释以及该表的主键等。

其中使用的而是JDBC最为简单的读取数据库。代码简化一下

//映射Tables
preparedStatement = connection.prepareStatement(tablesSql);
ResultSet results = preparedStatement.executeQuery();
TableInfo tableInfo;
while (results.next()) {   
    ......
    includeTableList.add(tableInfo);
   
}

//映射Fields
for (TableInfo ti : includeTableList) {
           PreparedStatement preparedStatement = connection.prepareStatement(tableFieldsSql);
            ResultSet results = preparedStatement.executeQuery();
           TableField field = new TableField();
            while (results.next()) {
            ......
                 fieldList.add(field);
            }
}

tableInfo.setFields(fieldList);
tableInfo.setCommonFields(commonFieldList);
}

至此所有表以及所有表对应的字段已经完全映射到了基础实体。接下来就是根据基础实体的内容,填充对应的模板。

Template(模板生成)

有了上面所产生的实体,下面就是填模板的过程了。入口如下:

if(null == this.templateEngine) {
            this.templateEngine = new VelocityTemplateEngine();
        }

        this.templateEngine.init(this.pretreatmentConfigBuilder(this.config)).mkdirs().batchOutput().open();
       

TemplateEngine(模板引擎)

MP现在支持两种模板引擎Velocity、Freemarker。这里以Velocity为例。

模板生成相关一共有三个类,分别是AbstractTemplateEngine、FreemarkerTemplateEngine、VelocityTemplateEngine。AbstractTemplateEngine是抽象类定义了相关的接口。具体来讲,提供了如下信息:

其中的writertemplateFilePath为抽象方法,根据不同的模板引擎,选择不同的实现。最终是调用batchOutput来输出所有自动生成的代码。

在batchOutPut中将各个层级的对象,根据模板路径,生成最终的文件。

//遍历所有的表信息,生成文件
List<TableInfo> tableInfoList = this.getConfigBuilder().getTableInfoList();
            for (TableInfo tableInfo : tableInfoList) {
                Map<String, Object> objectMap = this.getObjectMap(tableInfo);
                Map<String, String> pathInfo = this.getConfigBuilder().getPathInfo();
                TemplateConfig template = this.getConfigBuilder().getTemplate();
                
                // Mp.java
                String entityName = tableInfo.getEntityName();
                if (null != entityName && null != pathInfo.get(ConstVal.ENTITY_PATH)) {
                    String entityFile = String.format((pathInfo.get(ConstVal.ENTITY_PATH) + File.separator + "%s" + this.suffixJavaOrKt()), entityName);
                    if (this.isCreate(entityFile)) {
                        this.writer(objectMap, this.templateFilePath(template.getEntity(this.getConfigBuilder().getGlobalConfig().isKotlin())), entityFile);
                    }
                }
                ......
                
                // MpMapper.xml
                // IMpService.java
                // MpServiceImpl.java
                // MpController.java
                }
            }
}
            

接下来看一下VelocityTemplateEngine中的write方法。首先会进行初始化配置

 public VelocityTemplateEngine init(ConfigBuilder configBuilder) {
        //将configBuilder传给父类,在父类中需要用到
        super.init(configBuilder);
        if (null == velocityEngine) {
            Properties p = new Properties();
            p.setProperty(ConstVal.VM_LOADPATH_KEY, ConstVal.VM_LOADPATH_VALUE);
            p.setProperty(Velocity.FILE_RESOURCE_LOADER_PATH, "");
            p.setProperty(Velocity.ENCODING_DEFAULT, ConstVal.UTF8);
            p.setProperty(Velocity.INPUT_ENCODING, ConstVal.UTF8);
            p.setProperty("file.resource.loader.unicode", "true");
            //初始化模板引擎
            velocityEngine = new VelocityEngine(p);
        }
        return this;
    }

父类中调用writer,并将objectMap(包含所有的映射信息)传入,根据templatePath(不同类型模板不一样)创建template。最后将模板内容依据objectMap替换掉。其中的模板路径则根据之前的TemplateConfig得到

    @Override
    public void writer(Map<String, Object> objectMap, String templatePath, String outputFile) throws Exception {
        if (StringUtils.isEmpty(templatePath)) {
            return;
        }
        Template template = velocityEngine.getTemplate(templatePath, ConstVal.UTF8);
        FileOutputStream fos = new FileOutputStream(outputFile);
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(fos, ConstVal.UTF8));
        template.merge(new VelocityContext(objectMap), writer);
        writer.close();
        logger.debug("模板:" + templatePath + ";  文件:" + outputFile);
    }

这里以生存entity为例。

生成的entity

package com.baomidou.test.entity;

import com.baomidou.mybatisplus.annotations.TableId;
import com.baomidou.mybatisplus.enums.IdType;
import com.baomidou.mybatisplus.activerecord.Model;
import java.io.Serializable;

/**
 * <p>
 * 权限表
 * </p>
 *
 * @author wesly
 * @since 2018-06-08
 */
public class Permission extends Model<Permission> {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    /**
     * 上级ID
     */
    private Long pid;
       
    ......
       
    private String description;

    @Override
    public String toString() {
        return "Permission{" +
        ", id=" + id +
        ", pid=" + pid +
        ", title=" + title +
        ", type=" + type +
        ", state=" + state +
        ", sort=" + sort +
        ", url=" + url +
        ", permCode=" + permCode +
        ", icon=" + icon +
        ", description=" + description +
        "}";
    }
}

总结

Generate部分总体来讲思路比价简单。麻烦的部分在于如何去对各个部拆分。最后简单画出了怎个生成框架的类图结构如下:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,566评论 18 139
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,967评论 3 119
  • 时间过得这样快,樱花散尽,蔷薇盛开,栀子谢幕,初荷绽放,转眼,我们的人生就这样疾徐不定的,一路走远了。 有好多事我...
    衿悠阅读 421评论 0 0
  • 文/杠杠 她,名唤水墨画,宰相之女,她的父亲辅助过先皇,如今皇上也让他三分。可她,五岁那年得了罕见的大病,她父亲找...
    啊哈哈哈在笑什么阅读 278评论 0 1
  • 周五晚上从同事的辅导教室出来,已到10点,没想女儿还进学校跑了一圈步,虽说跑的不多,但已有锻炼的意识,值得...
    静等花开之心路阅读 257评论 6 11