效果动态图

实体类一键生成DDL展示

场景

在大多情况,我们都是用数据库中的表生成我们用的实体类对象,但是有的时候,我们习惯使用UML或者拿到新的项目,只有实体类,没有DDL语句,那么这种情况下,我们除了使用三方工具,三方插件,其实利用简单的反射和注释提取就能轻轻松松完成实体类逆向生成建表语句这个功能。

环境

springboot+jdk8

实体类

package com.**.manager.domain.entity;

import lombok.Data;

import javax.persistence.*;
import java.util.Date;


/**
 * 运营收入实体类
 *
 * @author liaoqian
 * @since 2023/8/8
 */
@Data
@Table(name = "t_operating_income")
public class OperatingIncomeEntity {
    /**
     * 主键
     */
    @Id
    @Column(name = "operating_income_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer operatingIncomeId;
    /**
     * 收入日期
     */
    @Column(name = "income_date")
    private String incomeDate;
    /**
     * 组织id
     */
    @Column(name = "unit_id")
    private Integer unitId;
    /**
     * 组织编码
     */
    @Column(name = "unit_code")
    private String unitCode;
    /**
     * 组织类型
     * 1平台 2商户 3点位 4门店 5供应商 6品牌商 7未来商户 8仓储
     * 目前可用 247
     */
    @Column(name = "unit_type")
    private Integer unitType;
    /**
     * 组织名称
     */
    @Column(name = "unit_name")
    private String unitName;
    /**
     * 夜售编号
     */
    @Column(name = "yeso_code")
    private String yesoCode;
    /**
     * 区域id
     */
    @Column(name = "region_id")
    private Integer regionId;
    /**
     * 区域名
     */
    @Column(name = "region_name")
    private String regionName;
    /**
     * 订单数-货架 甲方
     */
    @Column(name = "order_num_hj_a")
    private Integer orderNumHJA;
    /**
     * 支付金额-货架 甲方
     */
    @Column(name = "order_amount_hj_a")
    private Integer orderAmountHJA;
    /**
     * 毛利-货架 甲方
     */
    @Column(name = "profit_amount_hj_a")
    private Integer profitAmountHJA;
    /**
     * 采购订单数-ToB商城 甲方
     */
    @Column(name = "order_num_sc_a")
    private Integer orderNumSCA;
    /**
     * 采购金额-ToB商城 甲方
     */
    @Column(name = "order_amount_sc_a")
    private Integer orderAmountSCA;
    /**
     * 毛利ToB商城 甲方
     */
    @Column(name = "profit_amount_sc_a")
    private Integer profitAmountSCA;
    /**
     * 其他收入-甲方
     */
    @Column(name = "other_income_a")
    private Integer otherIncomeA;
    /**
     * 汇总收入-甲方
     */
    @Column(name = "total_income_a")
    private Integer totalIncomeA;


    /**
     * 订单数-货架 乙方
     */
    @Column(name = "order_num_hj_b")
    private Integer orderNumHJB;
    /**
     * 支付金额-货架 乙方
     */
    @Column(name = "order_amount_hj_b")
    private Integer orderAmountHJB;
    /**
     * 毛利-货架 乙方
     */
    @Column(name = "profit_amount_hj_b")
    private Integer profitAmountHJB;
    /**
     * 采购订单数-ToB商城 乙方
     */
    @Column(name = "order_num_sc_b")
    private Integer orderNumSCB;
    /**
     * 采购金额-ToB商城 乙方
     */
    @Column(name = "order_amount_sc_b")
    private Integer orderAmountSCB;
    /**
     * 毛利ToB商城 乙方
     */
    @Column(name = "profit_amount_sc_b")
    private Integer profitAmountSCB;
    /**
     * 其他收入-乙方
     */
    @Column(name = "other_income_b")
    private Integer otherIncomeB;
    /**
     * 汇总收入-乙方
     */
    @Column(name = "total_income_b")
    private Integer totalIncomeB;

    /**
     * 创建人
     */
    @Column(name = "create_user_id")
    private String createUserId;
    /**
     * 创建时间
     */
    @Column(name = "create_time")
    private Date createTime;
    /**
     * 最后修改人
     */
    @Column(name = "last_update_user_id")
    private String lastUpdateUserId;
    /**
     * 最后修改时间
     */
    @Column(name = "last_update_time")
    private Date lastUpdateTime;
    /**
     * 是否删除
     * 0否1是
     */
    @Column(name = "is_deleted")
    private Integer isDeleted;
    /**
     * 来源
     * 0手动 default
     * 1自动
     */
    @Column(name = "source")
    private Integer source;
    /**
     * 来源数据源多少个
     * 0 default
     */
    @Column(name = "source_count")
    private Integer sourceCount;
}

工具类+测试

package com.**.manager.test;

import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.Tree;
import com.sun.source.util.JavacTask;
import com.sun.source.util.TaskEvent;
import com.sun.source.util.TaskListener;
import com.sun.tools.javac.api.JavacTool;
import com.sun.tools.javac.parser.Tokens;
import com.sun.tools.javac.tree.DocCommentTable;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import com.yeso.manager.domain.entity.OperatingIncomeEntity;

import javax.persistence.*;
import javax.tools.SimpleJavaFileObject;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;

/**
 * 实体类到数据库表的测试
 *
 * @author liaoqian
 * @since 2023/8/9
 */
public class EntityToDDLTest {

    public static void main(String[] args) throws IOException {
        String table = createTable(OperatingIncomeEntity.class);
        System.out.println(table);
    }

    public static String createTable(Class clazz) throws IOException {
        StringBuilder sql = new StringBuilder("CREATE TABLE IF NOT EXISTS ");
        StringBuilder pkid = new StringBuilder();
        Map<String, String> map = filedComment(clazz);
        Table tableAnn = (Table) clazz.getAnnotation(Table.class);
        sql.append(String.format("`%s`", tableAnn.name())).append("\n").append(" (");

        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            Column columnAnn = (Column) field.getAnnotation(Column.class);
            Id idAnn = (Id) field.getAnnotation(Id.class);
            String name = field.getName();
            String fieldName = columnAnn.name();
            String fieldType = field.getType().getSimpleName();
            int length = columnAnn.length();
            int scale = columnAnn.scale();
            String columnDefinition = columnAnn.columnDefinition();
            sql.append(String.format("`%s`", fieldName)).append(" ").append(StringUtils.isBlank(columnDefinition) ? getColumnType(fieldType, length, scale) : columnDefinition);
            if (idAnn != null) {

                sql.append(" NOT NULL AUTO_INCREMENT ");

                pkid.append(String.format(" PRIMARY KEY (`%s`) ", fieldName));
            }
            if ("last_update_time".equals(fieldName)) {
                sql.append(" ON UPDATE CURRENT_TIMESTAMP");
            }
            String comment = map.getOrDefault(name, name);
            sql.append(String.format(" COMMENT '%s'", comment.trim()));
            sql.append(", ");
            sql.append("\n");
        }
        sql.append(pkid);
        sql.append(")");
        sql.append("\n");
        sql.append(" ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ");
        sql.append(";");
        return sql.toString();
    }

    private static String getColumnType(String fieldType, int length, int sacle) {
        switch (fieldType) {
            case "Integer":
                return String.format("INT(%d)", length >= 255 ? 13 : length);
            case "Long":
                return String.format("BIGINT(%d)", length >= 255 ? 13 : length);
            case "Date":
                return "DATETIME";
            case "BigDecimal":
                return String.format("DECIMAL(%d,%d)", length, sacle >= 5 ? 5 : sacle);
            case "String":
                return String.format("VARCHAR(%d)", length);
        }
        return "";
    }

    private static Map<String, String> filedComment(Class clazz) throws IOException {


        String name = clazz.getName();
        String path = name.replaceAll("\\.", "\\\\");

        // 1. 指定要提取注释的Java源文件路径
        String filePath = System.getProperty("user.dir") + "\\src\\main\\java\\" + path + ".java";

        // 2. 读取源文件内容
        String source = new String(Files.readAllBytes(new File(filePath).toPath()), StandardCharsets.UTF_8);

        // 3. 创建Java编译器上下文
        Context context = new Context();

        // 4. 获取Java编译器实例
        JavacTool javacTool = JavacTool.create();

        // 5. 创建Java编译任务
        JavacTask task = javacTool.getTask(null, null, null, null, null, List.of(new EntityToDDLUtil.JavaSourceFromString(filePath, source)), context);

        Map<String, String> map = new HashMap<>(1 << 6);
        // 6. 注册任务监听器
        task.addTaskListener(new TaskListener() {
            @Override
            public void started(TaskEvent taskEvent) {
            }

            @Override
            public void finished(TaskEvent taskEvent) {
                if (taskEvent.getKind() == TaskEvent.Kind.PARSE) {
                    CompilationUnitTree compilationUnit = taskEvent.getCompilationUnit();
                    if (compilationUnit == null) {
                        return;
                    }

                    // 7. 遍历AST树,提取注释
                    for (Tree tree : compilationUnit.getTypeDecls()) {
                        if (tree instanceof JCTree.JCClassDecl) {
                            JCTree.JCClassDecl classDecl = (JCTree.JCClassDecl) tree;
                            List<JCTree> members = classDecl.getMembers();
                            if (members != null && members.size() > 0) {
                                for (JCTree member : members) {
                                    if (!(member instanceof JCTree.JCVariableDecl)) {
                                        continue;
                                    }
                                    JCTree.JCVariableDecl variableDecl = (JCTree.JCVariableDecl) member;
                                    DocCommentTable docComments = ((JCTree.JCCompilationUnit) compilationUnit).docComments;
                                    Tokens.Comment comment = docComments.getComment(variableDecl);
                                    String commentStr = comment == null ? "\n" : comment.getText();
                                    commentStr = commentStr.replaceAll("\n", " ");
                                    map.put(variableDecl.getName().toString(), commentStr);
                                }
                            }
                        }
                    }
                }
            }
        });

        // 8. 执行Java编译任务
        task.call();
        return map;
    }

    private static class JavaSourceFromString extends SimpleJavaFileObject {
        private final String sourceCode;

        public JavaSourceFromString(String name, String sourceCode) {
            super(new File(name).toURI(), Kind.SOURCE);
            this.sourceCode = sourceCode;
        }

        @Override
        public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {
            return sourceCode;
        }
    }
}

运行效果

CREATE TABLE IF NOT EXISTS `t_operating_income` (`operating_income_id` INT NOT NULL AUTO_INCREMENT  COMMENT '主键', `income_date` VARCHAR(255) COMMENT '收入日期', `unit_id` INT COMMENT '组织id', `unit_code` VARCHAR(255) COMMENT '组织编码', `unit_type` INT COMMENT '组织类型  1平台 2商户 3点位 4门店 5供应商 6品牌商 7未来商户 8仓储  目前可用 247', `unit_name` VARCHAR(255) COMMENT '组织名称', `yeso_code` VARCHAR(255) COMMENT '夜售编号', `region_id` INT COMMENT '区域id', `region_name` VARCHAR(255) COMMENT '区域名', `order_num_hj_a` INT COMMENT '订单数-货架 甲方', `order_amount_hj_a` INT COMMENT '支付金额-货架 甲方', `profit_amount_hj_a` INT COMMENT '毛利-货架 甲方', `order_num_sc_a` INT COMMENT '采购订单数-ToB商城 甲方', `order_amount_sc_a` INT COMMENT '采购金额-ToB商城 甲方', `profit_amount_sc_a` INT COMMENT '毛利ToB商城 甲方', `other_income_a` INT COMMENT '其他收入-甲方', `total_income_a` INT COMMENT '汇总收入-甲方', `order_num_hj_b` INT COMMENT '订单数-货架 乙方', `order_amount_hj_b` INT COMMENT '支付金额-货架 乙方', `profit_amount_hj_b` INT COMMENT '毛利-货架 乙方', `order_num_sc_b` INT COMMENT '采购订单数-ToB商城 乙方', `order_amount_sc_b` INT COMMENT '采购金额-ToB商城 乙方', `profit_amount_sc_b` INT COMMENT '毛利ToB商城 乙方', `other_income_b` INT COMMENT '其他收入-乙方', `total_income_b` INT COMMENT '汇总收入-乙方', `create_user_id` VARCHAR(255) COMMENT '创建人', `create_time` DATETIME COMMENT '创建时间', `last_update_user_id` VARCHAR(255) COMMENT '最后修改人', `last_update_time` DATETIME ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间', `is_deleted` INT COMMENT '是否删除  0否1是', `source` INT COMMENT '来源  0手动 default  1自动', `source_count` INT COMMENT '来源数据源多少个  0 default',  PRIMARY KEY (`operating_income_id`) );

美化下DDL

CREATE TABLE
IF
	NOT EXISTS `t_operating_income` (
		`operating_income_id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
		`income_date` VARCHAR ( 255 ) COMMENT '收入日期',
		`unit_id` INT COMMENT '组织id',
		`unit_code` VARCHAR ( 255 ) COMMENT '组织编码',
		`unit_type` INT COMMENT '组织类型  1平台 2商户 3点位 4门店 5供应商 6品牌商 7未来商户 8仓储  目前可用 247',
		`unit_name` VARCHAR ( 255 ) COMMENT '组织名称',
		`yeso_code` VARCHAR ( 255 ) COMMENT '夜售编号',
		`region_id` INT COMMENT '区域id',
		`region_name` VARCHAR ( 255 ) COMMENT '区域名',
		`order_num_hj_a` INT COMMENT '订单数-货架 甲方',
		`order_amount_hj_a` INT COMMENT '支付金额-货架 甲方',
		`profit_amount_hj_a` INT COMMENT '毛利-货架 甲方',
		`order_num_sc_a` INT COMMENT '采购订单数-ToB商城 甲方',
		`order_amount_sc_a` INT COMMENT '采购金额-ToB商城 甲方',
		`profit_amount_sc_a` INT COMMENT '毛利ToB商城 甲方',
		`other_income_a` INT COMMENT '其他收入-甲方',
		`total_income_a` INT COMMENT '汇总收入-甲方',
		`order_num_hj_b` INT COMMENT '订单数-货架 乙方',
		`order_amount_hj_b` INT COMMENT '支付金额-货架 乙方',
		`profit_amount_hj_b` INT COMMENT '毛利-货架 乙方',
		`order_num_sc_b` INT COMMENT '采购订单数-ToB商城 乙方',
		`order_amount_sc_b` INT COMMENT '采购金额-ToB商城 乙方',
		`profit_amount_sc_b` INT COMMENT '毛利ToB商城 乙方',
		`other_income_b` INT COMMENT '其他收入-乙方',
		`total_income_b` INT COMMENT '汇总收入-乙方',
		`create_user_id` VARCHAR ( 255 ) COMMENT '创建人',
		`create_time` DATETIME COMMENT '创建时间',
		`last_update_user_id` VARCHAR ( 255 ) COMMENT '最后修改人',
		`last_update_time` DATETIME ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间',
		`is_deleted` INT COMMENT '是否删除  0否1是',
		`source` INT COMMENT '来源  0手动 default  1自动',
		`source_count` INT COMMENT '来源数据源多少个  0 default',
	PRIMARY KEY ( `operating_income_id` ) 
	);

原理

字段和注解的获取就是简单的反射,字段上的注释就是通过使用com.sun.tools.javac工具包获取原java文件字段上的注释封装成map,再解析完成的,所以是不是很简单耐用,非常的nice!

总结

反射很强大,一定要好好学习

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐