天天看點

基于SpringBoot的代碼自動生成基于SpringBoot的代碼自動生成

基于SpringBoot的代碼自動生成

條目 條目說明
作者 田樂樂
版本号 v1.0
版本描述 新增

目錄

文章目錄

  • 基于SpringBoot的代碼自動生成
    • 目錄
    • 使用技術
    • 一、背景
    • 二、需求
    • 三、優勢
      • 3.1、規範開發
      • 3.2、提高開發效率
    • 四、設計思路
      • 4.1、資料庫連接配接
      • 4.2、多資料源問題
        • 4.2.1、基本資訊配置
        • 4.2.2、選擇配置
      • 4.3、逆向工程配置
        • 4.3.1、逆向實體類
        • 4.3.2、逆向規則配置
        • 4.3.3實體類資訊裝入模闆引擎
    • 五、總結
    • 六、擴充
      • 6.1表數量擴充
        • 雙表操作
        • 多表操作
      • 6.2測試擴充
      • 6.3前端頁面擴充
        • 6.3.1前端頁面擴充一
        • 6.3.2前端頁面擴充二

使用技術

架構:SpringBoot、Mybatis/Mybatis-plus

其他:lombok、druid、velocity

一、背景

1、項目中多人開發,使用JDK1.6,IDE編譯使用JDK1.5。一人使用1.6在項目上添加注解,部分使用1.5編譯報錯。

2、架構搭建,使用簡單工廠建立Service對象。開發項目代碼中存在不使用該架構單獨建立對象操作。

3、DAO實作類中,對資料庫表進行CRUD,使用Date替換HiveQL中相應的占位符,使用SimpleDateFormat對時間進行parse()操作,一定機率下會導緻并發問題。

當微服務項目啟動,使用JDK1.8解決了上述部分問題,但是有沒有能夠類似想方正平台簡化部分開發呢?

二、需求

基于以上背景,現需要一套解決方案進而規範開發,提高開發效率。

實作目标:spring/springboot/springCloud項目代碼自動生成。

三、優勢

3.1、規範開發

代碼自動生成帶來的第一個好處就是開發規範,本身因為考慮是否是編譯錯誤引起的不必要麻煩。

另外,使用JDK1.8,也算是從剛開始就解決了一些并發問題。比如在JDK1.8中java.time包新增一些類就對此種并發問題進行了解決——新日期時間API。

3.2、提高開發效率

3.2.1不再需要進行繁瑣的手動逆向工程

雖然使用插件可以手動進行逆向工程(資料庫表、字段生成對應的POJO、mapper;生成對應的DAO、Service及其實作、生成規則配置等),比如EasyCode。但是如果使用自定義代碼,生成更多對應産物,可以更加友善的完成開發,提高開發效率,比如生成單元測試的json檔案(為滿足所有DAO、Service單元測試100%的覆寫率和單元測試單元測試本身語句覆寫率的70%使用)、快速開發産品化需要新增的SQL(為滿足産品化新增頁面)等等。

四、設計思路

4.1、資料庫連接配接

基于SpringBoot的代碼自動生成基于SpringBoot的代碼自動生成

pom.xml部分内容如下(依賴未做删減):

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
    <mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version>
    <druid.version>1.0.28</druid.version>
    <commons.lang.version>2.6</commons.lang.version>
    <commons.io.version>2.5</commons.io.version>
    <commons.configuration.version>1.10</commons.configuration.version>
    <fastjson.version>1.2.45</fastjson.version>
    <velocity.version>1.7</velocity.version>
    <pagehelper.spring.boot.version>1.2.5</pagehelper.spring.boot.version>
    <mysql.version>5.1.38</mysql.version>
    <mssql.version>4.0</mssql.version>
    <oracle.version>11.2.0.3</oracle.version>
</properties>

<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>${mybatis.spring.boot.version}</version>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>${druid.version}</version>
		</dependency>
		<dependency>
			<groupId>commons-lang</groupId>
			<artifactId>commons-lang</artifactId>
			<version>${commons.lang.version}</version>
		</dependency>
		<dependency>
			<groupId>commons-io</groupId>
			<artifactId>commons-io</artifactId>
			<version>${commons.io.version}</version>
		</dependency>
		<dependency>
			<groupId>commons-configuration</groupId>
			<artifactId>commons-configuration</artifactId>
			<version>${commons.configuration.version}</version>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>${fastjson.version}</version>
		</dependency>
		<dependency>
			<artifactId>velocity</artifactId>
			<groupId>org.apache.velocity</groupId>
			<version>${velocity.version}</version>
		</dependency>
		<dependency>
			<groupId>com.github.pagehelper</groupId>
			<artifactId>pagehelper-spring-boot-starter</artifactId>
			<version>${pagehelper.spring.boot.version}</version>
		</dependency>
		<!-- mysql驅動 -->
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>${mysql.version}</version>
		</dependency>
		<!-- oracle驅動 -->
		<dependency>
			<groupId>com.oracle</groupId>
			<artifactId>ojdbc6</artifactId>
			<version>${oracle.version}</version>
		</dependency>
		<!-- sqlserver驅動 -->
		<dependency>
			<groupId>com.microsoft.sqlserver</groupId>
			<artifactId>sqljdbc4</artifactId>
			<version>${mssql.version}</version>
		</dependency>
		<!-- postgresql驅動 -->
		<dependency>
			<groupId>org.postgresql</groupId>
			<artifactId>postgresql</artifactId>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
	</dependencies>
           

4.2、多資料源問題

所有逆向工程需要需要解決多資料源連接配接問題

比如:Oracle、MySQL

在yml配置如下:

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    #MySQL配置
    driverClassName: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/dbname?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    username: root
    password: root
      #oracle配置
#      driverClassName: oracle.jdbc.OracleDriver
#      url: jdbc:oracle:thin:@172.168.1.5:1521:orcl
#      username: prtdev
#      password: prtdevpw
    #SQLServer配置
    #    driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
    #    url: jdbc:sqlserver://192.168.10.10:1433;DatabaseName=dbname
    #    username: sa
    #    password: 123456
    #PostgreSQL配置
  #    driverClassName: org.postgresql.Driver
  #    url: jdbc:postgresql://192.168.10.10:5432/
  #    username: postgres
  #    password: 123456
  
  #指定資料庫,可選值有【mysql、oracle、sqlserver、postgresql】
tianll:
  database: mysql
           

說明:首先在application.yml中進行資料庫配置,不使用yml配置選擇properties進行配置效果想同。

4.2.1、基本資訊配置

spring:datasource:下進行多資料源的配置,正在使用的資料源需要解除注解并提到所有資料源配置最上方,否則SpringBoot項目啟動失敗。

4.2.2、選擇配置

tianll:database:下可選擇不同資料庫配置,可自行配置,在引用的時候标記處使用哪一種資料源,友善采用不同的連接配接工具。如下:

在config包中DBConfig中使用變量接收yml配置資訊、選擇不同資料源、使用不同的資料源DAO連接配接。(在代碼中用注解記性描述)

@Configuration
public class DbConfig {
    /**
     *database 用于接收yml改标簽的配置
     */
    @Value("${tianll.database: mysql}")
    private String database;
    
    /**
     * 使用不同的資料源DAO進行選擇
     * 所有的具體GeneratorDao都繼承GeneratorDao,
     *使用不同的sql腳本配置實作對所有表結構資訊的查詢
     */
    @Bean
    @Primary
    public GeneratorDao getGeneratorDao(){
        if("mysql".equalsIgnoreCase(database)){
            return mySQLGeneratorDao;
        }else if("oracle".equalsIgnoreCase(database)){
            return oracleGeneratorDao;
        }else if("sqlserver".equalsIgnoreCase(database)){
            return sqlServerGeneratorDao;
        }else if("postgresql".equalsIgnoreCase(database)){
            return postgreSQLGeneratorDao;
        }else {
            throw new RRException("不支援目前資料庫:" + database);
        }
    }
           

mysql對表結構資訊查詢如下(mySQLGeneratorDao對應的mySQLGeneratorDao.xml):

<mapper namespace="對應包名+類名">
	<select id="queryList" resultType="map">
		select table_name tableName, engine, table_comment tableComment, create_time createTime from information_schema.tables
		where table_schema = (select database())
		<if test="tableName != null and tableName.trim() != ''">
			and table_name like concat('%', #{tableName}, '%')
		</if>
		order by create_time desc
	</select>

	<select id="queryTable" resultType="map">
		select table_name tableName, engine, table_comment tableComment, create_time createTime from information_schema.tables
			where table_schema = (select database()) and table_name = #{tableName}
	</select>

	<select id="queryColumns" resultType="map">
		select column_name columnName, data_type dataType, column_comment columnComment, column_key columnKey, extra from information_schema.columns
 			where table_name = #{tableName} and table_schema = (select database()) order by ordinal_position
	</select>
</mapper>
           

4.3、逆向工程配置

4.2中已經通過SQL擷取到所有的表資訊和表的列資訊,此時需要使用實體列進行包裝。

4.3.1、逆向實體類

定義兩個entity,一個表實體類Table,一個欄位實體類Column.

@Data
public class Table {
		/**
		 * 表的名稱
		 */
		private String tableName;
		/**
		 * 表的備注
		 */
		private String comments;
		/**
		 * 表的主鍵
		 */
		private ColumnEntity pk;
		/**
		 * 表的列名(不包含主鍵)
		 */
		private List<ColumnEntity> columns;
		/**
		 * 類名(第一個字母大寫),如:sys_user => SysUser
		 */
		private String className;
		/**
		 * 類名(第一個字母小寫),如:sys_user => sysUser
		 */
		private String classname;
}
           
@Data
public class Column {
	/**
	 * 列名
	 */
    private String columnName;
	/**
	 * 列名類型
	 */
	private String dataType;
	/**
	 * 列名備注
	 */
	private String comments;
	/**
	 * 屬性名稱(第一個字母大寫),如:user_name => UserName
	 */
	private String attrName;
	/**
	 * 屬性名稱(第一個字母小寫),如:user_name => userName
	 */
	private String attrname;
	/**
	 * 屬性類型
	 */
	private String attrType;
	/**
	 * auto_increment
	 */
	private String extra;
}
           

4.3.2、逆向規則配置

自己建立一個properties進行規則配置,主要負責資料庫表欄位類型逆向成實體類對應的屬性的類型。

tinyint=Integer
smallint=Integer
mediumint=Integer
int=Integer
integer=Integer
bigint=Long
float=Float
double=Double
decimal=BigDecimal
bit=Boolean

char=String
varchar=String
tinytext=String
text=String
mediumtext=String
longtext=String
....=....
           

因為篇幅和時間精力問題,這裡就不在一一列舉了。

4.3.3實體類資訊裝入模闆引擎

一切準備工作都已經完成,最後使用模闆生成需要的代碼

//templates為擷取本地Resource下所有VM模闆 
List<String> templates = getTemplates();
for (String template : templates) {
    //渲染模闆
    StringWriter sw = new StringWriter();
    Template tpl = Velocity.getTemplate(template, "UTF-8" );
    tpl.merge(context, sw);
}
           

vm模闆舉例如下(contorller):

package ${package}.${moduleName}.controller;

import java.util.Arrays;
import java.util.Map;

import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import ${package}.${moduleName}.entity.${className}Entity;
import ${package}.${moduleName}.service.${className}Service;
import ${mainPath}.common.utils.PageUtils;
import ${mainPath}.common.utils.R;



/**
 * ${comments}
 *
 * @author ${author}
 * @email ${email}
 * @date ${datetime}
 */
@RestController
@RequestMapping("${moduleName}/${pathName}")
public class ${className}Controller {
    @Autowired
    private ${className}Service ${classname}Service;

    /**
     * 清單
     */
    @RequestMapping("/list")
    @RequiresPermissions("${moduleName}:${pathName}:list")
    public Result list(@RequestParam Map<String, Object> params){
        PageUtils page = ${classname}Service.queryPage(params);

        return Result.ok().put("page", page);
    }


    /**
     * 資訊
     */
    @RequestMapping("/info/{${pk.attrname}}")
    @RequiresPermissions("${moduleName}:${pathName}:info")
    public Result info(@PathVariable("${pk.attrname}") ${pk.attrType} ${pk.attrname}){
		${className}Entity ${classname} = ${classname}Service.getById(${pk.attrname});

        return Result.ok().put("${classname}", ${classname});
    }

    /**
     * 儲存
     */
    @RequestMapping("/save")
    @RequiresPermissions("${moduleName}:${pathName}:save")
    public Result save(@RequestBody ${className}Entity ${classname}){
		${classname}Service.save(${classname});

        return Result.ok();
    }

    /**
     * 修改
     */
    @RequestMapping("/update")
    @RequiresPermissions("${moduleName}:${pathName}:update")
    public Result update(@RequestBody ${className}Entity ${classname}){
		${classname}Service.updateById(${classname});

        return Result.ok();
    }

    /**
     * 删除
     */
    @RequestMapping("/delete")
    @RequiresPermissions("${moduleName}:${pathName}:delete")
    public Result delete(@RequestBody ${pk.attrType}[] ${pk.attrname}s){
		${classname}Service.removeByIds(Arrays.asList(${pk.attrname}s));

        return Result.ok();
    }

}

           

其中Result為自定義的關于REST風格的使用 Result 方式的傳回(封裝“錯誤碼”、“錯誤資訊”、資料總數、資料T或者T清單)。

五、總結

從第四章的講述中可以看出,如果使用此方法。所有的單表需求已經完全可以用代碼生成完成。單表的情況下,完全可以做到:

1、可移植性。針對不同版本不同架構,僅僅需要添加不同架構所需要的vm模闆,做到一次添加到處使用。

2、目标多元性——滿足不同客戶群體資料庫需求。針對客戶資料庫選擇不同資料源進行對應資料逆向和SQL定制開發(完全寫在vm中)。

3、目标多元性——滿足不同群體架構使用需求。隻要VM包含,可以使用不同的架構選擇、比如spring或者springboot,比如mybatis、hibernate或者SpringJPA。

當然,如在JDK版本不同的情況下,比如模闆生成中都是JDK1.8新特性代碼,放到1.6的項目中肯定是不行的。使用1.6代碼的模闆即可解決。

綜上,建議如下:

模闆檔案命名建議如下:

項目名.功能名.檔案類型.vm

因為從項目名即可判斷使用JDK版本,是以不再需要重複表述。

比如:

“Taiping.Dao.java.vm"适用于JDK1.6太平傳遞平台DAO層Interface

“Taiping.Dao.xml.vm”适用于JDK1.6太平傳遞平台DAO層對應的xml實作(包含針對不同資料庫需要的單表SQL)

“Taiping.Dao.java.vm"适用于JDK1.8太平傳遞平台一體化DAO層Interface

“Taiping.Dao.xml.vm”适用于JDK1.8太平傳遞平台一體化DAO層對應的xml實作(包含針對不同資料庫需要的單表SQL)

另外,原本的代碼開發工作基本上變成了模闆開發工作。

六、擴充

以上僅針對但表操作,實際項目中雙表多表關聯查連接配接查如何解決?

因為如今隻實作了單表操作,雙表多表的提供意見參考。

6.1表數量擴充

雙表操作

vm中同樣可以增加雙表模闆,例如:

select * from 
batchinfo b,batchwork bw 
where 
b.batch_id=bw.batch_id 
order by b.id;

select * from 
batchinfo b left join batchwork bw 
on 
b.batch_id=bw.batch_id 
order by b.id;

           

此時隻需要在vm模闆中進行關聯,基于上述代碼,容易實作的是根據不同欄位進行關聯查詢。但是實際項目中,資料庫表欄位很多,關聯附屬表的某一欄位也不确定,雖然可以實作,但基本上實作周期比較長(vm模闆修改也需要一定的學習時間),多表關聯查直接正常開發比代碼生成準備工作快很多,是以基本實作的有效性不高。

多表操作

由于雙表操作已經存在很大不确定性,多表複雜度更高,不建議使用。

6.2測試擴充

針對SpringBoot或者基于SpringBoot的微服務項目,因測試子產品最好覆寫項目DAO、Servcie層所有方法,并需要滿足自動化、獨立性、可重複。則代碼生成測試資料等是否更加有利?答案是肯定的。

比如:所有含有@RestController标簽的controller類所需要的測試json和方法傳回的json是否一緻,這時候,代碼生成測試json會帶來很大友善。

此時,隻需要在json模闆中循環出需要的資料庫欄位并替換即可。

6.3前端頁面擴充

雖然可以進行前端頁面的擴充,但是和實際開發需求會有很大出入,不建議使用。但是此處提供下實作方法,vm模闆如下:

6.3.1前端頁面擴充一

#foreach($column in $columns)
      <el-table-column
        prop="${column.attrname}"
        header-align="center"
        align="center"
        label="${column.comments}">
      </el-table-column>
#end
<script>
    delete (id) {
        var ids = id ? [id] : this.dataListSelections.map(item => {
          return item.${pk.attrname}
        });
    }
</script>

           

6.3.2前端頁面擴充二

當有些項目的菜單menu通過資料庫查詢進行顯示。每次新增一個功能需要新增若幹個DML語言。此部分如沒實作管理者配置設定出頁面或者管理者配置設定不能完全滿足的情況下,可通過定制vm完成。

比如,某一系統通過對SYS_MENU表查詢父子id顯示菜單。則可通過vm配置新增SQL

--父菜單
INSERT INTO `sys_menu` (`parent_id`, `name`, `url`, `perms`, `type`, `icon`, `order_num`)
    VALUES ('1', '${comments}', '${moduleName}/${pathName}', NULL, '1', 'config', '6');
--子菜單
INSERT INTO `sys_menu` (`parent_id`, `name`, `url`, `perms`, `type`, `icon`, `order_num`)
    SELECT @parentId, '檢視', null, '${moduleName}:${pathName}:list,${moduleName}:${pathName}:info', '2', null, '6';