代码生成器核心思路

代码生成器相信大家都见过或者用过吧?最典型的就是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类等,大体思路一样。

完整代码已上传,点击下载

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值