nbsaas-boot代码生成器设计原理

nbsaas-boot代码生成器是一种用于快速生成Spring Boot项目代码的工具。其设计原理主要包括以下几个方面:

1. 模板引擎:

代码生成器使用模板引擎定义和生成代码。模板引擎允许开发人员创建带有占位符的代码模板,这些占位符将在生成代码时被具体内容替代。nbsaas-boot的模板引擎使用的是FreeMarker。

{主工程}
{主工程}.template
{主工程}.template.hibernate  #hibernate代码模板目录  
{主工程}.template.jpa   #jpa代码模板目录 
{主工程}.template.mybatis-plus  #mybatis-plus代码模板目录 
{主工程}.template.vue3 #vue3 代码模板目录 

git代码地址 https://gitee.com/cng1985/nbsaas-boot/tree/main/code-generator/src/main/resources/template

2. 配置文件:

 用户通过配置文件或界面提供生成规则、模板路径、输出路径等信息。这包括数据库连接信息、表名、字段信息等。
projectName: ad #多项目的时候模块名
multiple: true #是否多模块项目
templateDir: /template/vue #代码模块文件夹
entityPackage: com.nbsaas.boot.generator.entity
  entities:
   - Ad
outputPath: E:codesvue3
bsaas-life-admin #项目根目录
basePackage: com.nbsaas.boot #基础包名

3. 元数据解析:

通过解析Java注解,代码生成器提取实体类的元数据信息,例如表名、字段名、主键等。这为后续代码生成提供了必要的信息。
nbsaas-boot代码生成注解包括
@BeanExt
@CatalogClass
@ComposeView
@CreateByUser
@FieldConvert
@FieldName
@FormAnnotation
@FormExtField
@FormField
@LbsClass
@NoHandle
@NoResponse
@NoSimple
@PermissionClass
@PermissionDataClass
@SearchBean
@SearchItem
@TenantPermissionClass
@UniqueField
@VersionClass
@Dict @DictItem @DictKey
@StoreStateBean

4. 代码生成:

根据模板和元数据,代码生成器生成具体的源代码文件。在生成过程中,模板中的占位符被实际的元数据和字段信息替代。

5. 代码结构:

生成器按照一定的目录结构组织生成的代码,以符合项目规范。这涉及将实体类、Api层、Resource层等组织在特定的包路径下。

Api模块结构规范

com.{公司域名}.{主工程}.{子工程}
com.{公司域名}.{主工程}.{子工程}.api.apis
com.{公司域名}.{主工程}.{子工程}.api.domain.enums
com.{公司域名}.{主工程}.{子工程}.api.domain.request
com.{公司域名}.{主工程}.{子工程}.api.domain.response
com.{公司域名}.{主工程}.{子工程}.api.domain.simple
com.{公司域名}.{主工程}.{子工程}.ext.apis
com.{公司域名}.{主工程}.{子工程}.ext.domain.enums
com.{公司域名}.{主工程}.{子工程}.ext.domain.request
com.{公司域名}.{主工程}.{子工程}.ext.domain.response
com.{公司域名}.{主工程}.{子工程}.ext.domain.simple

Resource模块结构规范

com.{公司域名}.{主工程}.{子工程}
com.{公司域名}.{主工程}.{子工程}.data.entity
com.{公司域名}.{主工程}.{子工程}.data.repository
com.{公司域名}.{主工程}.{子工程}.rest.conver
com.{公司域名}.{主工程}.{子工程}.rest.resource
com.{公司域名}.{主工程}.{子工程}.ext.conver
com.{公司域名}.{主工程}.{子工程}.ext.resource

6. 定制化选项

代码生成器允许用户定义自定义模板或修改现有模板,以适应不同项目和团队的需求。

7.部分核心代码

public class ApiCommand extends BaseCommand {
    @Override
    public ResponseObject<?> handle(InputRequestObject context) {
        Config config = inputRequestObject.getConfig();
        makeCode("Api", "."+config.getModuleName()+".api.apis");
        return ResponseObject.success();
    }

    @Override
    public String outPath() {
        Config config = inputRequestObject.getConfig();
        if (config.getMultiple()) {
            return config.getOutputPath() + "\apis\[[]]-api".replace("[[]]", config.getProjectName());
        } else {
            return config.getOutputPath();
        }
    }
}

处理元数据提取

public interface BeanHandle {

    void handle(Class<?> object, FormBean formBean);
}

通过Reflections实例化BeanHandle 接口,这样可以动态增加元数据处理功能。

 Reflections reflections = new Reflections("com.nbsaas.boot.generator.handle");
        Set<Class<? extends BeanHandle>> handleList = reflections.getSubTypesOf(BeanHandle.class);
        for (Class<? extends BeanHandle> handle : handleList) {
            if (Modifier.isAbstract(handle.getModifiers())) {
                continue;
            }
            try {
                BeanHandle beanHandle = handle.newInstance();
                beanHandle.handle(object, formBean);
            } catch (InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
            }
        }

例子

元数据实体代码

@Data
@FormAnnotation(title = "商品管理", model = "商品", menu = "1,164,165")
@Entity
@Table(name = "bs_product")
public class Product extends AbstractEntity {
    public static Product fromId(Long id) {
        Product result = new Product();
        result.setId(id);
        return result;
    }

    @FormField(title = "商品编码",  grid = true, col = 12)
    private String barCode;

    @SearchItem(label = "商家", name = "shop", key = "shop.id", operator = Operator.eq, classType = Long.class,show = false)
    @Comment("商家id")
    @JoinColumn(name = "shop_id")
    @FieldConvert
    @FieldName
    @ManyToOne(fetch = FetchType.LAZY)
    private Shop shop;

    @FormField(title = "商品分组",grid = true, col = 12)
    @FieldName
    @FieldConvert
    @ManyToOne(fetch = FetchType.LAZY)
    private ProductGroup productGroup;


    @Comment("商品状态 1上架 2下架")
    @Dict(items = {
            @DictItem(value = 1, label = "上架"),
            @DictItem(value = 2, label = "下架")
    })
    private Integer state;

    @FormField(title = "商品主图", sortNum = "2", grid = false, col = 12)
    private String logo;

    @FormField(title = "商品缩略图", sortNum = "2", grid = false, col = 12)
    private String thumbnail;


    @SearchItem(label = "商品名称", name = "name")
    @FormField(title = "商品名称", sortNum = "2", grid = true, col = 12)
    private String name;

    @FormField(title = "商品简介", sortNum = "111", grid = false, col = 12, type = InputType.textarea)
    private String summary;


    @Column(length = 65538)
    private String note;

    @FormField(title = "库存", sortNum = "2", grid = true, col = 12)
    private Long stockNum;

    @FormField(title = "即时库存", sortNum = "2", grid = true, col = 12)
    private Long realStock;

    //更新库存日期
    private Date stockDate;

    @FormField(title = "价格", sortNum = "2", grid = true, col = 12, sort = true)
    private BigDecimal price;

    @FormField(title = "餐盒费", sortNum = "2", grid = true, col = 12)
    private BigDecimal mealFee;

    @FormField(title = "是否开启规格", sortNum = "2", col = 12)
    private Boolean skuEnable;


    @FormField(title = "折扣", sortNum = "2", col = 12)
    private BigDecimal discount;


    @Type(type = "io.hypersistence.utils.hibernate.type.json.JsonType")
    @Column(columnDefinition = "json")
    private List<Spec> specs;



    private BigDecimal minPrice;

    private BigDecimal maxPrice;

    @SearchItem(label = "storeState", name = "storeState",classType = StoreState.class,operator = Operator.eq)
    private StoreState storeState;
}

生成代码

package com.nbsaas.boot;

import com.nbsaas.boot.command.ControllerShopCommand;
import com.nbsaas.boot.command.MapperPackageCommand;
import com.nbsaas.boot.command.MapperXmlCommand;
import com.nbsaas.boot.generator.GeneratorApp;
import com.nbsaas.boot.generator.beans.FormBean;
import com.nbsaas.boot.generator.beans.FormBeanConvert;
import com.nbsaas.boot.generator.command.common.*;
import com.nbsaas.boot.generator.command.jpa.RepositoryCommand;
import com.nbsaas.boot.generator.config.Config;
import com.nbsaas.boot.generator.context.InputRequestObject;
import org.yaml.snakeyaml.Yaml;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.sql.SQLException;
import java.util.List;

/**
 * Hello world!
 */
public class App {
    public static void main(String[] args) throws IOException, SQLException, ClassNotFoundException {

        makeCodes("config/shop.yml");
        makeCodes("config/shopArticle.yml");
        makeCodes("config/talk.yml");
        makeCodes("config/ad.yml");
        makeCodes("config/product.yml");
        makeCodes("config/customer.yml");
        makeCodes("config/promote.yml");
        makeCodes("config/common.yml");
        makeCodes("config/order.yml");
        makeCodes("config/store.yml");
        makeCodes("config/room.yml");


    }

    private static void makeCodes(String configFile) throws IOException, SQLException, ClassNotFoundException {
        Yaml yaml = new Yaml();
        String baseFile = GeneratorApp.class.getClassLoader().getResource("").getFile();
        File f = new File(baseFile + configFile);
        //读入文件

        Config config = yaml.loadAs(Files.newInputStream(f.toPath()), Config.class);
        config.setBase(baseFile);

        List<String> tables = config.getEntities();
        if (tables == null) {
            return;
        }
        for (String table : tables) {

            FormBean formBean = new FormBeanConvert().convertClass(Class.forName(config.getEntityPackage() + "." + table));

            InputRequestObject context = new InputRequestObject();
            context.setConfig(config);
            context.setFormBean(formBean);

            new DomainCommand()
                    .after(new ApiCommand())
                    .after(new ConvertCommand())
                    .after(new ControllerAdminCommand())
                    .after(new ControllerFront2Command())
                    .after(new RestCommand())
                    .after(new ExtApiCommand())
                    .after(new RepositoryCommand())
                    .after(new ExtResourceCommand())
                    .after(new MapperXmlCommand())
                    .after(new MapperPackageCommand())
                    .after(new ControllerShopCommand())
                    .after(new FieldCommand())
                    .after(new FinishCommand()).execute(context);
        }
    }
}