前言

在实际的项目开发中,根据数据库表创建实体、service、controller等结构是一件非常繁琐的事。所以我们经常需要使用到各种代码生成器,例如mybatis-plus,若依等框架都有自己的代码生成器和生成逻辑。本篇文章我们就从0开始,手写一个简单的代码生成器。 源码github地址

项目依赖

本项目是基于springboot+javapoet+dom4j实现的。

    <dependencies>
        <!-- DOM4J依赖 -->
        <dependency>
            <groupId>dom4j</groupId>
            <artifactId>dom4j</artifactId>
            <version>1.6.1</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/com.squareup/javapoet -->
        <dependency>
            <groupId>com.squareup</groupId>
            <artifactId>javapoet</artifactId>
            <version>1.13.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>
        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>2.2.0</version>
        </dependency>
        <!-- jdbc -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jdbc</artifactId>
            <version>2.1.7.RELEASE</version>
        </dependency>
        <!-- jdbc -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.6.8</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.0</version>
        </dependency>
    </dependencies>

整体思路

代码生成的逻辑主要是基于以下两个sql语句实现的:

show tables //查询所有数据库表
SHOW FULL COLUMNS FROM User" // 查询指定表中的所有字段信息

查询出来的表字段信息如下图所示:
在这里插入图片描述
我们遍历这些表,根据每个表生成对应的实体类,再根据这些表的字段信息生成不同的属性和变量。整体架构图如下:
在这里插入图片描述

核心类

在主程序使用代码生成器的时候将Datasource传入给Generate 类,Generate 对象根据数据源使用jdbcTemplate查询数据库表信息。并根据配置设置各项需求(我没有写,例如是否使用lombok、controllerPath等设置,读者可以自己尝试完成)

/**
 * 生成器最核心的类,保存了实体类、service、mapper的输出路径以及数据库的链接信息
 * @author 黎勇炫
 * @date 2022年08月19日 11:17
 */
@Data
@Accessors(chain = true)
public class Generate {

    private static final String SHOWTABLES = "show tables";

    /** jdbc */
    private JdbcTemplate jdbcTemplate;
    /** 实体类 */
    private String domainPath;
    /** service */
    private String servicePath;
    /** dao */
    private String daoPath;
    /** controller */
    private String controllerPath;
    /** 是否使用lombok */
    private boolean lombok;
    /** 所有表名 */
    private List<String> table;
    /** 表前缀 */
    private String tablePrefix;
     /**
       * 包路径(entity,controller,service包的父包)
       */
    private String pkgRootPath;
     /**
       * 资源路径
       */
    private String sourcePath;

    private BuilderChain chain;

    public Generate(DataSource dataSource,String pkgRootPath,String sourcePath) {
        this.pkgRootPath = pkgRootPath;
        this.sourcePath = sourcePath;
        jdbcTemplate = new JdbcTemplate(dataSource);
    }

    /**
       * 开始生成代码
       */
    public void doCreate(){
        // 1.在数据库中查询所有的表
        table = jdbcTemplate.queryForList(SHOWTABLES,String.class);
        // 处理表名
        table = table.stream().map(item -> {
            String entityName = buildEntityName(item);
            // 从新建实体类开始
            chain = createChain(jdbcTemplate,entityName,item,pkgRootPath,sourcePath);
            chain.build(jdbcTemplate, entityName, item, pkgRootPath, sourcePath);
            return entityName;
        }).collect(Collectors.toList());
    }

     /**
       * 创建生成类的调用链 实体类-dao-service-controller
       */
    private BuilderChain createChain(JdbcTemplate jdbcTemplate, String entityName, String item, String pkgRootPath, String sourcePath) {
        EntityBuilder chain = new EntityBuilder();
        chain.appendNext(new DaoBuilder())
                .appendNext(new ServiceBuilder())
                .appendNext(new ControllerBuilder());
        return chain;
    }

    /**
       * 构建实体类名称
       */
    private String buildEntityName(String name) {
        // 替换前缀
        if (!StringUtils.isEmpty(tablePrefix)) {
            name = name.replaceFirst(tablePrefix,"");
        }
        // 首字母大写
        // 驼峰命名
        String[] hump = name.split("_");
        StringBuilder builder = new StringBuilder();
        for (String s : hump) {
            builder.append(s.substring(0,1).toUpperCase()+s.substring(1));
        }

        return builder.toString();
    }

}

类构建者

因为实体类、Dao、Service、Controller之间有调用关系,例如Dao需要实体类作为泛型,Service又需要注入Dao层的类。所以我们需要保证整个创建者链的调用顺序,使用责任链模式实现。

/**
 * @author 黎勇炫
 * @date 2022年08月22日 16:03
 */
public class EntityBuilder extends BuilderChain{

     /**
       *
       */
    public static final String ENTITY_PKG_SUFFID = ".domain";

    @Override
    public BuilderChain appendNext(BuilderChain next){
        this.next = next;
        return next;
    }

    @Override
    public void build(JdbcTemplate jdbcTemplate, String entityName, String tableName, String pkgRootPath, String sourcePath) {
        // 查询表中所有的字段
        List<Columns> columns = jdbcTemplate.query("SHOW FULL COLUMNS FROM " + tableName, new BeanPropertyRowMapper<Columns>(Columns.class));
        // 开始创建实体类
        TypeSpec.Builder builder = TypeSpec.classBuilder(entityName)
                // 关联表
                .addAnnotation(AnnotationSpec.builder(TableName.class).addMember("value","\""+tableName+"\"").build())
                // lombok注解
                .addAnnotation(AnnotationSpec.builder(Data.class).build())
                // 修饰符
                .addModifiers(Modifier.PUBLIC)
                .addJavadoc(entityName+"\n@author 黎勇炫 \n@Date "+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        for (Columns column : columns) {
            System.out.println(column);
            FieldSpec.Builder fieldBuilder = FieldSpec.builder((TypeName) FieldRel.getJavaType(column.getType()), buildFieldName(column.getField()), Modifier.PRIVATE)
                    .addAnnotation(AnnotationSpec.builder(Schema.class).addMember("title", "\""+column.getComment()+"\"").build());
            // 如果是主键就加上@TableId
            if(!StringUtils.isEmpty(column.getKey())){
                fieldBuilder.addAnnotation(AnnotationSpec.builder(TableId.class).build());
            }
            builder.addField(fieldBuilder.build());
        }

        genJavaSourceFile(pkgRootPath+ENTITY_PKG_SUFFID,sourcePath,builder);
        // 创建实体类
        if(null != next){
            next.build(jdbcTemplate,entityName,tableName,pkgRootPath,sourcePath);
        }
    }

     /**
       * 转换变量名称-小写/驼峰
       */
    private String buildFieldName(String field) {
        field = field.toLowerCase();
        String[] segment = field.split("_");
        StringBuilder str = new StringBuilder();
        for (int i = 0; i < segment.length; i++) {
            if(i==0){
                str.append(segment[i]);
            }else {
                str.append(segment[i].substring(0,1).toUpperCase()+segment[i].substring(1));
            }
        }
        return str.toString();
    }
}
/**
 * @author 黎勇炫
 * @date 2022年08月23日 11:25
 */
public class DaoBuilder extends BuilderChain{

     /**
       * edit
       */
    public static final String DAO_SUFFIX = "Dao";
     /**
       * edit
       */
    public static final String DAO_PKG_SUFFIX = ".dao";

    @Override
    public void build(JdbcTemplate jdbcTemplate, String entityName, String tableName, String pkgRootPath, String sourcePath) {

        Class<BaseMapper> clazz = BaseMapper.class;

        // 创建接口
        TypeSpec.Builder builder = TypeSpec.interfaceBuilder(entityName + DAO_SUFFIX)
                .addAnnotation(Mapper.class)
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ParameterizedTypeName.get(ClassName.get(clazz), ClassName.get(pkgRootPath + EntityBuilder.ENTITY_PKG_SUFFID, entityName)))
                .addJavadoc(entityName+"\n@author 黎勇炫 \n@Date "+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));

        genJavaSourceFile(pkgRootPath+DAO_PKG_SUFFIX,sourcePath,builder);
        // 生成mapper文件
        createMapperFile(pkgRootPath+DAO_PKG_SUFFIX+"."+entityName,entityName+"Dao");
        if(null != this.next){
            this.next.build(jdbcTemplate,entityName,tableName,pkgRootPath,sourcePath);
        }
    }
}
/**
 * @author 黎勇炫
 * @date 2022年08月23日 15:37
 */
public class ServiceBuilder extends BuilderChain{

     /**
       * service后缀
       */
    public static final String SERVICE_SUFFIX = "Service";
    /**
     * serviceimpl后缀
     */
    public static final String SERVICEIMPL_SUFFIX = "ServiceImpl";
    /**
     * service包后缀
     */
    public static final String SERVICE_PKG_SUFFIX = ".service";
    /**
     * serviceimpl包后缀
     */
    public static final String SERVICEIMPL_PKG_SUFFIX = ".service.impl";


    @Override
    public void build(JdbcTemplate jdbcTemplate, String entityName, String tableName, String pkgRootPath, String sourcePath) {

        Class<IService> service = IService.class;
        Class<ServiceImpl> serviceImpl = ServiceImpl.class;
        // 创建inteface
        TypeSpec.Builder builder = TypeSpec.interfaceBuilder(entityName + SERVICE_SUFFIX)
                // 继承iservice接口
                .addSuperinterface(ParameterizedTypeName.get(ClassName.get(service), ClassName.get(pkgRootPath+EntityBuilder.ENTITY_PKG_SUFFID,entityName)))
                .addModifiers(Modifier.PUBLIC)
                .addJavadoc(entityName+"\n@author 黎勇炫 \n@Date "+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        // 生成service接口
        genJavaSourceFile(pkgRootPath+SERVICE_PKG_SUFFIX, sourcePath, builder);
        // 加载刚才生成的service接口,在生成实现类的时候继承这个接口
        // 创建impl
        TypeSpec.Builder impl = null;
        impl = TypeSpec.classBuilder(entityName + SERVICEIMPL_SUFFIX)
                .superclass(ParameterizedTypeName.get(ClassName.get(serviceImpl), ClassName.get(pkgRootPath+DaoBuilder.DAO_PKG_SUFFIX,entityName+DaoBuilder.DAO_SUFFIX), ClassName.get(pkgRootPath+EntityBuilder.ENTITY_PKG_SUFFID,entityName)))
                .addSuperinterface(ClassName.get(pkgRootPath+SERVICE_PKG_SUFFIX,entityName+SERVICE_SUFFIX))
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Service.class)
                .addJavadoc(entityName+"\n@author 黎勇炫 \n@Date "+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        // 生成实现类
        genJavaSourceFile(pkgRootPath+SERVICEIMPL_PKG_SUFFIX + ".impl", sourcePath, impl);

        // todo 生成mapper文件

        if(null != this.next){
            this.next.build(jdbcTemplate,entityName,tableName,pkgRootPath,sourcePath);
        }
    }
}
/**
 * 生成controller
 * @author 黎勇炫
 * @date 2022年08月24日 13:51
 */
public class ControllerBuilder extends BuilderChain{

    public static final String CONTROLLER_SUFFIX = "Controller";
    public static final String CONTROLLER_OKG_SUFFIX = ".controller";

    @Override
    public void build(JdbcTemplate jdbcTemplate, String entityName, String item, String pkgRootPath, String sourcePath) {

         /**
           * 1.先创建controller类,添加相关注解和权限标识
           * 2.创建属性,注入service类
           * 3.创建api接口
           */

        TypeSpec.Builder builder = TypeSpec.classBuilder(entityName + CONTROLLER_SUFFIX)
                // 添加@RestController注解
                .addAnnotation(AnnotationSpec.builder(RestController.class).build())
                // 添加RequestMapping注解
                .addAnnotation(AnnotationSpec.builder(RequestMapping.class).addMember("value", "\"/" + entityName.toLowerCase()+"\"").build())
                // 导入实体类
                // 权限修饰符
                .addModifiers(Modifier.PUBLIC);

        // 添加Service注入
        builder.addField(FieldSpec.builder(ClassName.get(pkgRootPath+ServiceBuilder.SERVICE_PKG_SUFFIX,entityName+ServiceBuilder.SERVICE_SUFFIX),entityName.substring(0,1).toLowerCase()+entityName.substring(1)+ServiceBuilder.SERVICE_SUFFIX)
                .addModifiers(Modifier.PRIVATE)
                .addAnnotation(AnnotationSpec.builder(Autowired.class).build())
                .build());

        genJavaSourceFile(pkgRootPath+CONTROLLER_OKG_SUFFIX,sourcePath,builder);
    }
}

测试代码生成器

到这里,实体类、Dao层、Service层以及Controller层的类基本都已经创建了。在需要代码生成的项目中导入代码生成器,在测试类中调用

    @Test
    void contextLoads() {
        DataSource dataSource = context.getBean(DataSource.class);
        Generate generate  = new Generate(dataSource,"gencode.demo","src/main/java");
        generate.setTablePrefix("l_")
                .setDaoPath("ss");
        generate.doCreate();
    }

测试前项目结构:
在这里插入图片描述
测试后项目结构:
在这里插入图片描述
简单的代码生成器已经完成了,还有很多功能因为我太懒了没开发,感兴趣的可以自己可以尝试拓展。这个代码生成器当然是不够完善的,主要是尝试理解代码生成器的设计思路。

Logo

快速构建 Web 应用程序

更多推荐