相关文章推荐
彷徨的仙人掌  ·  Java ...·  2 周前    · 
要出家的吐司  ·  工具 | ...·  6 月前    · 
年轻有为的针织衫  ·  浅析VB.Net语言 ...·  10 月前    · 
胆小的签字笔  ·  elasticsearch - ...·  1 年前    · 

2021年4月构建本项目,集成了 Mybatis 和 Mybatis Plus 两种的生成逻辑。

2021年5月添加resultMap模板生成逻辑。

2022年9月补充了 SpringData JPA 的代码生成逻辑,同时重构了一下代码。

在 SpringBoot 项目开发前,关于初始代码的生成,是值得考虑的一件事。当我们根据业务需求完成表设计后,接下来就需要根据表生成相关代码,在 SpringBoot 项目中需要以下几部分内容:

  • entity, 实体层,用于存放我们的实体类,与数据库中的属性值基本保持一致,实现set和get的方法 ;
  • mapper,对数据库进行数据持久化操作,它的方法语句是直接针对数据库操作的,主要实现一些增删改查操作,在 mybatis 中方法主要与与 xxx.xml 内相互一一映射;
  • service,业务 service 层,给 controller 层的类提供接口进行调用。一般就是自己写的方法封装起来,就是声明一下,具体实现在 serviceImpl 中;
  • controller,控制层,负责具体模块的业务流程控制,需要调用 service 逻辑设计层的接口来控制业务流程。因为 service 中的方法是我们使用到的,controller 通过接收前端 H5 或者 App 传过来的参数进行业务操作,再将处理结果返回到前端。
  • 除了上述项目架构中最基本的文件,为了更好的管理项目,我们还增加以下几个层级:

  • dto文件,用来分担实体类的功效,可以将查询条件单独封装一个类,以及前后端交互的实体类(有时候我们可能会传入 entity 实体类中不存在的字段);
  • vo文件,后台返回给前台的数据结构,同样可以自定义字段;
  • struct文件,用来处理 dto 、entity、vo 文件之间的转换。
  • 项目中使用的 ORM 框架多为 Mybatis、 Mybatis Plus 和 Spring Data JPA,虽然各自的官方文档都有代码生成器配置,但是过于简单,无法满足实际需求,因此整理出一套通用的代码生成器,势在必行。

    在开始本文之前,首先介绍一下要用到的知识点。

    FreeMarker

    FreeMarker 是一款模板引擎: 即一种基于模板和要改变的数据, 并用来生成输出文本(HTML网页,电子邮件,配置文件,源代码等)的通用工具。 它不是面向最终用户的,而是一个Java类库,是一款程序员可以嵌入他们所开发产品的组件。

    模板编写为FreeMarker Template Language (FTL)。它是简单的,专用的语言, 不是 像 PHP 那样成熟的编程语言。 那就意味着要准备数据在真实编程语言中来显示,比如数据库查询和业务运算, 之后模板显示已经准备好的数据。在模板中,你可以专注于如何展现数据, 而在模板之外可以专注于要展示什么数据。

    Mybatis

    MyBatis 是一款优秀的持久层框架,它支持自定义 SQL、存储过程以及高级映射。MyBatis 免除了几乎所有的 JDBC 代码以及设置参数和获取结果集的工作。MyBatis 可以通过简单的 XML 或注解来配置和映射原始类型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

    Mybatis Plus

    MyBatis-Plus (opens new window)(简称 MP)是一个 MyBatis (opens new window)的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

    Spring Data JPA

    Spring Data JPA 是 Spring Data 项目的一部分,它可以更轻松地实现基于 JPA 的存储库。

    Spring Data JPA 可以与 Hibernate、Eclipse Link 或任何其他 JPA 提供程序一起使用。使用 Spring 或 Java EE 的一个非常有趣的好处是您可以使用 @Transactional 注解以声明方式控制事务边界。

    本文主要讲述选择使用 Mybatis、 Mybatis Plus 和 Spring Data JPA 时,相关代码文件的生成过程。

    JCommander

    JCommander 是一个用于解析命令行参数的Java框架,支持解析所有基本的数据类型,也支持将命令行解析成用户自定义的类型,只需要写一个转变函数。

    接下来就进行代码实战环节。

    首先新建一个 maven 项目,命名为 mybatis-generator。

    基本环境配置

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.6.3</version>
    <!--        <version>2.5.12</version>-->
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
        <groupId>com.msdn.generator</groupId>
        <artifactId>orm-generator</artifactId>
        <version>1.0-SNAPSHOT</version>
        <properties>
            <java.version>1.8</java.version>
            <logback.version>1.2.3</logback.version>
            <fastjson.version>1.2.73</fastjson.version>
            <hutool.version>5.5.1</hutool.version>
            <mysql.version>8.0.19</mysql.version>
            <mybatis.version>2.1.4</mybatis.version>
            <mapper.version>4.1.5</mapper.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    <!--        <dependency>-->
    <!--            <groupId>org.springframework.boot</groupId>-->
    <!--            <artifactId>spring-boot-starter-validation</artifactId>-->
    <!--        </dependency>-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-jpa</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-security</artifactId>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>${fastjson.version}</version>
            </dependency>
            <dependency>
                <groupId>ch.qos.logback</groupId>
                <artifactId>logback-classic</artifactId>
                <version>${logback.version}</version>
            </dependency>
            <dependency>
                <groupId>cn.hutool</groupId>
                <artifactId>hutool-all</artifactId>
                <version>${hutool.version}</version>
            </dependency>
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
                <scope>runtime</scope>
            </dependency>
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.version}</version>
            </dependency>
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper</artifactId>
                <version>${mapper.version}</version>
            </dependency>
            <!--JCommander解析命令行参数-->
            <dependency>
                <groupId>com.beust</groupId>
                <artifactId>jcommander</artifactId>
                <version>1.78</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-freemarker</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-configuration-processor</artifactId>
                <optional>true</optional>
            </dependency>
            <dependency>
                <groupId>com.github.pagehelper</groupId>
                <artifactId>pagehelper-spring-boot-starter</artifactId>
                <version>1.4.3</version>
            </dependency>
            <dependency>
                <groupId>org.springframework.data</groupId>
                <artifactId>spring-data-commons</artifactId>
                <version>2.4.6</version>
            </dependency>
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus-boot-starter</artifactId>
                <version>3.5.1</version>
            </dependency>
            <dependency>
                <groupId>org.springdoc</groupId>
                <artifactId>springdoc-openapi-ui</artifactId>
                <version>1.6.9</version>
            </dependency>
            <dependency>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-jdk8</artifactId>
                <version>1.5.2.Final</version>
            </dependency>
            <dependency>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>1.5.2.Final</version>
            </dependency>
        </dependencies>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
            </plugins>
        </build>
    </project>
    

    application.yml 文件内容如下:

    server:
      port: 8525
    spring:
      application:
        name: orm-generator
    springdoc:
      swagger-ui:
        # 修改Swagger UI路径
        path: /swagger-ui.html
        # 开启Swagger UI界面
        enabled: true
      api-docs:
        # 修改api-docs路径
        path: /v3/api-docs
        # 开启api-docs
        enabled: true
        # 配置需要生成接口文档的扫描包
        packages-to-scan: com.msdn.generator.controller
    

    为了接收相关配置参数,我们通过 JCommander 解析命令行参数,此处创建对应的实体类 GenerateParameter来接收这些参数。

    @Getter
    @Setter
    @Schema(name = "使用帮助")
    @Parameters(commandDescription = "使用帮助")
    public class GenerateParameter {
      @Schema(name = "mysql主机名")
      @Parameter(names = {"--host", "-h"}, description = "mysql主机名")
      private String host;
      @Schema(name = "mysql端口")
      @Parameter(names = {"--port", "-P"}, description = "mysql端口")
      private Integer port;
      @Schema(name = "mysql用户名")
      @Parameter(names = {"--username", "-u"}, description = "mysql用户名")
      private String username;
      @Schema(name = "mysql密码")
      @Parameter(names = {"--password", "-p"}, description = "mysql密码")
      private String password;
      @Schema(name = "mysql数据库名")
      @Parameter(names = {"--database", "-d"}, description = "mysql数据库名")
      private String database;
      @Schema(name = "mysql数据库表")
      @Parameter(names = {"--table", "-t"}, description = "mysql数据库表")
      private List<String> table;
      @Schema(name = "业务模块名")
      @Parameter(names = {"--module", "-m"}, description = "业务模块名")
      private String module;
      @Schema(name = "业务分组,目前是base和business")
      @Parameter(names = {"--group", "-g"}, description = "业务分组,目前是base和business")
      private String group;
      @Schema(name = "是否按表名分隔目录")
      @Parameter(names = {"--flat"}, description = "是否按表名分隔目录")
      private boolean flat;
      @Schema(name = "orm框架选择")
      @Parameter(names = {"--type"}, description = "orm框架选择")
      private String type;
      @Schema(name = "查看帮助")
      @Parameter(names = "--help", help = true, description = "查看帮助")
      private boolean help;
      @Schema(name = "表名截取起始索引,比如表名叫做t_sale_contract_detail,生成的实体类为ContractDetail,则该字段为7")
      @Parameter(names = {"--tableStartIndex", "-tsi"}, description = "表名截取起始索引")
      private String tableStartIndex;
    

    当连接上数据库后,我们需要解析读取的表结构,包括获取表字段,字段备注,字段类型等内容,对应此处创建的 Column 类。

    @Data
    public class Column {
         * 是否是主键
        private Boolean isPrimaryKey;
         * Mybatis plus生成类主键类型,默认为ASSIGN_ID(3)
        private String primaryKeyType = "ASSIGN_ID";
         * 数据库表名称
        private String tableName;
         * 表描述
        private String tableDesc;
         * 数据库字段名称
        private String fieldName;
         * 数据库字段类型
        private String fieldType;
         * Java类型
        private String javaType;
         * 是否是数字类型
        private Boolean isNumber;
         * 数据库字段驼峰命名,saleBooke
        private String camelName;
         * 数据库字段Pascal命名,SaleBook
        private String pascalName;
         * 数据库字段注释
        private String comment;
        private String field;
        private String key;
         * 是否是公共字段
        private Boolean isCommonField;
    

    最后创建一个常量类 Config,来存储常量信息。

    public class Config {
      public static final String OUTPUT_PATH = "." + File.separator + "output";
      public static final String AUTHOR = "hresh";
      // 公共实体类字段
      public static final String[] JPA_COMMON_COLUMNS = new String[]{
          "create_user_code", "create_user_name", "created_date", "last_modified_code",
          "last_modified_name"
          , "last_modified_date", "version", "id", "del_flag"
      public static final String[] MYBATIS_COMMON_COLUMNS = new String[]{
          "create_user_code", "create_user_name", "created_date", "last_modified_code",
          "last_modified_name"
          , "last_modified_date", "version", "id", "del_flag"
      public static final String[] MYBATIS_PLUS_COMMON_COLUMNS = new String[]{
          "create_user_code", "create_user_name", "created_date", "last_modified_code",
          "last_modified_name"
          , "last_modified_date", "version", "id", "del_flag"
    

    首先定义 FreeMarker 的使用代码:

    @Service
    public class FreemarkerService {
      @Autowired
      private Configuration configuration;
       * 输出文件模板
       * @param templateName      resources 文件夹下的模板名,比如说model.ftl,是生成实体类的模块
       * @param dataModel         表名,字段名等内容集合
       * @param filePath          输出文件名,包括路径
       * @param generateParameter
       * @throws Exception
      public void write(String templateName, Map<String, Object> dataModel, String filePath,
          GenerateParameter generateParameter) throws Exception {
        // FTL(freemarker templete language)模板的文件名称
        Template template = configuration
            .getTemplate(dataModel.get("type") + File.separator + templateName + ".ftl");
        File file;
        // 判断是不是多表,如果是,则按照表名生成各自的文件夹目录
        if (generateParameter.isFlat()) {
          file = new File(
              Config.OUTPUT_PATH + File.separator + dataModel.get("tempId") + File.separator + filePath);
        } else {
          file = new File(
              Config.OUTPUT_PATH + File.separator + dataModel.get("tempId") + File.separator + dataModel
                  .get("tableName") + File.separator + filePath);
        if (!file.exists()) {
          file.getParentFile().mkdirs();
          file.createNewFile();
        FileOutputStream fileOutputStream = new FileOutputStream(file);
        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(fileOutputStream,
            StandardCharsets.UTF_8);
        template.process(dataModel, outputStreamWriter);
        fileOutputStream.flush();
        fileOutputStream.close();
    

    接下来是本项目最核心的代码,通过读取数据表,获取表的定义信息,然后利用 FreeMarker 读取 Ftl 模板文件来生成关于该表的基础代码。

    基础服务类 BaseService

    public class BaseService {
      private static Connection connection;
      public static void setConnection(GenerateParameter generateParameter) throws Exception {
        connection = getConnection(generateParameter);
      public static void closeConnection() throws SQLException {
        connection.close();
      public static String getUrl(GenerateParameter generateParameter) {
        return "jdbc:mysql://" + generateParameter.getHost() + ":" + generateParameter.getPort() + "/"
            + generateParameter.getDatabase()
            + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8";
       * 数据库连接,类似于:DriverManager.getConnection("jdbc:mysql://localhost:3306/test_demo?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC","root","password");
       * @param generateParameter 请求参数
       * @return 数据库连接
       * @throws Exception
      public static Connection getConnection(GenerateParameter generateParameter) throws Exception {
        return DriverManager.getConnection(getUrl(generateParameter), generateParameter.getUsername(),
            generateParameter.getPassword());
       * 根据表具体位置,获取表中字段的具体信息,包括字段名,字段类型,备注等
       * @param tableName
       * @return
       * @throws Exception
      public List<Column> getColumns(String tableName, String[] commonColumns) throws Exception {
        // 获取表定义的字段信息
        ResultSet resultSet = connection.createStatement()
            .executeQuery("SHOW FULL COLUMNS FROM " + tableName);
        List<Column> columnList = new ArrayList<>();
        while (resultSet.next()) {
          String fieldName = resultSet.getString("Field");
          Column column = new Column();
          // 判断是否是主键
          column.setIsPrimaryKey("PRI".equals(resultSet.getString("Key")));
          // 获取字段名称
          column.setFieldName(fieldName);
          // 实体类特定字段从核心类里获取
          if (Objects.nonNull(commonColumns) && Arrays.asList(commonColumns).contains(fieldName)) {
            column.setIsCommonField(true);
          } else {
            column.setIsCommonField(false);
          // 获取字段类型
          column.setFieldType(resultSet.getString("Type").replaceAll("\\(.*\\)", ""));
          switch (column.getFieldType()) {
            case "json":
            case "longtext":
            case "char":
            case "varchar":
            case "text":
              column.setJavaType("String");
              column.setIsNumber(false);
              break;
            case "date":
            case "datetime":
              column.setJavaType("Date");
              column.setIsNumber(false);
              break;
            case "timestamp":
              column.setJavaType("LocalDateTime");
              column.setIsNumber(false);
              break;
            case "bit":
              column.setJavaType("Boolean");
              column.setIsNumber(false);
              break;
            case "int":
            case "tinyint":
              column.setJavaType("Integer");
              column.setIsNumber(true);
              break;
            case "bigint":
              column.setJavaType("Long");
              column.setIsNumber(true);
              break;
            case "decimal":
              column.setJavaType("BigDecimal");
              column.setIsNumber(true);
              break;
            case "varbinary":
              column.setJavaType("byte[]");
              column.setIsNumber(false);
              break;
            default:
              throw new Exception(
                  tableName + " " + column.getFieldName() + " " + column.getFieldType() + "类型没有解析");
          // 转换字段名称,receipt_sign_name字段改为 receiptSignName
          column.setCamelName(StringUtils.underscoreToCamel(column.getFieldName()));
          // 首字母大写
          column.setPascalName(StringUtils.firstLetterUpperCase(column.getCamelName()));
          // 字段在数据库的注释
          column.setComment(resultSet.getString("Comment"));
          columnList.add(column);
        return columnList;
       * 获取表的描述
       * @param tableName
       * @param parameter
       * @return
       * @throws Exception
      public String getTableComment(String tableName, GenerateParameter parameter) throws Exception {
        Connection connection = getConnection(parameter);
        ResultSet resultSet = connection.createStatement().executeQuery(
            "SELECT table_comment FROM INFORMATION_SCHEMA.TABLES WHERE table_schema = '" + parameter
                .getDatabase()
                + "' AND table_name = '" + tableName + "'");
        String tableComment = "";
        while (resultSet.next()) {
          tableComment = resultSet.getString("table_comment");
        return tableComment;
    

    GenerateService 获取表信息生成相关代码

    @Service
    @Slf4j
    public class GenerateService extends BaseService {
      @Autowired
      private FreemarkerService freemarkerService;
       * @param tableName 数据库表名
       * @param parameter 模块名
       * @param uuid      生成uuid
       * @throws Exception
      public void generate(String tableName, GenerateParameter parameter, String uuid)
          throws Exception {
        // 各模块包名,比如 com.msdn.sale 或 com.msdn.finance
        String packagePrefix = "com.msdn." + parameter.getModule();
        // 分组
        if (!StringUtils.isEmpty(parameter.getGroup())) {
          packagePrefix = packagePrefix + "." + parameter.getGroup();
        // 根据项目设计的表名获取到表名,比如表名叫做:t_sale_contract_detail
        // 现在表名截取起始索引该由参数配置
        Integer index = new Integer(parameter.getTableStartIndex());
        // 驼峰命名,首字母小写,比如:contractDetail
        String camelName = StringUtils.underscoreToCamel(tableName.substring(index));
        Map<String, Object> dataModel = new HashMap<>();
        //获取表中字段的具体信息,包括字段名,字段类型,备注等,排除指定字段
        String[] commonColumns = Config.MYBATIS_COMMON_COLUMNS;
        if ("jpa".equals(parameter.getType().toLowerCase())) {
          commonColumns = Config.JPA_COMMON_COLUMNS;
        } else if ("mybatis".equals(parameter.getType().toLowerCase())) {
          commonColumns = Config.MYBATIS_COMMON_COLUMNS;
        } else if ("mybatisplus".equals(parameter.getType().toLowerCase())) {
          commonColumns = Config.MYBATIS_PLUS_COMMON_COLUMNS;
        List<Column> columns = getColumns(tableName, commonColumns);
        Column primaryColumn = columns.stream().filter(Column::getIsPrimaryKey).findFirst()
            .orElse(null);
        dataModel.put("package", packagePrefix);
        dataModel.put("camelName", camelName);
        // 首字母转大写,作为实体类名称等
        dataModel.put("pascalName", StringUtils.firstLetterUpperCase(camelName));
        dataModel.put("moduleName", parameter.getModule());
        dataModel.put("tableName", tableName);
        // 表描述
        dataModel.put("tableComment", getTableComment(tableName, parameter));
        dataModel.put("columns", columns);
        dataModel.put("primaryColumn", primaryColumn);
        dataModel.put("tempId", uuid);
        dataModel.put("author", Config.AUTHOR);
        dataModel.put("date", DateUtil.now());
        dataModel.put("type", parameter.getType());
        log.info("准备生成模板代码的表名为:" + tableName + ",表描述为:" + dataModel.get("tableComment"));
        // 生成模板代码
        log.info("**********开始生成Model模板文件**********");
        generateModel(dataModel, parameter);
        log.info("**********开始生成VO视图模板文件**********");
        generateVO(dataModel, parameter);
        log.info("**********开始生成DTO模板文件**********");
        generateDTO(dataModel, parameter);
        log.info("**********开始生成Struct模板文件**********");
        generateStruct(dataModel, parameter);
        log.info("**********开始生成Mapper模板文件**********");
        generateMapper(dataModel, parameter);
        log.info("**********开始生成Service模板文件**********");
        generateService(dataModel, parameter);
        log.info("**********开始生成Controller模板文件**********");
        generateController(dataModel, parameter);
       * 生成 controller 模板代码
       * @param dataModel
       * @param generateParameter
       * @throws Exception
      private void generateController(Map<String, Object> dataModel,
          GenerateParameter generateParameter) throws Exception {
        String path =
            "java" + File.separator + "controller" + File.separator + dataModel.get("pascalName")
                + "Controller.java";
        freemarkerService.write("controller", dataModel, path, generateParameter);
      private void generateDTO(Map<String, Object> dataModel, GenerateParameter generateParameter)
          throws Exception {
        String path = "java" + File.separator + "dto" + File.separator + dataModel.get("pascalName");
        freemarkerService.write("dto", dataModel, path + "DTO.java", generateParameter);
        freemarkerService.write("dto-page", dataModel, path + "QueryPageDTO.java", generateParameter);
      private void generateModel(Map<String, Object> dataModel, GenerateParameter generateParameter)
          throws Exception {
        String path =
            "java" + File.separator + "model" + File.separator + dataModel.get("pascalName") + ".java";
        freemarkerService.write("model", dataModel, path, generateParameter);
      private void generateStruct(Map<String, Object> dataModel, GenerateParameter generateParameter)
          throws Exception {
        String path = "java" + File.separator + "struct" + File.separator + dataModel.get("pascalName")
            + "Struct.java";
        freemarkerService.write("struct", dataModel, path, generateParameter);
      private void generateMapper(Map<String, Object> dataModel, GenerateParameter generateParameter)
          throws Exception {
        if (!"jpa".equals(generateParameter.getType().toLowerCase())) {
          String path = "java" + File.separator + "mapper" + File.separator + dataModel.get("pascalName")
              + "Mapper.java";
          freemarkerService.write("mapper", dataModel, path, generateParameter);
          path = "resources" + File.separator + dataModel.get("pascalName") + "Mapper.xml";
          freemarkerService.write("mapper-xml", dataModel, path, generateParameter);
        }else {
          String path = "java" + File.separator + "repository" + File.separator + dataModel.get("pascalName")
              + "Repository.java";
          freemarkerService.write("repository", dataModel, path, generateParameter);
      private void generateService(Map<String, Object> dataModel, GenerateParameter generateParameter)
          throws Exception {
        String path = "java" + File.separator + "service" + File.separator + dataModel.get("pascalName")
            + "Service.java";
        freemarkerService.write("service", dataModel, path, generateParameter);
        path =
            "java" + File.separator + "service" + File.separator + "impl" + File.separator + dataModel
                .get("pascalName") + "ServiceImpl.java";
        freemarkerService.write("service-impl", dataModel, path, generateParameter);
      private void generateVO(Map<String, Object> dataModel, GenerateParameter generateParameter)
          throws Exception {
        String path =
            "java" + File.separator + "vo" + File.separator + dataModel.get("pascalName") + "VO.java";
        freemarkerService.write("vo", dataModel, path, generateParameter);
    

    为了更加方便地使用代码生成器,我们通过 swagger 来调用 Rest 服务接口。

    @RestController("/generator")
    @Slf4j
    @RequiredArgsConstructor
    public class GeneratorController {
      private final GenerateService generateService;
      private final XmlGenerateService xmlGenerateService;
          // 请求参数
              "database": "db_tl_sale",
              "flat": true,
              "type": "mybatis",
              "group": "base",
              "host": "127.0.0.1",
              "module": "sale",
              "password": "123456",
              "port": 3306,
              "table": [
                  "t_xs_sale_contract"
              "username": "root"
      @PostMapping("/build")
      @Operation(description = "选择orm框架后生成基础模版代码")
      public void build(@RequestBody GenerateParameter parameter, HttpServletResponse response)
          throws Exception {
        log.info("**********欢迎使用基于FreeMarker的模板文件生成器**********");
        log.info("************************************************************");
        String uuid = UUID.randomUUID().toString();
        BaseService.setConnection(parameter);
        for (String table : parameter.getTable()) {
          generateService.generate(table, parameter, uuid);
        log.info("**********模板文件生成完毕,准备下载**********");
        String path = Config.OUTPUT_PATH + File.separator + uuid;
        //设置响应头控制浏览器的行为,这里我们下载zip
        response.setHeader("Content-disposition", "attachment; filename=code.zip");
        response.setHeader("Access-Control-Expose-Headers", "Content-disposition");
        // 将response中的输出流中的文件压缩成zip形式
        ZipDirectory(path, response.getOutputStream());
        // 递归删除目录
        FileSystemUtils.deleteRecursively(new File(path));
        BaseService.closeConnection();
        log.info("************************************************************");
        log.info("**********模板文件下载完毕,谢谢使用**********");
      @PostMapping("/buildXml")
      @Operation(description = "选择orm框架后生成基础模版代码,针对Mybatis会补充生成xml文件中的resultMap")
      public void buildXml(@RequestBody GenerateParameter parameter, HttpServletResponse response)
          throws Exception {
        log.info("**********欢迎使用基于FreeMarker的模板文件生成器**********");
        log.info("************************************************************");
        String uuid = UUID.randomUUID().toString();
        BaseService.setConnection(parameter);
        for (String table : parameter.getTable()) {
          xmlGenerateService.generate(table, parameter, uuid);
        log.info("**********模板文件生成完毕,准备下载**********");
        String path = Config.OUTPUT_PATH + File.separator + uuid;
        //设置响应头控制浏览器的行为,这里我们下载zip
        response.setHeader("Content-disposition", "attachment; filename=code.zip");
        response.setHeader("Access-Control-Expose-Headers", "Content-disposition");
        // 将response中的输出流中的文件压缩成zip形式
        ZipDirectory(path, response.getOutputStream());
        // 递归删除目录
        FileSystemUtils.deleteRecursively(new File(path));
        BaseService.closeConnection();
        log.info("************************************************************");
        log.info("**********模板文件下载完毕,谢谢使用**********");
       * 一次性压缩多个文件,文件存放至一个文件夹中
      public static void ZipDirectory(String directoryPath, ServletOutputStream outputStream) {
        try {
          ZipOutputStream output = new ZipOutputStream(outputStream);
          List<File> files = getFiles(new File(directoryPath));
          for (File file : files) {
            try (InputStream input = new FileInputStream(file)) {
              output.putNextEntry(new ZipEntry(file.getPath().substring(directoryPath.length() + 1)));
              int temp;
              while ((temp = input.read()) != -1) {
                output.write(temp);
          output.close();
        } catch (Exception e) {
          e.printStackTrace();
      public static List<File> getFiles(File file) {
        List<File> files = new ArrayList<>();
        for (File subFile : Objects.requireNonNull(file.listFiles())) {
          if (subFile.isDirectory()) {
            List<File> subFiles = getFiles(subFile);
            files.addAll(subFiles);
          } else {
            files.add(subFile);
        return files;
    
    @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
    public class GeneratorApplication {
       * 测试的时候添加参数 -h 127.0.0.1 -P 3306 -d db_tl_sale -u root -p 123456 -m sale -g base -t
       * t_xs_sale_contract,t_xs_sale_contract_detail
       * @param args
      public static void main(String[] args) {
        SpringApplication.run(GeneratorApplication.class, args);
    

    定义的模板文件如下图所示:

    除了上述代码,还有一些工具类,以及公共组件,这里就不一一介绍了,我会在下篇文章中详细介绍这些基础代码,大致内容如下图所示:

    包括请求日志记录、返回对象封装、全局异常捕获等等。

    项目整体目录结构如下所示:

    启动项目后,直接访问 http://localhost:8525/swagger-ui.html#/。

    传入参数根据个人需要按照如下格式整理信息:

    "database": "db_tl_sale", "flat": true, "type": "mybatis", "group": "base", "host": "127.0.0.1", "module": "sale", "password": "123456", "port": 3306, "table": [ "t_xs_sale_contract" "username": "root", "tableStartIndex":"5"

    type 属性可以设置为 common、mybatis、mybatisplus、jpa,后三个属性值对应不同的 orm 框架。

    然后点击执行,执行成功后点击下载,将生成好的代码下载到本地。文件结构如下图所示:

    这里截取一部分代码图片,首先是实体类:

    然后是查询实体类:

    接着是 Service 接口:

    以及对应的实现类:

    最后是 controller:

    一对多关联查询

    resultMap 元素是 MyBatis 中最重要最强大的元素。它可以让你从 90% 的 JDBC ResultSets 数据提取代码中解放出来,并在一些情形下允许你进行一些 JDBC 不支持的操作。实际上,在为一些比如连接的复杂语句编写映射代码的时候,一份 resultMap 能够代替实现同等功能的数千行代码。ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。

    目前订单类详情查询返回的结果中,除了包含订单类的全部信息,还需要返回多个订单子项的数据,也就是我们常说的一对多关系,那么在实际开发中如何操作呢?

    首先我们看一下代码案例:

    1、订单类

    public class OmsOrder implements Serializable {
        private static final long serialVersionUID = 1L;
        @ApiModelProperty(value = "订单id")
        private Long id;
        private Long memberId;
        private Long couponId;
        @ApiModelProperty(value = "订单编号")
        private String orderSn;
       ........
    

    2、订单子项类

    public class OmsOrderItem implements Serializable {
    	private static final long serialVersionUID = 1L;
        private Long id;
        @ApiModelProperty(value = "订单id")
        private Long orderId;
        @ApiModelProperty(value = "订单编号")
        private String orderSn;
        private Long productId;
      	........
    

    3、前端返回类

    public class OmsOrderDetail extends OmsOrder {
        @Getter
        @Setter
        @ApiModelProperty("订单商品列表")
        private List<OmsOrderItem> orderItemList;
    

    4、OmsOrderMapper.xml 文件中自定义 SQL 语句

    <resultMap id="orderDetailResultMap" type="com.macro.mall.dto.OmsOrderDetail" extends="com.macro.mall.mapper.OmsOrderMapper.BaseResultMap">
        <collection property="orderItemList" resultMap="com.macro.mall.mapper.OmsOrderItemMapper.BaseResultMap" columnPrefix="item_"/>
    </resultMap>
    <select id="getDetail" resultMap="orderDetailResultMap">
        SELECT o.*,
        oi.id item_id,
        oi.product_id item_product_id,
        oi.product_sn item_product_sn,
        oi.product_pic item_product_pic,
        oi.product_name item_product_name,
        oi.product_brand item_product_brand,
        oi.product_price item_product_price,
        oi.product_quantity item_product_quantity,
        oi.product_attr item_product_attr
        oms_order o
        LEFT JOIN oms_order_item oi ON o.id = oi.order_id
        WHERE
        o.id = #{id}
        ORDER BY oi.id ASC DESC
    </select>
    

    其中 com.macro.mall.mapper.OmsOrderItemMapper.BaseResultMap 是引用自 OmsOrderItemMapper.xml 文件中的定义,

    <resultMap id="BaseResultMap" type="com.macro.mall.model.OmsOrderItem">
        <id column="id" property="id" />
        <result column="order_id"  property="orderId" />
        <result column="order_sn" property="orderSn" />
        <result column="product_id" jdbcType="BIGINT" property="productId" />
        <result column="product_pic" jdbcType="VARCHAR" property="productPic" />
        <result column="product_name" jdbcType="VARCHAR" property="productName" />
        <result column="product_brand" jdbcType="VARCHAR" property="productBrand" />
        <result column="product_sn" jdbcType="VARCHAR" property="productSn" />
        <result column="product_price" jdbcType="DECIMAL" property="productPrice" />
        <result column="product_quantity" jdbcType="INTEGER" property="productQuantity" />
        <result column="product_sku_id" jdbcType="BIGINT" property="productSkuId" />
        <result column="product_sku_code" jdbcType="VARCHAR" property="productSkuCode" />
        <result column="product_category_id" jdbcType="BIGINT" property="productCategoryId" />
        <result column="promotion_name" jdbcType="VARCHAR" property="promotionName" />
        <result column="promotion_amount" jdbcType="DECIMAL" property="promotionAmount" />
        <result column="coupon_amount" jdbcType="DECIMAL" property="couponAmount" />
        <result column="integration_amount" jdbcType="DECIMAL" property="integrationAmount" />
        <result column="real_amount" jdbcType="DECIMAL" property="realAmount" />
        <result column="gift_integration" jdbcType="INTEGER" property="giftIntegration" />
        <result column="gift_growth" jdbcType="INTEGER" property="giftGrowth" />
        <result column="product_attr" jdbcType="VARCHAR" property="productAttr" />
    </resultMap>
    

    5、执行效果

    这种查询方式相较于先查主表,再根据主表字段关联查询子表信息,减少了 IO 连接查询次数,效率更高一些。

    resultMap模板生成

    通过上述代码我们可知,实现一对多关联查询的关键在于定义子项数据(多)的 resultMap 定义,既然我们通过代码生成器生成了基本的项目代码,那么是否可以生成 resultMap 呢?说干就干,代码如下:

    1、定义模板 ftl 文件

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="${package}.mapper.${pascalName}Mapper">
        <resultMap id="BaseResultMap" type="${package}.model.${pascalName}">
        <#list columns as column>
        <#if column.isPrimaryKey>
            <id column="${column.fieldName}" property="${column.camelName}" />
        <#else>
            <result column="${column.fieldName}" property="${column.camelName}" />
        </#list>
        </resultMap>
    </mapper>
    

    2、编写服务类 XmlGenerateService

    @Service
    public class XmlGenerateService extends BaseService {
        private static final Logger logger = LoggerFactory.getLogger(XmlGenerateService.class);
        @Autowired
        private FreemarkerService freemarkerService;
         * @param tableName 数据库表名
         * @param parameter 模块名
         * @param uuid
         * @throws Exception
        public void generate(String tableName, GenerateParameter parameter, String uuid) throws Exception {
            // 各模块包名,比如 com.msdn.sale 或 com.msdn.finance
            String packagePrefix = "com.msdn." + parameter.getModule();
            // 分组
            if (!StringUtils.isEmpty(parameter.getGroup())) {
                packagePrefix = packagePrefix + "." + parameter.getGroup();
            // 根据项目设计的表名获取到表名,比如表名叫做:t_sale_contract_detail
            // 现在表名截取起始索引该由参数配置
    //        int index = tableName.indexOf("_", 2);
            Integer index = new Integer(parameter.getTableStartIndex());
            // 驼峰命名,首字母小写,比如:contractDetail
            String camelName = StringUtils.underscoreToCamel(tableName.substring(index));
            Map<String, Object> dataModel = new HashMap<>();
            //获取表中字段的具体信息,包括字段名,字段类型,备注等,排除指定字段
            List<Column> columns = getColumns(tableName, parameter, null);
            Column primaryColumn = columns.stream().filter(Column::getIsPrimaryKey).findFirst().orElse(null);
            dataModel.put("package", packagePrefix);
            dataModel.put("camelName", camelName);
            // 首字母转大写,作为实体类名称等
            dataModel.put("pascalName", StringUtils.capitalize(camelName));
            dataModel.put("moduleName", parameter.getModule());
            dataModel.put("tableName", tableName);
            // 表描述
            dataModel.put("tableComment", getTableComment(tableName, parameter));
            dataModel.put("columns", columns);
            dataModel.put("primaryColumn", primaryColumn);
            dataModel.put("tempId", uuid);
            dataModel.put("author", Config.Author);
            dataModel.put("date", DateUtil.now());
            dataModel.put("type", parameter.getType());
            logger.info("准备生成模板代码的表名为:" + tableName + ",表描述为:" + dataModel.get("tableComment"));
            // 生成模板代码
            logger.info("**********开始生成Model模板文件**********");
            generateXML(dataModel, parameter);
         * 生成 controller 模板代码
         * @param dataModel
         * @param generateParameter
         * @throws Exception
        private void generateXML(Map<String, Object> dataModel, GenerateParameter generateParameter) throws Exception {
            String path = "resources" + File.separator + "xml" + File.separator + dataModel.get("pascalName") + "Mapper.xml";
            freemarkerService.write("mybatis-xml", dataModel, path, generateParameter);
    

    3、服务接口

    @PostMapping("/generator/buildXml")
    public void buildXml(@RequestBody GenerateParameter parameter, HttpServletResponse response) throws Exception {
        logger.info("**********欢迎使用基于FreeMarker的模板文件生成器**********");
        logger.info("************************************************************");
        String uuid = UUID.randomUUID().toString();
        for (String table : parameter.getTable()) {
            xmlGenerateService.generate(table, parameter, uuid);
        logger.info("**********模板文件生成完毕,准备下载**********");
        String path = Config.OutputPath + File.separator + uuid;
        //设置响应头控制浏览器的行为,这里我们下载zip
        response.setHeader("Content-disposition", "attachment; filename=code.zip");
        response.setHeader("Access-Control-Expose-Headers", "Content-disposition");
        // 将response中的输出流中的文件压缩成zip形式
        ZipDirectory(path, response.getOutputStream());
        // 递归删除目录
        FileSystemUtils.deleteRecursively(new File(path));
        logger.info("************************************************************");
        logger.info("**********模板文件下载完毕,谢谢使用**********");
    

    4、通过 swagger 调用 api

    5、执行结果

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.msdn.mall.mapper.OmsOrderItemMapper">
        <resultMap id="BaseResultMap" type="com.msdn.mall.model.OmsOrderItem">
            <id column="order_item_id" property="orderItemId" />
            <result column="order_id" property="orderId" />
            <result column="order_sn" property="orderSn" />
            <result column="product_id" property="productId" />
            <result column="product_pic" property="productPic" />
            <result column="product_name" property="productName" />
            <result column="product_brand" property="productBrand" />
            <result column="product_sn" property="productSn" />
            <result column="product_price" property="productPrice" />
            <result column="purchase_amount" property="purchaseAmount" />
            <result column="product_sku_id" property="productSkuId" />
            <result column="product_sku_code" property="productSkuCode" />
            <result column="product_category_id" property="productCategoryId" />
            <result column="sp1" property="sp1" />
            <result column="sp2" property="sp2" />
            <result column="sp3" property="sp3" />
            <result column="promotion_name" property="promotionName" />
            <result column="promotion_money" property="promotionMoney" />
            <result column="coupon_money" property="couponMoney" />
            <result column="integration_money" property="integrationMoney" />
            <result column="real_money" property="realMoney" />
            <result column="gift_integration" property="giftIntegration" />
            <result column="gift_growth" property="giftGrowth" />
            <result column="product_attr" property="productAttr" />
            <result column="is_deleted" property="isDeleted" />
            <result column="create_user_code" property="createUserCode" />
            <result column="create_user_name" property="createUserName" />
            <result column="create_date" property="createDate" />
            <result column="update_user_code" property="updateUserCode" />
            <result column="update_user_name" property="updateUserName" />
            <result column="update_date" property="updateDate" />
            <result column="version" property="version" />
        </resultMap>
    </mapper>
    

    在生产开发中如果还遇到好玩的东西,会不定期追加更新,希望工具越来越强大。如果有更好的建议,也可以在评论区@我。

    虽然 Mybatis 和 Mybatis Plus 都有相关的代码生成器配置,但是构建器代码不容易整合,外部调用也不方便,最主要的是无法满足实际需求。为了能够一次性生成所有代码,最终选择 SpringBoot 和 FreeMarker 来构建我们专属的代码生成器。

    除了可以生成 Java 相关代码,FreeMarker 还可以根据模板文件来生成前端代码,又或者是 Word 文档等,后续更多功能会根据情况逐步补充的。

    感兴趣的朋友可以去我的 Github 下载相关代码,如果对你有所帮助,不妨 Star 一下,谢谢大家支持!

    ZipOutputStream相关知识

    使用JCommander开发命令行交互(CLI)式JAVA程序

    分类:
    后端
    标签: