代碼生成器核心思路

代碼生成器相信大家都見過或者用過吧?最典型的就是MyBatis那個生成實體類和Mapper文件的工具。顯然,代碼自動生成可以減少一些重複代碼的編寫,從而提高開發效率。

代碼生成工具主要做法就是使用模板引擎,把公共的部分抽取出來形成一個模板,對於變化的一些數據暫時使用佔位符標記。到了生成具體代碼的時候,就把佔位符替換成實際需要的內容。

下面就做一個根據MySQL數據庫表自動生成實體類的小案例,希望達到拋磚引玉的效果。

1、首先我們在MySQL數據庫建立一張表,建表SQL如下

CREATE TABLE `student` (
  `id` bigint(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(100) NOT NULL DEFAULT '' COMMENT '姓名',
  `code` varchar(100) NOT NULL DEFAULT '' COMMENT '編號',
  `sex` tinyint(1) NOT NULL DEFAULT '0' COMMENT '性別:1-男,2-女',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  `is_deleted` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是否刪除:1-是,0否',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE KEY `uk_code` (`code`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='學生表';

2、然後我們新建一個maven項目,引入MySQL的驅動jar包和模板引擎Velocity的相關jar包。爲了代碼簡潔,引入Lombok工具。

3、在項目裏面,創建一個模板文件Entity.java.vm,存放在resources的template目錄下,內容如下

類的註釋、名稱和字段的註釋、類型、名稱都是從數據庫表讀取並轉換得來的,其它的包名、作者可以寫在配置文件裏面。

4、創建一個代表實體類信息的Java類,用於保存從數據庫讀取一個表並轉化的信息。

這裏的外部類記錄表對應的類信息,內部類記錄列對應的字段信息。

/**
 * 實體類信息
 *
 * @author z_hh
 * @date 2019/6/25
 */
@Data
public class EntityInfo {
    /**
     * 類名
     */
    private String className;
    /**
     * 註釋
     */
    private String classComment;
    /**
     * 屬性集合
     */
    private List<FieldInfo> filedInfoList;

    /**
     * 字段信息
     *
     * @author z_hh
     * @date 2019/6/25
     */
    @Data
    public static class FieldInfo {
        /**
         * 字段名稱
         */
        private String fieldName;
        /**
         * 註釋
         */
        private String fieldComment;
        /**
         * 字段類型
         */
        private String fieldType;
    }
}

5、從數據庫讀取表的信息,轉換爲表示實體類信息的Java類對象。

這裏使用最簡單的JDBC了。如果是其它數據庫,查詢的SQL和獲取表列信息的方式就需要變一下了。Oracle的話我之前寫的博客有現成的代碼,可以去看一下:https://blog.csdn.net/qq_31142553/article/details/81256516

/**
 * 數據訪問對象
 *
 * @author z_hh
 * @date 2019/6/25
 */
public class Dao {

    public static EntityInfo getEntityInfoByTableName(String tableName) throws Exception {
        Connection connection = DriverManager.getConnection(Const.CONFIG.getString("database.url"),
                Const.CONFIG.getString("database.username"),
                Const.CONFIG.getString("database.password"));

        // 表信息
        PreparedStatement preparedStatement = connection.prepareStatement(Const.QUERY_TABLE_SQL);
        preparedStatement.setString(1, tableName);
        ResultSet resultSet = preparedStatement.executeQuery();
        EntityInfo entityInfo = new EntityInfo();
        while (resultSet.next()) {
            entityInfo.setClassName(StrConvertUtils.underlineCase2CamelCase(tableName, true));
            entityInfo.setClassComment(resultSet.getString("table_comment"));
        }

        // 列信息
        preparedStatement = connection.prepareStatement(Const.QUERY_COLUMN_SQL);
        preparedStatement.setString(1, tableName);
        resultSet = preparedStatement.executeQuery();
        List<EntityInfo.FieldInfo> fieldInfoList = new ArrayList<>();
        while (resultSet.next()) {
            EntityInfo.FieldInfo fieldInfo = new EntityInfo.FieldInfo();
            // 字段名轉爲屬性名
            String columnName = resultSet.getString("column_name");
            fieldInfo.setFieldName(StrConvertUtils.underlineCase2CamelCase(columnName, false));
            // 列類型轉爲Java類型
            String tbType = resultSet.getString("data_type");
            fieldInfo.setFieldType(StrConvertUtils.dbType2JavaType(tbType));
            // 註釋
            fieldInfo.setFieldComment(resultSet.getString("column_comment"));
            fieldInfoList.add(fieldInfo);
        }

        entityInfo.setFiledInfoList(fieldInfoList);

        return entityInfo;
    }

這裏面用到的兩個工具類方法如下

/**
 * 字符串轉換工具
 *
 * @author z_hh
 * @date 2019/6/25
 */
public class StrConvertUtils {

    /**
     * 下滑線改爲駝峯式
     *
     * @param str 下劃線字符串
     * @param upperFirst 首字母是否大寫
     * @return 駝峯式字符串
     */
    public static String underlineCase2CamelCase(CharSequence str, boolean upperFirst) {
        if (Objects.isNull(str)) {
            return null;
        }
        // 下滑線改爲駝峯式
        String str2 = str.toString();
        if (str2.contains("_")) {
            str2 = str2.toLowerCase();
            StringBuilder sb = new StringBuilder(str2.length());
            boolean upperCase = false;

            for (int i = 0; i < str2.length(); ++i) {
                char c = str2.charAt(i);
                if (c == '_') {
                    upperCase = true;
                } else if (upperCase) {
                    sb.append(Character.toUpperCase(c));
                    upperCase = false;
                } else {
                    sb.append(c);
                }
            }
            str2 = sb.toString();
        }
        // 首字母大寫
        if (upperFirst && str2.length() > 0) {
            char firstChar = str2.charAt(0);
            if (Character.isLowerCase(firstChar)) {
                return Character.toUpperCase(firstChar) + str2.substring(1);
            }
        }

        return str2;
    }

    /**
     * 數據庫類型轉爲Java類型
     *
     * @param dbType 數據庫的列類型
     * @return Java類型
     */
    public static String dbType2JavaType(String dbType) {
        return Optional.ofNullable(Const.TYPE_CONVERT.getString(dbType)).orElse("UknowType");
    }
}

其中數據庫連接信息、查詢的SQL寫到了常量類裏面。然後數據庫連接信息又是從配置文件讀取的,這裏使用了ResourceBundle工具,注意裏面填的配置文件名是省略後綴的。

/**
 * 常量
 *
 * @author z_hh
 * @date 2019/6/25
 */
public interface Const {
    String QUERY_TABLE_SQL = "select table_name, table_comment from information_schema.tables"
            + " where table_schema = (select database()) and table_name = ?";

    String QUERY_COLUMN_SQL = "select column_name, data_type, column_comment, column_key, extra from information_schema.columns"
            + " where table_schema = (select database()) and table_name = ?";

    ResourceBundle CONFIG = ResourceBundle.getBundle("config");

    ResourceBundle TYPE_CONVERT = ResourceBundle.getBundle("type-convert");
}

對應的配置文件放在resources根目錄下,其中config.properties的內容如下,後面的三個配置是生成代碼時用到的。

#數據庫地址
database.url=jdbc:mysql://xxx000.com:9527/demo?useUnicode=true&characterEncoding=UTF-8&useSSL=false
#數據庫用戶
database.username=admin
#數據庫密碼
database.password=admin
#路徑
basePath=C:/Users/dell/Desktop/java
#包名
packageName=cn.zhh.entity
#作者
author=ZhouHH

另一個配置文件type-convert.properties寫的是數據庫的數據類型和Java裏面的數據類型之間的對應關係,這裏先看一下吧

#類型轉換,配置信息
tinyint=Byte
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
date=LocalDateTime
datetime=Date
timestamp=Date

6、得到了表示實體類信息的Java類對象,就可以渲染模板了。最後將內容寫入文件並保存到指定位置。

注意列columns是集合遍歷。

/**
 * 業務處理對象
 *
 * @author z_hh
 * @date 2019/6/25
 */
public class MainService {

    public static void generateCode(String tableName) throws Exception {
        // 設置模板填充數據
        System.out.println("正在查詢表信息:" + tableName);
        EntityInfo entityInfo = Dao.getEntityInfoByTableName(tableName);
        Map contextMap = new HashMap();
        contextMap.put("packageName", Const.CONFIG.getString("packageName"));
        contextMap.put("classComment", entityInfo.getClassComment());
        contextMap.put("author", Const.CONFIG.getString("author"));
        contextMap.put("datetime", LocalDateTime.now().format(DateTimeFormatter.BASIC_ISO_DATE));
        contextMap.put("className", entityInfo.getClassName());
        contextMap.put("packageName", Const.CONFIG.getString("packageName"));
        contextMap.put("columns", entityInfo.getFiledInfoList());

        // 渲染模板
        System.out.println("正在渲染模板...");
        Properties p = new Properties();
        p.put("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
        Velocity.init(p);
        String template = "template/Entity.java.vm";
        VelocityContext velocityContext = new VelocityContext(contextMap);
        StringWriter sw = new StringWriter();
        Template tpl = Velocity.getTemplate(template);
        tpl.merge(velocityContext, sw);
        String context = sw.toString();

        // 生成文件
        System.out.println("正在生成文件...");
        Path filePath = Paths.get(Const.CONFIG.getString("basePath"), entityInfo.getClassName() + ".java");
        Files .write(filePath, context.getBytes());

        System.out.println("生成代碼成功,路徑->" + filePath.toString());
    }

}

7、最後對我們創建的那個表試一下

public class Test {

    public static void main(String[] args) throws Exception {
        MainService.generateCode("student");
    }
}

控制檯輸出 

 

文件效果展示

總結與反思

1、讀取列的信息可以更多,比如主鍵、是否爲空、長度、默認值等,有必要的話可以按需加上。

2、可以支持更多數據庫類型,但是查詢表和列的SQL會不一樣,可以使用策略模式靈活切換。

3、一般結合圖形界面使用會更加人性化,並且支持生成Service類、Dao類等,大體思路一樣。

完整代碼已上傳,點擊下載

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章