08-boot项目生成

  1. 工具 - Bug 修复
  2. 工具 - 易用性优化
  3. 制作 SpringBoot 生成器
  4. 测试成果
  5. 扩展思路

1. Bug修复

目前,虽然工具已经完成核心功能的开发,但是并没有经过充分的测试验证

package com.yupi.maker.template.go;

public class Tmp7 {

    /**
     * 1. 同配置多次生成,变为静态
     * 2. 误处理.ftl
     * 3. 文件输入和输出路径相反
     * 4. 调整meta.json路径
     */
    @Test
    public void test() {
        Meta meta = new Meta();
        meta.setName("acm-template-generator");
        meta.setDescription("ACM 示例模板生成器");

        String projectPath = System.getProperty("user.dir");
        String originProjectPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/springboot-init";
        String inputFilePath1 = "src/main/java/com/yupi/springbootinit/common";
        String inputFilePath2 = "src/main/resources/application.yml";

        // 模型参数配置
        TemplateMakerModelConfig templateMakerModelConfig = new TemplateMakerModelConfig();

        // - 模型组配置
        TemplateMakerModelConfig.ModelGroupConfig modelGroupConfig = new TemplateMakerModelConfig.ModelGroupConfig();
        modelGroupConfig.setGroupKey("mysql");
        modelGroupConfig.setGroupName("数据库配置");
        templateMakerModelConfig.setModelGroupConfig(modelGroupConfig);

        // - 模型配置
        TemplateMakerModelConfig.ModelInfoConfig modelInfoConfig1 = new TemplateMakerModelConfig.ModelInfoConfig();
        modelInfoConfig1.setFieldName("url");
        modelInfoConfig1.setType("String");
        modelInfoConfig1.setDefaultValue("jdbc:mysql://localhost:3306/my_db");
        modelInfoConfig1.setReplaceText("jdbc:mysql://localhost:3306/my_db");

        TemplateMakerModelConfig.ModelInfoConfig modelInfoConfig2 = new TemplateMakerModelConfig.ModelInfoConfig();
        modelInfoConfig2.setFieldName("username");
        modelInfoConfig2.setType("String");
        modelInfoConfig2.setDefaultValue("root");
        modelInfoConfig2.setReplaceText("root");

        List<TemplateMakerModelConfig.ModelInfoConfig> modelInfoConfigList = Arrays.asList(modelInfoConfig1, modelInfoConfig2);
        templateMakerModelConfig.setModels(modelInfoConfigList);

        // 文件过滤
        TemplateMakerFileConfig templateMakerFileConfig = new TemplateMakerFileConfig();
        TemplateMakerFileConfig.FileInfoConfig fileInfoConfig1 = new TemplateMakerFileConfig.FileInfoConfig();
        fileInfoConfig1.setPath(inputFilePath1);
        List<FileFilterConfig> fileFilterConfigList = new ArrayList<>();

        FileFilterConfig fileFilterConfig = FileFilterConfig.builder()
                .range(FileFilterRangeEnum.FILE_NAME.getValue())
                .rule(FileFilterRuleEnum.CONTAINS.getValue())
                .value("Base")
                .build();

        fileFilterConfigList.add(fileFilterConfig);
        fileInfoConfig1.setFilterConfigList(fileFilterConfigList);

        TemplateMakerFileConfig.FileInfoConfig fileInfoConfig2 = new TemplateMakerFileConfig.FileInfoConfig();
        fileInfoConfig2.setPath(inputFilePath2);
        templateMakerFileConfig.setFiles(Arrays.asList(fileInfoConfig1, fileInfoConfig2));

        // 分组配置
        TemplateMakerFileConfig.FileGroupConfig fileGroupConfig = new TemplateMakerFileConfig.FileGroupConfig();
        fileGroupConfig.setCondition("outputText");
        fileGroupConfig.setGroupKey("test");
        fileGroupConfig.setGroupName("测试分组");
        templateMakerFileConfig.setFileGroupConfig(fileGroupConfig);

        long id = makeTemplate(meta, originProjectPath, templateMakerFileConfig, templateMakerModelConfig, 1854123915333320704L);
        System.out.println(id);
    }

    /**
     * 制作模板
     */
    public static long makeTemplate(Meta newMeta, String originProjectPath,
                                    TemplateMakerFileConfig templateMakerFileConfig,
                                    TemplateMakerModelConfig templateMakerModelConfig,
                                    Long id) {

        // 没有 id 则生成
        if (id == null) {
            id = IdUtil.getSnowflakeNextId();
        }

        // 复制目录
        String projectPath = System.getProperty("user.dir");
        String tempDirPath = projectPath + File.separator + ".temp";
        String templatePath = tempDirPath + File.separator + id;

        // 是否为首次制作模板
        // 目录不存在,则是首次制作
        if (!FileUtil.exist(templatePath)) {
            FileUtil.mkdir(templatePath);
            FileUtil.copy(originProjectPath, templatePath, true);
        }

        // 一、输入信息
        // 输入文件信息
        String sourceRootPath = templatePath + File.separator + FileUtil.getLastPathEle(Paths.get(originProjectPath)).toString();
        // 注意 win 系统需要对路径进行转义
        sourceRootPath = sourceRootPath.replaceAll("\\\\", "/");
        List<TemplateMakerFileConfig.FileInfoConfig> fileConfigInfoList = templateMakerFileConfig.getFiles();

        // 二、生成文件模板
        // 遍历输入文件
        List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>();
        for (TemplateMakerFileConfig.FileInfoConfig fileInfoConfig : fileConfigInfoList) {
            String inputFilePath = fileInfoConfig.getPath();

            // 如果填的是相对路径,要改为绝对路径
            if (!inputFilePath.startsWith(sourceRootPath)) {
                inputFilePath = sourceRootPath + File.separator + inputFilePath;
            }

            // 获取过滤后的文件列表(不会存在目录)
            List<File> fileList = FileFilter.doFilter(inputFilePath, fileInfoConfig.getFilterConfigList());
            // 不处理已生成的 FTL 模板文件
            fileList = fileList.stream()
                    .filter(file -> !file.getAbsolutePath().endsWith(".ftl"))
                    .collect(Collectors.toList());

            for (File file : fileList) {
                Meta.FileConfig.FileInfo fileInfo = makeFileTemplate(templateMakerModelConfig, sourceRootPath, file);
                newFileInfoList.add(fileInfo);
            }
        }

        // 如果是文件组
        TemplateMakerFileConfig.FileGroupConfig fileGroupConfig = templateMakerFileConfig.getFileGroupConfig();
        if (fileGroupConfig != null) {
            String condition = fileGroupConfig.getCondition();
            String groupKey = fileGroupConfig.getGroupKey();
            String groupName = fileGroupConfig.getGroupName();

            // 新增分组配置
            Meta.FileConfig.FileInfo groupFileInfo = new Meta.FileConfig.FileInfo();
            groupFileInfo.setType(FileTypeEnum.GROUP.getValue());
            groupFileInfo.setCondition(condition);
            groupFileInfo.setGroupKey(groupKey);
            groupFileInfo.setGroupName(groupName);
            // 文件全放到一个分组内
            groupFileInfo.setFiles(newFileInfoList);
            newFileInfoList = new ArrayList<>();
            newFileInfoList.add(groupFileInfo);
        }

        // 处理模型信息
        List<TemplateMakerModelConfig.ModelInfoConfig> models = templateMakerModelConfig.getModels();
        // - 转换为配置接受的 ModelInfo 对象
        List<Meta.ModelConfig.ModelInfo> inputModelInfoList = models.stream().map(modelInfoConfig -> {
            Meta.ModelConfig.ModelInfo modelInfo = new Meta.ModelConfig.ModelInfo();
            BeanUtil.copyProperties(modelInfoConfig, modelInfo);
            return modelInfo;
        }).collect(Collectors.toList());

        // - 本次新增的模型配置列表
        List<Meta.ModelConfig.ModelInfo> newModelInfoList = new ArrayList<>();

        // - 如果是模型组
        TemplateMakerModelConfig.ModelGroupConfig modelGroupConfig = templateMakerModelConfig.getModelGroupConfig();
        if (modelGroupConfig != null) {
            String condition = modelGroupConfig.getCondition();
            String groupKey = modelGroupConfig.getGroupKey();
            String groupName = modelGroupConfig.getGroupName();
            Meta.ModelConfig.ModelInfo groupModelInfo = new Meta.ModelConfig.ModelInfo();
            groupModelInfo.setGroupKey(groupKey);
            groupModelInfo.setGroupName(groupName);
            groupModelInfo.setCondition(condition);

            // 模型全放到一个分组内
            groupModelInfo.setModels(inputModelInfoList);
            newModelInfoList.add(groupModelInfo);
        } else {
            // 不分组,添加所有的模型信息到列表
            newModelInfoList.addAll(inputModelInfoList);
        }

        // 三、生成配置文件
        String metaOutputPath = templatePath + File.separator + "meta.json";

        // 如果已有 meta 文件,说明不是第一次制作,则在 meta 基础上进行修改
        if (FileUtil.exist(metaOutputPath)) {
            Meta oldMeta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);
            BeanUtil.copyProperties(newMeta, oldMeta, CopyOptions.create().ignoreNullValue());
            newMeta = oldMeta;

            // 1. 追加配置参数
            List<Meta.FileConfig.FileInfo> fileInfoList = newMeta.getFileConfig().getFiles();
            fileInfoList.addAll(newFileInfoList);
            List<Meta.ModelConfig.ModelInfo> modelInfoList = newMeta.getModelConfig().getModels();
            modelInfoList.addAll(newModelInfoList);

            // 配置去重
            newMeta.getFileConfig().setFiles(distinctFiles(fileInfoList));
            newMeta.getModelConfig().setModels(distinctModels(modelInfoList));
        } else {
            // 1. 构造配置参数
            Meta.FileConfig fileConfig = new Meta.FileConfig();
            newMeta.setFileConfig(fileConfig);
            fileConfig.setSourceRootPath(sourceRootPath);
            List<Meta.FileConfig.FileInfo> fileInfoList = new ArrayList<>();
            fileConfig.setFiles(fileInfoList);
            fileInfoList.addAll(newFileInfoList);

            Meta.ModelConfig modelConfig = new Meta.ModelConfig();
            newMeta.setModelConfig(modelConfig);
            List<Meta.ModelConfig.ModelInfo> modelInfoList = new ArrayList<>();
            modelConfig.setModels(modelInfoList);
            modelInfoList.addAll(newModelInfoList);
        }

        // 2. 输出元信息文件
        FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(newMeta), metaOutputPath);
        return id;
    }

    /**
     * 制作文件模板
     */
    private static Meta.FileConfig.FileInfo makeFileTemplate(
            TemplateMakerModelConfig templateMakerModelConfig,
            String sourceRootPath, File inputFile) {

        // 要挖坑的文件绝对路径(用于制作模板)
        // 注意 win 系统需要对路径进行转义
        String fileInputAbsolutePath = inputFile.getAbsolutePath().replaceAll("\\\\", "/");
        String fileOutputAbsolutePath = fileInputAbsolutePath + ".ftl";

        // 文件输入输出相对路径(用于生成配置)
        String fileInputPath = fileInputAbsolutePath.replace(sourceRootPath + "/", "");
        String fileOutputPath = fileInputPath + ".ftl";

        // 使用字符串替换,生成模板文件
        String fileContent;
        // 如果已有模板文件,说明不是第一次制作,则在模板基础上再次挖坑
        boolean hasTemplateFile = FileUtil.exist(fileOutputAbsolutePath);
        if (hasTemplateFile) {
            fileContent = FileUtil.readUtf8String(fileOutputAbsolutePath);
        } else {
            fileContent = FileUtil.readUtf8String(fileInputAbsolutePath);
        }

        // 支持多个模型:对同一个文件的内容,遍历模型进行多轮替换
        TemplateMakerModelConfig.ModelGroupConfig modelGroupConfig = templateMakerModelConfig.getModelGroupConfig();
        String newFileContent = fileContent;
        String replacement;
        for (TemplateMakerModelConfig.ModelInfoConfig modelInfoConfig : templateMakerModelConfig.getModels()) {
            // 不是分组
            if (modelGroupConfig == null) {
                replacement = String.format("${%s}", modelInfoConfig.getFieldName());
            } else {
                // 是分组
                String groupKey = modelGroupConfig.getGroupKey();
                // 注意挖坑要多一个层级
                replacement = String.format("${%s.%s}", groupKey, modelInfoConfig.getFieldName());
            }
            // 多次替换
            newFileContent = StrUtil.replace(newFileContent, modelInfoConfig.getReplaceText(), replacement);
        }

        // 文件配置信息
        Meta.FileConfig.FileInfo fileInfo = new Meta.FileConfig.FileInfo();
        // 注意文件输入路径要和输出路径反转
        fileInfo.setInputPath(fileOutputPath);
        fileInfo.setOutputPath(fileInputPath);
        fileInfo.setType(FileTypeEnum.FILE.getValue());
        fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());

        // 是否更改了文件内容
        boolean contentEquals = newFileContent.equals(fileContent);
        // 之前不存在模板文件,并且没有更改文件内容,则为静态生成
        if (!hasTemplateFile) {
            if (contentEquals) {
                // 输入路径没有 FTL 后缀
                fileInfo.setInputPath(fileInputPath);
                fileInfo.setGenerateType(FileGenerateTypeEnum.STATIC.getValue());
            } else {
                // 没有模板文件,需要挖坑,生成模板文件
                FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
            }
        } else if (!contentEquals) {
            // 有模板文件,且增加了新坑,生成模板文件
            FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
        }

        return fileInfo;
    }

    /**
     * 模型分组去重
     */
    private static List<Meta.ModelConfig.ModelInfo> distinctModels(List<Meta.ModelConfig.ModelInfo> modelInfoList) {
        // 策略:同分组内模型 merge,不同分组保留

        // 1. 有分组的,以组为单位划分
        Map<String, List<Meta.ModelConfig.ModelInfo>> groupKeyModelInfoListMap = modelInfoList
                .stream()
                .filter(modelInfo -> StrUtil.isNotBlank(modelInfo.getGroupKey()))
                .collect(
                        Collectors.groupingBy(Meta.ModelConfig.ModelInfo::getGroupKey)
                );

        // 2. 同组内的模型配置合并
        // 保存每个组对应的合并后的对象 map
        Map<String, Meta.ModelConfig.ModelInfo> groupKeyMergedModelInfoMap = new HashMap<>();
        for (Map.Entry<String, List<Meta.ModelConfig.ModelInfo>> entry : groupKeyModelInfoListMap.entrySet()) {
            List<Meta.ModelConfig.ModelInfo> tempModelInfoList = entry.getValue();
            List<Meta.ModelConfig.ModelInfo> newModelInfoList = new ArrayList<>(tempModelInfoList.stream()
                    .flatMap(modelInfo -> modelInfo.getModels().stream())
                    .collect(
                            Collectors.toMap(Meta.ModelConfig.ModelInfo::getFieldName, o -> o, (e, r) -> r)
                    ).values());

            // 使用新的 group 配置
            Meta.ModelConfig.ModelInfo newModelInfo = CollUtil.getLast(tempModelInfoList);
            newModelInfo.setModels(newModelInfoList);
            String groupKey = entry.getKey();
            groupKeyMergedModelInfoMap.put(groupKey, newModelInfo);
        }

        // 3. 将模型分组添加到结果列表
        List<Meta.ModelConfig.ModelInfo> resultList = new ArrayList<>(groupKeyMergedModelInfoMap.values());

        // 4. 将未分组的模型添加到结果列表
        List<Meta.ModelConfig.ModelInfo> noGroupModelInfoList = modelInfoList.stream()
                .filter(modelInfo -> StrUtil.isBlank(modelInfo.getGroupKey()))
                .collect(Collectors.toList());
        resultList.addAll(new ArrayList<>(noGroupModelInfoList.stream()
                .collect(
                        Collectors.toMap(Meta.ModelConfig.ModelInfo::getFieldName, o -> o, (e, r) -> r)
                ).values()));
        return resultList;
    }

    /**
     * 文件分组去重
     */
    private static List<Meta.FileConfig.FileInfo> distinctFiles(List<Meta.FileConfig.FileInfo> fileInfoList) {
        // 策略:同分组内文件 merge,不同分组保留

        // 1. 有分组的,以组为单位划分
        Map<String, List<Meta.FileConfig.FileInfo>> groupKeyFileInfoListMap = fileInfoList
                .stream()
                .filter(fileInfo -> StrUtil.isNotBlank(fileInfo.getGroupKey()))
                .collect(
                        Collectors.groupingBy(Meta.FileConfig.FileInfo::getGroupKey)
                );


        // 2. 同组内的文件配置合并
        // 保存每个组对应的合并后的对象 map
        Map<String, Meta.FileConfig.FileInfo> groupKeyMergedFileInfoMap = new HashMap<>();
        for (Map.Entry<String, List<Meta.FileConfig.FileInfo>> entry : groupKeyFileInfoListMap.entrySet()) {
            List<Meta.FileConfig.FileInfo> tempFileInfoList = entry.getValue();
            List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>(tempFileInfoList.stream()
                    .flatMap(fileInfo -> fileInfo.getFiles().stream())
                    .collect(
                            Collectors.toMap(Meta.FileConfig.FileInfo::getOutputPath, o -> o, (e, r) -> r)
                    ).values());

            // 使用新的 group 配置
            Meta.FileConfig.FileInfo newFileInfo = CollUtil.getLast(tempFileInfoList);
            newFileInfo.setFiles(newFileInfoList);
            String groupKey = entry.getKey();
            groupKeyMergedFileInfoMap.put(groupKey, newFileInfo);
        }

        // 3. 将文件分组添加到结果列表
        List<Meta.FileConfig.FileInfo> resultList = new ArrayList<>(groupKeyMergedFileInfoMap.values());

        // 4. 将未分组的文件添加到结果列表
        List<Meta.FileConfig.FileInfo> noGroupFileInfoList = fileInfoList.stream().filter(fileInfo -> StrUtil.isBlank(fileInfo.getGroupKey()))
                .collect(Collectors.toList());

        resultList.addAll(new ArrayList<>(noGroupFileInfoList.stream()
                .collect(
                        Collectors.toMap(Meta.FileConfig.FileInfo::getOutputPath, o -> o, (e, r) -> r)
                ).values()));
        return resultList;
    }

}








































































 


















































 



 























































 
























































 
 


























 
 

 


 

 








 












































































 


















 





1. 同配置多次执行变为静态

1. Bug介绍

  • 第一次生成的 meta.json 文件
{
    "inputPath": "src/main/resources/application.yml",
    "outputPath": "src/main/resources/application.yml.ftl",
    "type": "file",
    "generateType": "dynamic"
}
  • 第二次生成时,不修改任何的输入配置,直接再次执行工具
    • generateType 被修改为 static 类型,显然这是有问题的,因为 application.yml 已经被制作为了模板
{
    "inputPath": "src/main/resources/application.yml",
    "outputPath": "src/main/resources/application.yml",
    "type": "file",
    "generateType": "static"
}


 

 

2. 解决方案

该 Bug 的解决方案很简单:如果后续制作时发现已有模板文件,则该文件不会被设置为静态生成

  1. makeFileTemplate() 方法中,抽象出 hasTemplateFile 布尔变量,用于判断是否已有模板文件
  2. 调整设置文件信息对象的逻辑,默认设置生成类型为动态
    • 如果之前不存在模板文件,并且经过字符串替换后没有更改文件内容(只有这一种情况),才为静态生成
/**
 * 制作文件模板
 */
private static Meta.FileConfig.FileInfo makeFileTemplate(
        TemplateMakerModelConfig templateMakerModelConfig,
        String sourceRootPath, File inputFile) {

    // 要挖坑的文件绝对路径(用于制作模板)
    // 注意 win 系统需要对路径进行转义
    String fileInputAbsolutePath = inputFile.getAbsolutePath().replaceAll("\\\\", "/");
    String fileOutputAbsolutePath = fileInputAbsolutePath + ".ftl";

    // 文件输入输出相对路径(用于生成配置)
    String fileInputPath = fileInputAbsolutePath.replace(sourceRootPath + "/", "");
    String fileOutputPath = fileInputPath + ".ftl";

    // 使用字符串替换,生成模板文件
    String fileContent;
    // 如果已有模板文件,说明不是第一次制作,则在模板基础上再次挖坑
    boolean hasTemplateFile = FileUtil.exist(fileOutputAbsolutePath);
    if (hasTemplateFile) {
        fileContent = FileUtil.readUtf8String(fileOutputAbsolutePath);
    } else {
        fileContent = FileUtil.readUtf8String(fileInputAbsolutePath);
    }

    // 支持多个模型:对同一个文件的内容,遍历模型进行多轮替换
    TemplateMakerModelConfig.ModelGroupConfig modelGroupConfig = templateMakerModelConfig.getModelGroupConfig();
    String newFileContent = fileContent;
    String replacement;
    for (TemplateMakerModelConfig.ModelInfoConfig modelInfoConfig : templateMakerModelConfig.getModels()) {
        // 不是分组
        if (modelGroupConfig == null) {
            replacement = String.format("${%s}", modelInfoConfig.getFieldName());
        } else {
            // 是分组
            String groupKey = modelGroupConfig.getGroupKey();
            // 注意挖坑要多一个层级
            replacement = String.format("${%s.%s}", groupKey, modelInfoConfig.getFieldName());
        }
        // 多次替换
        newFileContent = StrUtil.replace(newFileContent, modelInfoConfig.getReplaceText(), replacement);
    }

    // 文件配置信息
    Meta.FileConfig.FileInfo fileInfo = new Meta.FileConfig.FileInfo();
    // 注意文件输入路径要和输出路径反转
    fileInfo.setInputPath(fileOutputPath);
    fileInfo.setOutputPath(fileInputPath);
    fileInfo.setType(FileTypeEnum.FILE.getValue());
    fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());

    // 是否更改了文件内容
    boolean contentEquals = newFileContent.equals(fileContent);
    // 之前不存在模板文件,并且没有更改文件内容,则为静态生成
    if (!hasTemplateFile) {
        if (contentEquals) {
            // 输入路径没有 FTL 后缀
            fileInfo.setInputPath(fileInputPath);
            fileInfo.setGenerateType(FileGenerateTypeEnum.STATIC.getValue());
        } else {
            // 没有模板文件,需要挖坑,生成模板文件
            FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
        }
    } else if (!contentEquals) {
        // 有模板文件,且增加了新坑,生成模板文件
        FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
    }

    return fileInfo;
}



















 
 





























 




 



 




 






3. 测试验证

TemplateMaker 类名上按 Alt + Enter,选中 Create Test,即可快速创建单元测试

ef33e5b6414c4a1fb2f7f3fc2bc2fde8

选择正确的单元测试库,并勾选需要测试的方法

bb37c04409e94ae4b31175fdddaacdb9

连续执行两次方法,发现文件的生成类型符合预期

9cd3afe5b7584a66ae4e48b868ddbb7f

2. 错误处理.ftl

1. Bug介绍

多次制作时,指定了相同的目录,会基于 FTL 文件再次制作模板,导致生成错误配置。BaseResponse.java.ftl 被进行了处理

{
    "inputPath": "src/main/java/com/yupi/springbootinit/common/BaseResponse.java",
    "outputPath": "src/main/java/com/yupi/springbootinit/common/BaseResponse.java.ftl",
    "type": "file",
    "generateType": "dynamic"
},
{
    "inputPath": "src/main/java/com/yupi/springbootinit/common/BaseResponse.java.ftl",
    "outputPath": "src/main/java/com/yupi/springbootinit/common/BaseResponse.java.ftl",
    "type": "file",
    "generateType": "static"
}







 


 

2. 解决方案

修改 makeTemplate(),在文件过滤后补充移除 FTL 模板文件

// 获取过滤后的文件列表(不会存在目录)
List<File> fileList = FileFilter.doFilter(inputFilePath, fileInfoConfig.getFilterConfigList());
// 不处理已生成的 FTL 模板文件
fileList = fileList.stream()
        .filter(file -> !file.getAbsolutePath().endsWith(".ftl"))
        .collect(Collectors.toList());




 

3. 文件输入输出路径相反

1. Bug介绍

  • 在制作模板时,根据原始文件得到 FTL 模板文件
  • 但在生成器的元信息中,其实是根据 FTL 模板文件来生成目标文件
{
    "inputPath": "src/main/java/com/yupi/springbootinit/common/BaseResponse.java",
    "outputPath": "src/main/java/com/yupi/springbootinit/common/BaseResponse.java.ftl",
    "type": "file",
    "generateType": "dynamic"
}

 
 



2. 解决方案

  • 在封装 fileInfo 对象时,对输入输出路径的值进行替换。保证输入路径是 FTL 模板文件、输出路径是预期生成的文件
  • 注意,如果是静态生成,也要保证输入路径等于输出路径
// 文件配置信息
Meta.FileConfig.FileInfo fileInfo = new Meta.FileConfig.FileInfo();
// 注意文件输入路径要和输出路径反转
fileInfo.setInputPath(fileOutputPath);
fileInfo.setOutputPath(fileInputPath);
fileInfo.setType(FileTypeEnum.FILE.getValue());
fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());

// 是否更改了文件内容
boolean contentEquals = newFileContent.equals(fileContent);
// 之前不存在模板文件,并且没有更改文件内容,则为静态生成
if (!hasTemplateFile) {
    if (contentEquals) {
        // 输入路径没有 FTL 后缀
        fileInfo.setInputPath(fileInputPath);
        fileInfo.setGenerateType(FileGenerateTypeEnum.STATIC.getValue());
    } else {
        // 没有模板文件,且增加了新坑,生成模板文件
        FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
    }
}
return fileInfo;



 
 

















  • 注意,文件去重方法 distinctFiles 也要同步修改,改为根据 outputPath 属性去重
List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>(tempFileInfoList.stream()
    .flatMap(fileInfo -> fileInfo.getFiles().stream())
    .collect(
            Collectors.toMap(Meta.FileConfig.FileInfo::getOutputPath, o -> o, (e, r) -> r)
    ).values());



 

4. 调整meta.json路径

1. Bug介绍

meta.json 会生成在工作空间内、项目的根目录下,如果输入文件路径是项目的根目录,那么 meta.json 也会被当成项目文件被处理

31abbb75914e4ec78f208b363c6143fa

2. 解决方案

修改 meta.json 的生成路径,调整为工作空间根目录下,和项目目录平级

// 三、生成配置文件
String metaOutputPath = templatePath + File.separator + "meta.json";
40fb0b3819404e95978f58079da0be47

2. 参数封装 - 易用性优化

把工具需要的所有参数统一封装为一个对象,通过传递一个 JSON 配置文件(或后续的 HTTP Post 请求)来快速填充参数

  1. template.model 包下新建 TemplateMakerConfig
package com.yupi.maker.template.model;

import com.yupi.maker.meta.Meta;
import lombok.Data;

/**
 * 模板制作配置
 */
@Data
public class TemplateMakerConfig {

    private Long id;

    private Meta meta = new Meta();

    private String originProjectPath;

    TemplateMakerFileConfig fileConfig = new TemplateMakerFileConfig();

    TemplateMakerModelConfig modelConfig = new TemplateMakerModelConfig();
}
  1. TemplateMaker 工具类中新增接受该封装类的重载方法
package com.yupi.maker.template;

public class TemplateMaker {

    /**
     * 制作模板
     */
    public static long makeTemplate(TemplateMakerConfig templateMakerConfig) {
        Meta meta = templateMakerConfig.getMeta();
        String originProjectPath = templateMakerConfig.getOriginProjectPath();
        TemplateMakerFileConfig templateMakerFileConfig = templateMakerConfig.getFileConfig();
        TemplateMakerModelConfig templateMakerModelConfig = templateMakerConfig.getModelConfig();
        TemplateMakerOutputConfig templateMakerOutputConfig = templateMakerConfig.getOutputConfig();
        Long id = templateMakerConfig.getId();

        return makeTemplate(meta, originProjectPath, templateMakerFileConfig, templateMakerModelConfig, templateMakerOutputConfig, id);
    }

}







 











  1. resources 资源目录下新建临时 templateMaker.json,用于给封装对象设置参数
{
    "meta": {
        "name": "acm-template-pro-generator",
        "description": "ACM 示例模板生成器"
    },
    "originProjectPath": "../../../yuzi-generator-demo-projects/springboot-init",
    "fileConfig": {
        "files": [
            {
                "path": "src/main/java/com/yupi/springbootinit/common"
            }
        ]
    },
    "modelConfig": {
        "models": [
            {
                "fieldName": "className",
                "type": "String",
                "defaultValue": true,
                "replaceText": "BaseResponse"
            }
        ]
    }
}
  1. 编写单元测试,读取 JSON 并转换为配置对象
/**
 * 使用 JSON 制作模板
 */
@Test
public void testMakeTemplateWithJSON() {
    String configStr = ResourceUtil.readUtf8Str("templateMaker.json");
    TemplateMakerConfig templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);
    long id = TemplateMaker.makeTemplate(templateMakerConfig);
    System.out.println(id);
}





 
 



  1. 测试执行,生成了符合预期的代码
1c1121b6cb36469ab26cf9cbeb4ca869

3. SpringBoot生成器

制作思路:

  • 通过一步一步编写工具所需的配置文件,自动生成模板和元信息文件,依次完成动态生成需求
  • 然后再通过工具的生成能力,得到可执行的生成器

1. 项目基本信息

1. 编写配置

{
    "id": 1,
    "meta": {
        "name": "springboot-init-generator",
        "description": "Spring Boot 模板项目生成器"
    },
    "originProjectPath": "../../../yuzi-generator-demo-projects/springboot-init"
}
/**
 * 制作 SpringBoot 模板
 */
@Test
public void test() {
    String rootPath = "examples/springboot-init/";
    String configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker.json");
    TemplateMakerConfig templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);
    long id = TemplateMaker.makeTemplate(templateMakerConfig);
    System.out.println("id = " + id);
}

运行,结果报错啦!

c8ba3770c3b44da58445c4be2d630faf

2. 增加非空校验

持久化项目路径

1. makeFileTemplates()

抽象出 makeFileTemplates()(制作文件模板的逻辑),补充文件非空校验

private static List<Meta.FileConfig.FileInfo> makeFileTemplates(TemplateMakerFileConfig templateMakerFileConfig
        ,TemplateMakerModelConfig templateMakerModelConfig
        ,String sourceRootPath) {

    // 遍历输入文件
    List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>();
    // 非空校验
    if (templateMakerFileConfig == null) {
        return newFileInfoList;
    }
    List<TemplateMakerFileConfig.FileInfoConfig> fileConfigInfoList = templateMakerFileConfig.getFiles();
    // 非空校验
    if (CollUtil.isEmpty(fileConfigInfoList)) {
        return newFileInfoList;
    }

    for (TemplateMakerFileConfig.FileInfoConfig fileInfoConfig : fileConfigInfoList) {
        String inputFilePath = fileInfoConfig.getPath();

        // 如果填的是相对路径,要改为绝对路径
        if (!inputFilePath.startsWith(sourceRootPath)) {
            inputFilePath = sourceRootPath + File.separator + inputFilePath;
        }

        // 获取过滤后的文件列表(不会存在目录)
        List<File> fileList = FileFilter.doFilter(inputFilePath, fileInfoConfig.getFilterConfigList());
        // 不处理已生成的 FTL 模板文件
        fileList = fileList.stream()
                .filter(file -> !file.getAbsolutePath().endsWith(".ftl"))
                .collect(Collectors.toList());

        for (File file : fileList) {
            Meta.FileConfig.FileInfo fileInfo = makeFileTemplate(templateMakerModelConfig, sourceRootPath, file);
            newFileInfoList.add(fileInfo);
        }
    }

    // 如果是文件组
    TemplateMakerFileConfig.FileGroupConfig fileGroupConfig = templateMakerFileConfig.getFileGroupConfig();
    if (fileGroupConfig != null) {
        String condition = fileGroupConfig.getCondition();
        String groupKey = fileGroupConfig.getGroupKey();
        String groupName = fileGroupConfig.getGroupName();

        // 新增分组配置
        Meta.FileConfig.FileInfo groupFileInfo = new Meta.FileConfig.FileInfo();
        groupFileInfo.setType(FileTypeEnum.GROUP.getValue());
        groupFileInfo.setCondition(condition);
        groupFileInfo.setGroupKey(groupKey);
        groupFileInfo.setGroupName(groupName);
        // 文件全放到一个分组内
        groupFileInfo.setFiles(newFileInfoList);
        newFileInfoList = new ArrayList<>();
        newFileInfoList.add(groupFileInfo);
    }
    return newFileInfoList;
}







 
 
 


 
 
 

















 
























2. getModelInfoList()

抽象出 getModelInfoList()(模型配置列表的逻辑),补充模型非空校验

private static List<Meta.ModelConfig.ModelInfo> getModelInfoList(TemplateMakerModelConfig templateMakerModelConfig) {

    // - 本次新增的模型配置列表
    List<Meta.ModelConfig.ModelInfo> newModelInfoList = new ArrayList<>();
    if (templateMakerModelConfig == null) {
        return newModelInfoList;
    }

    List<TemplateMakerModelConfig.ModelInfoConfig> models = templateMakerModelConfig.getModels();
    if (CollUtil.isEmpty(models)) {
        return newModelInfoList;
    }

    // - 转换为配置接受的 ModelInfo 对象
    List<Meta.ModelConfig.ModelInfo> inputModelInfoList = models.stream().map(modelInfoConfig -> {
        Meta.ModelConfig.ModelInfo modelInfo = new Meta.ModelConfig.ModelInfo();
        BeanUtil.copyProperties(modelInfoConfig, modelInfo);
        return modelInfo;
    }).collect(Collectors.toList());

    // - 如果是模型组
    TemplateMakerModelConfig.ModelGroupConfig modelGroupConfig = templateMakerModelConfig.getModelGroupConfig();

    if (modelGroupConfig != null) {
        String condition = modelGroupConfig.getCondition();
        String groupKey = modelGroupConfig.getGroupKey();
        String groupName = modelGroupConfig.getGroupName();
        Meta.ModelConfig.ModelInfo groupModelInfo = new Meta.ModelConfig.ModelInfo();
        groupModelInfo.setGroupKey(groupKey);
        groupModelInfo.setGroupName(groupName);
        groupModelInfo.setCondition(condition);

        // 模型全放到一个分组内
        groupModelInfo.setModels(inputModelInfoList);
        newModelInfoList.add(groupModelInfo);
    } else {
        // 不分组,添加所有的模型信息到列表
        newModelInfoList.addAll(inputModelInfoList);
    }
    return newModelInfoList;
}




 
 
 


 
 
 





























再次制作模板,成功生成包含项目基本信息的元信息配置文件

f020be99061143dc987ce5c0b69cf804

2. 需求 - 替换包名

允许用户传入 basePackage 模型参数,对 SpringBoot 中所有出现包名的地方进行替换(eg:@MapperScan 注解里也有包名)

1. 持久化项目路径

  • 先完善一下配置追加能力。如果非首次制作,其实配置文件中肯定已经存在了 originProjectPath 参数,那么后续制作时,不需要再在配置文件中指定该参数
  • 工具只有获取 sourceRootPath 时用到了 originProjectPath,修改该变量的获取方式,自动读取工作空间下的第一个目录(项目根目录)即可
    • 注意:获取第一个目录时,需要设置层级为 1,且必须读取目录而不是文件。否则可能会因为 .DS_Store 等系统临时生成的文件干扰结果
/**
 * 制作模板
 */
public static long makeTemplate(Meta newMeta, String originProjectPath,
                                TemplateMakerFileConfig templateMakerFileConfig,
                                TemplateMakerModelConfig templateMakerModelConfig,
                                Long id) {

    // 没有 id 则生成
    if (id == null) {
        id = IdUtil.getSnowflakeNextId();
    }

    // 复制目录
    String projectPath = System.getProperty("user.dir");
    String tempDirPath = projectPath + File.separator + ".temp";
    String templatePath = tempDirPath + File.separator + id;

    // 是否为首次制作模板
    // 目录不存在,则是首次制作
    if (!FileUtil.exist(templatePath)) {
        FileUtil.mkdir(templatePath);
        FileUtil.copy(originProjectPath, templatePath, true);
    }

    // 一、输入信息
    // 输入文件信息
    // String sourceRootPath = templatePath + File.separator + FileUtil.getLastPathEle(Paths.get(originProjectPath)).toString();
    String sourceRootPath = FileUtil.loopFiles(new File(templatePath), 1, null)
            .stream()
            .filter(File::isDirectory)
            .findFirst()
            .orElseThrow(RuntimeException::new)
            .getAbsolutePath();
    // 注意 win 系统需要对路径进行转义
    sourceRootPath = sourceRootPath.replaceAll("\\\\", "/");

    // 二、生成文件模板
    List<Meta.FileConfig.FileInfo> newFileInfoList = makeFileTemplates(templateMakerFileConfig,
            templateMakerModelConfig, sourceRootPath);

    // 处理模型信息
    List<Meta.ModelConfig.ModelInfo> newModelInfoList = getModelInfoList(templateMakerModelConfig);

    // 三、生成配置文件
    String metaOutputPath = templatePath + File.separator + "meta.json";

    // 如果已有 meta 文件,说明不是第一次制作,则在 meta 基础上进行修改
    if (FileUtil.exist(metaOutputPath)) {
        Meta oldMeta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);
        BeanUtil.copyProperties(newMeta, oldMeta, CopyOptions.create().ignoreNullValue());
        newMeta = oldMeta;

        // 1. 追加配置参数
        List<Meta.FileConfig.FileInfo> fileInfoList = newMeta.getFileConfig().getFiles();
        fileInfoList.addAll(newFileInfoList);
        List<Meta.ModelConfig.ModelInfo> modelInfoList = newMeta.getModelConfig().getModels();
        modelInfoList.addAll(newModelInfoList);

        // 配置去重
        newMeta.getFileConfig().setFiles(distinctFiles(fileInfoList));
        newMeta.getModelConfig().setModels(distinctModels(modelInfoList));
    } else {
        // 1. 构造配置参数
        Meta.FileConfig fileConfig = new Meta.FileConfig();
        newMeta.setFileConfig(fileConfig);
        fileConfig.setSourceRootPath(sourceRootPath);
        List<Meta.FileConfig.FileInfo> fileInfoList = new ArrayList<>();
        fileConfig.setFiles(fileInfoList);
        fileInfoList.addAll(newFileInfoList);

        Meta.ModelConfig modelConfig = new Meta.ModelConfig();
        newMeta.setModelConfig(modelConfig);
        List<Meta.ModelConfig.ModelInfo> modelInfoList = new ArrayList<>();
        modelConfig.setModels(modelInfoList);
        modelInfoList.addAll(newModelInfoList);
    }

    // 2. 输出元信息文件
    FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(newMeta), metaOutputPath);
    return id;
}



























 
 
 
 
 
 
 




 



 







































2. 测试执行

全局替换 springboot-init 所有文件的 com.yupi 字符串为 basePackage 模型参数

{
    "id": 1,
    "fileConfig": {
        "files": [
            {
                "path": ""
            }
        ]
    },
    "modelConfig": {
        "models": [
            {
                "fieldName": "basePackage",
                "type": "String",
                "description": "基础包名",
                "defaultValue": "com.yupi",
                "replaceText": "com.yupi"
            }
        ]
    }
}












 



 




/**
 * 制作 SpringBoot 模板
 */
@Test
public void test() {
    String rootPath = "examples/springboot-init/";
    String configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker1.json");
    TemplateMakerConfig templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);
    long id = makeTemplate(templateMakerConfig);
    System.out.println("id = " + id);
}






 




执行成功,生成的模板和元信息配置文件符合预期

b566e73bf579461bb41a938330dcebd1

3. 需求 - 控制生成帖子

允许用户传入 needPost 模型参数,控制帖子功能相关的文件是否生成(PostControllerPostServicePostMapperPostMapper.xmlPost.java 等)

1. 测试执行

帖子相关文件分散在不同的目录下,使用文件过滤器,只保留文件名包含 Post。并设置为同一个文件组,needPost 控制是否生成

{
    "id": 1,
    "fileConfig": {
        "fileGroupConfig": {
            "groupKey": "post",
            "groupName": "帖子文件组",
            "condition": "needPost"
        },
        "files": [
            {
                "path": "src/main",
                "filterConfigList": [
                    {
                        "range": "fileName",
                        "rule": "contains",
                        "value": "Post"
                    }
                ]
            }
        ]
    },
    "modelConfig": {
        "models": [
            {
                "fieldName": "needPost",
                "type": "boolean",
                "description": "是否开启帖子功能",
                "defaultValue": true
            }
        ]
    }
}











 












 







查看生成的元信息配置,发现文件分组正确生成

de36761421064ab4bcf2b7da5a0e8969

问题:前面制作时,已经生成过帖子相关的模板文件,现在分组内新增了相同文件,导致同一个文件在组内外多次重复出现

3f61371f1898462eab07db1a2c262a16

2. 自定义去重

  1. template.model 包下新建 TemplateMakerOutputConfig 输出配置类,并定义一个控制分组去重的属性
package com.yupi.maker.template.model;

@Data
public class TemplateMakerOutputConfig {

    // 从未分组文件中移除组内的同名文件
    private boolean removeGroupFilesFromRoot = true;
}






 

  1. TemplateMakerConfig 中补充输出配置属性
@Data
public class TemplateMakerConfig {

    private Long id;

    private Meta meta = new Meta();

    private String originProjectPath;

    TemplateMakerFileConfig fileConfig = new TemplateMakerFileConfig();

    TemplateMakerModelConfig modelConfig = new TemplateMakerModelConfig();

    TemplateMakerOutputConfig outputConfig = new TemplateMakerOutputConfig();
}













 

  1. 编写分组去重的实现代码
    • 由于去重逻辑比较复杂,而且算是一个额外的能力,所以建议单独编写一个工具类实现
/**
 * 模板制作工具类
 */
public class TemplateMakerUtils {

    /**
     * 从未分组文件中移除组内的同名文件
     */
    public static List<Meta.FileConfig.FileInfo> removeGroupFilesFromRoot(List<Meta.FileConfig.FileInfo> fileInfoList) {
        // 1. 先获取到所有分组
        List<Meta.FileConfig.FileInfo> groupFileInfoList = fileInfoList.stream()
                .filter(fileInfo -> StrUtil.isNotBlank(fileInfo.getGroupKey()))
                .collect(Collectors.toList());

        // 2. 获取所有分组内的文件列表
        List<Meta.FileConfig.FileInfo> groupInnerFileInfoList = groupFileInfoList.stream()
                .flatMap(fileInfo -> fileInfo.getFiles().stream())
                .collect(Collectors.toList());

        // 3. 获取所有分组内文件输入路径集合
        Set<String> fileInputPathSet = groupInnerFileInfoList.stream()
                .map(Meta.FileConfig.FileInfo::getInputPath)
                .collect(Collectors.toSet());

        // 4. 移除所有输入路径在 set 中的外层文件
        return fileInfoList.stream()
                .filter(fileInfo -> !fileInputPathSet.contains(fileInfo.getInputPath()))
                .collect(Collectors.toList());
    }
}









 




 




 




 





  1. 增加输出配置参数,并根据配置执行文件分组合并
/**
 * 制作模板
 */
public static long makeTemplate(Meta newMeta, String originProjectPath,
                                TemplateMakerFileConfig templateMakerFileConfig,
                                TemplateMakerModelConfig templateMakerModelConfig,
                                TemplateMakerOutputConfig templateMakerOutputConfig,
                                Long id) {

    // 没有 id 则生成
    if (id == null) {
        id = IdUtil.getSnowflakeNextId();
    }

    // 复制目录
    String projectPath = System.getProperty("user.dir");
    String tempDirPath = projectPath + File.separator + ".temp";
    String templatePath = tempDirPath + File.separator + id;

    // 是否为首次制作模板
    // 目录不存在,则是首次制作
    if (!FileUtil.exist(templatePath)) {
        FileUtil.mkdir(templatePath);
        FileUtil.copy(originProjectPath, templatePath, true);
    }

    // 一、输入信息
    // 输入文件信息
    // String sourceRootPath = templatePath + File.separator + FileUtil.getLastPathEle(Paths.get(originProjectPath)).toString();
    String sourceRootPath = FileUtil.loopFiles(new File(templatePath), 1, null)
            .stream()
            .filter(File::isDirectory)
            .findFirst()
            .orElseThrow(RuntimeException::new)
            .getAbsolutePath();
    // 注意 win 系统需要对路径进行转义
    sourceRootPath = sourceRootPath.replaceAll("\\\\", "/");

    // 二、生成文件模板
    List<Meta.FileConfig.FileInfo> newFileInfoList = makeFileTemplates(templateMakerFileConfig,
            templateMakerModelConfig, sourceRootPath);

    // 处理模型信息
    List<Meta.ModelConfig.ModelInfo> newModelInfoList = getModelInfoList(templateMakerModelConfig);

    // 三、生成配置文件
    String metaOutputPath = templatePath + File.separator + "meta.json";

    // 如果已有 meta 文件,说明不是第一次制作,则在 meta 基础上进行修改
    if (FileUtil.exist(metaOutputPath)) {
        Meta oldMeta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);
        BeanUtil.copyProperties(newMeta, oldMeta, CopyOptions.create().ignoreNullValue());
        newMeta = oldMeta;

        // 1. 追加配置参数
        List<Meta.FileConfig.FileInfo> fileInfoList = newMeta.getFileConfig().getFiles();
        fileInfoList.addAll(newFileInfoList);
        List<Meta.ModelConfig.ModelInfo> modelInfoList = newMeta.getModelConfig().getModels();
        modelInfoList.addAll(newModelInfoList);

        // 配置去重
        newMeta.getFileConfig().setFiles(distinctFiles(fileInfoList));
        newMeta.getModelConfig().setModels(distinctModels(modelInfoList));
    } else {
        // 1. 构造配置参数
        Meta.FileConfig fileConfig = new Meta.FileConfig();
        newMeta.setFileConfig(fileConfig);
        fileConfig.setSourceRootPath(sourceRootPath);
        List<Meta.FileConfig.FileInfo> fileInfoList = new ArrayList<>();
        fileConfig.setFiles(fileInfoList);
        fileInfoList.addAll(newFileInfoList);

        Meta.ModelConfig modelConfig = new Meta.ModelConfig();
        newMeta.setModelConfig(modelConfig);
        List<Meta.ModelConfig.ModelInfo> modelInfoList = new ArrayList<>();
        modelConfig.setModels(modelInfoList);
        modelInfoList.addAll(newModelInfoList);
    }

    // 2. 额外的输出配置
    if (templateMakerOutputConfig != null) {
        // 文件外层和分组去重
        if (templateMakerOutputConfig.isRemoveGroupFilesFromRoot()) {
            List<Meta.FileConfig.FileInfo> fileInfoList = newMeta.getFileConfig().getFiles();
            newMeta.getFileConfig().setFiles(TemplateMakerUtils.removeGroupFilesFromRoot(fileInfoList));
        }
    }

    // 2. 输出元信息文件
    FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(newMeta), metaOutputPath);
    return id;
}






 








































































 
 
 
 
 
 
 
 





再次执行测试,分组内的文件不会在外层文件中重复出现了,符合预期

ebb0d097df184587879bc99418ea26bb

4. 需求 - 控制跨域

允许用户传入 needCors 模型参数,控制跨域相关的文件 CorsConfig.java 是否生成

1. 编写配置

设置文件路径 CorsConfig.java、新增模型参数 needCors,注意还要给文件配置指定一个生成条件

{
    "id": 1,
    "fileConfig": {
        "files": [
            {
                "path": "src/main/java/com/yupi/springbootinit/config/CorsConfig.java",
                "condition": "needCors"
            }
        ]
    },
    "modelConfig": {
        "models": [
            {
                "fieldName": "needCors",
                "type": "boolean",
                "description": "是否开启跨域功能",
                "defaultValue": true
            }
        ]
    }
}





 
 






 







2. 支持给单文件指定条件

  1. 文件配置类 TemplateMakerFileConfig$FileInfoConfig,补充 condition 条件参数
@Data
public class TemplateMakerFileConfig {

    private List<FileInfoConfig> files;

    private FileGroupConfig fileGroupConfig;

    @NoArgsConstructor
    @Data
    public static class FileInfoConfig {

        private String path;

        private String condition;

        private List<FileFilterConfig> filterConfigList;
    }

    @Data
    public static class FileGroupConfig {

        private String condition;

        private String groupKey;

        private String groupName;
    }

}













 















  1. makeFileTemplate() 新增 fileConfig 对象的传递,支持从配置中取出 condition 并填充 fileInfo 对象
/**
 * 制作文件模板
 */
private static Meta.FileConfig.FileInfo makeFileTemplate(
        TemplateMakerModelConfig templateMakerModelConfig, String sourceRootPath, File inputFile,
        TemplateMakerFileConfig.FileInfoConfig fileInfoConfig) {

    // 要挖坑的文件绝对路径(用于制作模板)
    // 注意 win 系统需要对路径进行转义
    String fileInputAbsolutePath = inputFile.getAbsolutePath().replaceAll("\\\\", "/");
    String fileOutputAbsolutePath = fileInputAbsolutePath + ".ftl";

    // 文件输入输出相对路径(用于生成配置)
    String fileInputPath = fileInputAbsolutePath.replace(sourceRootPath + "/", "");
    String fileOutputPath = fileInputPath + ".ftl";

    // 使用字符串替换,生成模板文件
    String fileContent;
    // 如果已有模板文件,说明不是第一次制作,则在模板基础上再次挖坑
    boolean hasTemplateFile = FileUtil.exist(fileOutputAbsolutePath);
    if (hasTemplateFile) {
        fileContent = FileUtil.readUtf8String(fileOutputAbsolutePath);
    } else {
        fileContent = FileUtil.readUtf8String(fileInputAbsolutePath);
    }

    // 支持多个模型:对同一个文件的内容,遍历模型进行多轮替换
    TemplateMakerModelConfig.ModelGroupConfig modelGroupConfig = templateMakerModelConfig.getModelGroupConfig();
    String newFileContent = fileContent;
    String replacement;
    for (TemplateMakerModelConfig.ModelInfoConfig modelInfoConfig : templateMakerModelConfig.getModels()) {
        // 不是分组
        if (modelGroupConfig == null) {
            replacement = String.format("${%s}", modelInfoConfig.getFieldName());
        } else {
            // 是分组
            String groupKey = modelGroupConfig.getGroupKey();
            // 注意挖坑要多一个层级
            replacement = String.format("${%s.%s}", groupKey, modelInfoConfig.getFieldName());
        }
        // 多次替换
        newFileContent = StrUtil.replace(newFileContent, modelInfoConfig.getReplaceText(), replacement);
    }

    // 文件配置信息
    Meta.FileConfig.FileInfo fileInfo = new Meta.FileConfig.FileInfo();
    // 注意文件输入路径要和输出路径反转
    fileInfo.setInputPath(fileOutputPath);
    fileInfo.setOutputPath(fileInputPath);
    fileInfo.setCondition(fileInfoConfig.getCondition());
    fileInfo.setType(FileTypeEnum.FILE.getValue());
    fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());

    // 是否更改了文件内容
    boolean contentEquals = newFileContent.equals(fileContent);
    // 之前不存在模板文件,并且没有更改文件内容,则为静态生成
    if (!hasTemplateFile) {
        if (contentEquals) {
            // 输入路径没有 FTL 后缀
            fileInfo.setInputPath(fileInputPath);
            fileInfo.setGenerateType(FileGenerateTypeEnum.STATIC.getValue());
        } else {
            // 没有模板文件,需要挖坑,生成模板文件
            FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
        }
    } else if (!contentEquals) {
        // 有模板文件,且增加了新坑,生成模板文件
        FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
    }

    return fileInfo;
}





 











































 






















3. 测试执行

执行成功,查看生成的元信息配置,发现跨域文件和模型配置正确生成

8feb6b040d834094a8157deb145a132b
436bf969e6044e1d8f2d38b8a0492180

5. 需求 - 自定义Knife4jConfig

  • 先让用户输入 needDocs 参数,决定是否开启接口文档配置
  • 如果开启,再让用户输入一组参数,能够修改 Knife4jConfig 文件中的配置

实现思路:修改 Knife4jConfig 文件中的配置。eg:接口文档的标题、描述、版本号等

1. 完善ModelGroup配置

实现这个需求,给模型分组配置增加一些字段。eg:分组的类型、描述

  1. 修改 TemplateMakerModelConfig$ModelGroupConfig
@Data
public class TemplateMakerModelConfig {

    private List<ModelInfoConfig> models;

    private ModelGroupConfig modelGroupConfig;

    @NoArgsConstructor
    @Data
    public static class ModelInfoConfig {

        private String fieldName;

        private String type;

        private String description;

        private Object defaultValue;

        private String abbr;

        // 用于替换哪些文本
        private String replaceText;
    }

    @Data
    public static class ModelGroupConfig {

        private String condition;

        private String groupKey;

        private String groupName;

        private String type;

        private String description;
    }
}


































 

 


  1. 获取模型信息列表时,需要将配置中指定的分组信息传递给 groupModelInfo 对象
    • 修改 getModelInfoList(),通过 BeanUtil.copyProperties() 拷贝对象的属性值
private static List<Meta.ModelConfig.ModelInfo> getModelInfoList(TemplateMakerModelConfig templateMakerModelConfig) {

    // - 本次新增的模型配置列表
    List<Meta.ModelConfig.ModelInfo> newModelInfoList = new ArrayList<>();
    if (templateMakerModelConfig == null) {
        return newModelInfoList;
    }

    List<TemplateMakerModelConfig.ModelInfoConfig> models = templateMakerModelConfig.getModels();
    if (CollUtil.isEmpty(models)) {
        return newModelInfoList;
    }

    // - 转换为配置接受的 ModelInfo 对象
    List<Meta.ModelConfig.ModelInfo> inputModelInfoList = models.stream().map(modelInfoConfig -> {
        Meta.ModelConfig.ModelInfo modelInfo = new Meta.ModelConfig.ModelInfo();
        BeanUtil.copyProperties(modelInfoConfig, modelInfo);
        return modelInfo;
    }).collect(Collectors.toList());

    // - 如果是模型组
    TemplateMakerModelConfig.ModelGroupConfig modelGroupConfig = templateMakerModelConfig.getModelGroupConfig();

    if (modelGroupConfig != null) {
        // String condition = modelGroupConfig.getCondition();
        // String groupKey = modelGroupConfig.getGroupKey();
        // String groupName = modelGroupConfig.getGroupName();

        Meta.ModelConfig.ModelInfo groupModelInfo = new Meta.ModelConfig.ModelInfo();
        BeanUtil.copyProperties(modelGroupConfig, groupModelInfo);

        // groupModelInfo.setGroupKey(groupKey);
        // groupModelInfo.setGroupName(groupName);
        // groupModelInfo.setCondition(condition);

        // 模型全放到一个分组内
        groupModelInfo.setModels(inputModelInfoList);
        newModelInfoList.add(groupModelInfo);
    } else {
        // 不分组,添加所有的模型信息到列表
        newModelInfoList.addAll(inputModelInfoList);
    }
    return newModelInfoList;
}





























 














2. 编写配置

分 2 步去制作模板,不要让每次制作的模型参数混在同一组,更清晰一些

  1. 先控制接口文档文件是否生成
  2. 再指定修改接口文档内容的参数

  1. 先控制文件是否生成
{
    "id": 1,
    "fileConfig": {
        "files": [
            {
                "path": "src/main/java/com/yupi/springbootinit/config/Knife4jConfig.java",
                "condition": "needDocs"
            }
        ]
    },
    "modelConfig": {
        "models": [
            {
                "fieldName": "needDocs",
                "type": "boolean",
                "description": "是否开启接口文档功能",
                "defaultValue": true
            }
        ]
    }
}





 
 






 







  1. 再定义一组配置,控制接口文档文件的内容
{
    "id": 1,
    "fileConfig": {
        "files": [
            {
                "path": "src/main/java/com/yupi/springbootinit/config/Knife4jConfig.java",
                "condition": "needDocs"
            }
        ]
    },
    "modelConfig": {
        "modelGroupConfig": {
            "groupKey": "docsConfig",
            "groupName": "接口文档配置",
            "type": "DocsConfig",
            "description": "用于生成接口文档配置",
            "condition": "needDocs"
        },
        "models": [
            {
                "fieldName": "title",
                "type": "String",
                "description": "接口文档标题",
                "defaultValue": "接口文档",
                "replaceText": "接口文档"
            },
            {
                "fieldName": "description",
                "type": "String",
                "description": "接口文档描述",
                "defaultValue": "springboot-init",
                "replaceText": "springboot-init"
            },
            {
                "fieldName": "version",
                "type": "String",
                "description": "接口文档版本",
                "defaultValue": "1.0",
                "replaceText": "1.0"
            }
        ]
    }
}











 






 
























3. 测试执行

执行成功,查看生成的元信息配置,发现模型分组正确生成

93e291c5b25c4ef583083fedd6ac212b

6. 需求 - 自定义MySQL

允许用户传入一组 MySQL 数据库模型参数,修改 application.yml 配置文件中 MySQL 的 urlusernamepassword 的值

1. 编写配置

{
    "id": 1,
    "fileConfig": {
        "files": [
            {
                "path": "src/main/resources/application.yml"
            }
        ]
    },
    "modelConfig": {
        "modelGroupConfig": {
            "groupKey": "mysqlConfig",
            "groupName": "MySQL数据库配置",
            "type": "MysqlConfig",
            "description": "用于生成MySQL数据库配置"
        },
        "models": [
            {
                "fieldName": "url",
                "type": "String",
                "description": "地址",
                "defaultValue": "jdbc:mysql://localhost:3306/my_db",
                "replaceText": "jdbc:mysql://localhost:3306/my_db"
            },
            {
                "fieldName": "username",
                "type": "String",
                "description": "用户名",
                "defaultValue": "root",
                "replaceText": "root"
            },
            {
                "fieldName": "password",
                "type": "String",
                "description": "密码",
                "defaultValue": "123456",
                "replaceText": "123456"
            }
        ]
    }
}










 





 
























2. 测试执行

执行成功,模型分组正确生成

ad0474a3d39747119e47289b6ea31acd

7. 需求 - 控制Redis

传入 needRedis 模型参数,控制是否开启和 Redis 相关代码。控制修改 application.ymlpom.xmlMainApplication.java 中的部分代码

  • 这个需求比较定制化,因为每个文件和 Redis 有关的代码都不一样,所以人工修改模板文件并“挖坑”

1. ftl修改

<#if needRedis>
  # Redis 配置
  redis:
    database: 1
    host: localhost
    port: 6379
    timeout: 5000
    password: 123456
</#if>
@SpringBootApplication<#if !needRedis>(exclude = {RedisAutoConfiguration.class})</#if>
<#if needRedis>
<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
</#if>
 









 

2. 编写配置

依然使用工具来生成元信息配置

{
    "id": 1,
    "fileConfig": {
        "files": [
            {
                "path": "src/main/resources/application.yml"
            },
            {
                "path": "src/main/java/com/yupi/springbootinit/MainApplication.java"
            },
            {
                "path": "pom.xml"
            }
        ]
    },
    "modelConfig": {
        "models": [
            {
                "fieldName": "needRedis",
                "type": "boolean",
                "description": "是否开启Redis功能",
                "defaultValue": true
            }
        ]
    }
}





 


 


 






 







3. 测试执行

执行成功,控制 Redis 的模型参数正确生成

515d47ed6b1e45c88202a8de677288f0

8. 需求 - 控制ES

传入 needEs 模型参数,控制是否开启 Elasticsearch 相关的代码。需要修改和 Elasticsearch 相关的代码

  • PostControllerPostServicePostServiceImplapplication.yml 文件的部分代码
  • needEs 控制 PostEsDTO 文件是否生成

1. ftl修改

<#if needEs>
/**
 * 分页搜索(从 ES 查询)
 *
 * @param postQueryRequest
 * @return
 */
@PostMapping("/search/page")
public BaseResponse<Page<Post>> searchPostByPage(@RequestBody PostQueryRequest postQueryRequest) {
    long size = postQueryRequest.getPageSize();
    // 限制爬虫
    ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
    Page<Post> postPage = postService.searchFromEs(postQueryRequest);
        return ResultUtils.success(postPage);
}
</#if>
 














 
<#if needEs>
/**
 * 从 ES 查询
 *
 * @param postQueryRequest
 * @return
 */
Page<Post> searchFromEs(PostQueryRequest postQueryRequest);
</#if>
 







 
<#if needEs>
@Override
public Page<Post> searchFromEs(PostQueryRequest postQueryRequest) {
    Long id = postQueryRequest.getId();
    Long notId = postQueryRequest.getNotId();
    String searchText = postQueryRequest.getSearchText();
    String title = postQueryRequest.getTitle();
    String content = postQueryRequest.getContent();
    List<String> tagList = postQueryRequest.getTags();
    List<String> orTagList = postQueryRequest.getOrTags();
    Long userId = postQueryRequest.getUserId();
    // es 起始页为 0
    long current = postQueryRequest.getCurrent() - 1;
    long pageSize = postQueryRequest.getPageSize();
    BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
    // 过滤
    boolQueryBuilder.filter(QueryBuilders.termQuery("isDelete", 0));
    if (id != null) {
        boolQueryBuilder.filter(QueryBuilders.termQuery("id", id));
    }
    if (notId != null) {
        boolQueryBuilder.mustNot(QueryBuilders.termQuery("id", notId));
    }
    if (userId != null) {
        boolQueryBuilder.filter(QueryBuilders.termQuery("userId", userId));
    }
    // 必须包含所有标签
    if (CollectionUtil.isNotEmpty(tagList)) {
        for (String tag : tagList) {
            boolQueryBuilder.filter(QueryBuilders.termQuery("tags", tag));
        }
    }
    // 包含任何一个标签即可
    if (CollectionUtil.isNotEmpty(orTagList)) {
        BoolQueryBuilder orTagBoolQueryBuilder = QueryBuilders.boolQuery();
        for (String tag : orTagList) {
            orTagBoolQueryBuilder.should(QueryBuilders.termQuery("tags", tag));
        }
        orTagBoolQueryBuilder.minimumShouldMatch(1);
        boolQueryBuilder.filter(orTagBoolQueryBuilder);
    }
    // 按关键词检索
    if (StringUtils.isNotBlank(searchText)) {
        boolQueryBuilder.should(QueryBuilders.matchQuery("title", searchText));
        boolQueryBuilder.should(QueryBuilders.matchQuery("description", searchText));
        boolQueryBuilder.should(QueryBuilders.matchQuery("content", searchText));
        boolQueryBuilder.minimumShouldMatch(1);
    }
    // 按标题检索
    if (StringUtils.isNotBlank(title)) {
        boolQueryBuilder.should(QueryBuilders.matchQuery("title", title));
        boolQueryBuilder.minimumShouldMatch(1);
    }
    // 按内容检索
    if (StringUtils.isNotBlank(content)) {
        boolQueryBuilder.should(QueryBuilders.matchQuery("content", content));
        boolQueryBuilder.minimumShouldMatch(1);
    }
    // 分页
    PageRequest pageRequest = PageRequest.of((int) current, (int) pageSize);
    // 构造查询
    NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder)
            .withPageable(pageRequest).build();
    SearchHits<PostEsDTO> searchHits = elasticsearchRestTemplate.search(searchQuery, PostEsDTO.class);
    Page<Post> page = new Page<>();
    page.setTotal(searchHits.getTotalHits());
    List<Post> resourceList = new ArrayList<>();
    // 查出结果后,从 db 获取最新动态数据(比如点赞数)
    if (searchHits.hasSearchHits()) {
        List<SearchHit<PostEsDTO>> searchHitList = searchHits.getSearchHits();
        List<Long> postIdList = searchHitList.stream().map(searchHit -> searchHit.getContent().getId())
                .collect(Collectors.toList());
        List<Post> postList = baseMapper.selectBatchIds(postIdList);
        if (postList != null) {
            Map<Long, List<Post>> idPostMap = postList.stream().collect(Collectors.groupingBy(Post::getId));
            postIdList.forEach(postId -> {
                if (idPostMap.containsKey(postId)) {
                    resourceList.add(idPostMap.get(postId).get(0));
                } else {
                    // 从 es 清空 db 已物理删除的数据
                    String delete = elasticsearchRestTemplate.delete(String.valueOf(postId), PostEsDTO.class);
                    log.info("delete post {}", delete);
                }
            });
        }
    }
    page.setRecords(resourceList);
    return page;
}
</#if>
 
























































































 
<#if needEs>
  # Elasticsearch 配置
  elasticsearch:
    uris: http://localhost:9200
    username: root
    password: 123456
</#if>
 





 

2. 编写配置

{
    "id": 1,
    "fileConfig": {
        "files": [
            {
                "path": "src/main/java/com/yupi/springbootinit/model/dto/post/PostEsDTO.java",
                "condition": "needPost && needEs"
            }
        ]
    },
    "modelConfig": {
        "models": [
            {
                "fieldName": "needEs",
                "type": "boolean",
                "description": "是否开启ES功能",
                "defaultValue": true
            }
        ]
    }
}






 






 







3. 测试执行

控制 Elasticsearch 的模型参数正确生成

5c5d3c2d2b0440228c1b15df69afebd1

但是,PostEsDTO 文件配置却没有 condition 条件

  • 因为 PostEsDTO 文件已经属于 Post 组,会被组内已有的配置覆盖
  • 手动调整下 PostEsDTO 的生成策略,将它移动到组外,并指定 condition 条件,同时开启 Post 和 Es 时才生成
3dcf33bfa3a34d11b69a766453817776
{
    "inputPath": "src/main/java/com/yupi/springbootinit/model/dto/post/PostEsDTO.java.ftl",
    "outputPath": "src/main/java/com/yupi/springbootinit/model/dto/post/PostEsDTO.java",
    "type": "file",
    "generateType": "dynamic",
    "condition": "needPost && needEs"
}





 

4. 测试成果

至此,所有的需求都已经实现,接下来到了激动人心的时刻,终于可以验证成果了!!!

1. 制作生成器

  1. 首先将已生成的 meta.json 文件,复制为 yuzi-generator-maker/resource/springboot-init-meta.json
7a47ba4c35384836b82289a5f87822a5
  1. 然后修改 MetaManager 类,加载该文件
public class MetaManager {

    private static volatile Meta meta;

    private MetaManager() {
        // 私有构造函数,防止外部实例化
    }

    public static Meta getMetaObject() {
        if (meta == null) {
            synchronized (MetaManager.class) {
                if (meta == null) {
                    meta = initMeta();
                }
            }
        }
        return meta;
    }

    private static Meta initMeta() {
        // String metaJson = ResourceUtil.readUtf8Str("meta.json");
        String metaJson = ResourceUtil.readUtf8Str("springboot-init-meta.json");
        Meta newMeta = JSONUtil.toBean(metaJson, Meta.class);
        // 校验和处理默认值
        MetaValidator.doValidAndFill(newMeta);
        return newMeta;
    }
}





















 






  1. 最后执行工具 main(),制作生成器
    • 结果,CommandLine 对象冲突啦!
    • 修改 GenerateCommand.java.ftl 的命令调用生成代码,根据模型的 groupKey 生成命令行对象名称
a7cf973aedbb4af18b4273d6490934f0
<#-- 生成命令调用 -->
<#macro generateCommand indent modelInfo>
${indent}System.out.println("输入${modelInfo.groupName}配置:");
${indent}CommandLine ${modelInfo.groupKey}CommandLine = new CommandLine(${modelInfo.type}Command.class);
${indent}${modelInfo.groupKey}CommandLine.execute(${modelInfo.allArgsStr});
</#macro>



 


  1. 再次执行 main()
    • 配置文件没有手动指定 inputRootPath,在 MetaValidator 校验器中自动生成了,而生成的逻辑有一些错误
    • 需要修改默认生成的路径,确保使用 / 符号分隔目录
67799b3c7c304150b10f1db80843e516
19b37db65ef447d7a2a70aaf894147f0
    public static void validAndFillFileConfig(Meta meta) {
        // fileConfig 默认值
        Meta.FileConfig fileConfig = meta.getFileConfig();
        if (fileConfig == null) {
            return;
        }
        // sourceRootPath:必填
        String sourceRootPath = fileConfig.getSourceRootPath();
        if (StrUtil.isBlank(sourceRootPath)) {
            throw new MetaException("未填写 sourceRootPath");
        }
        // inputRootPath:.source + sourceRootPath 的最后一个层级路径
        String inputRootPath = fileConfig.getInputRootPath();
        String defaultInputRootPath = ".source/" + FileUtil.getLastPathEle(Paths.get(sourceRootPath)).getFileName().toString();
        if (StrUtil.isEmpty(inputRootPath)) {
            fileConfig.setInputRootPath(defaultInputRootPath);
        }












 
 
 
 
 
  1. 打包成功!得到了一个 Spring Boot 生成器
b26f6341ae5a4a338f4c2d43629f2baa

2. 测试使用

  1. 查看生成命令的帮助手册
59696f2b9d5b4eb9979447c3378221f3
  1. 查看模型参数
9220dc838b0347338c7ce1292e6e98c9
  1. 查看模型列表文件
57c6dc70ab654f67ab7017416cf905f6
  1. 生成文件
./generator generate --needPost=false
d0313fd32e8443b480fd5853f7cc68c4
  • 查看生成的代码,没有产生 Post 相关文件,符合预期
78fc10285b7c412ca6a9495351e00f35
# `needRedis=false`,`needPost` 为默认值 true
./generator generate --needRedis=false
  • 结果生成报错,PostMapper.xml 文件错误
    • 原因:MyBatis 动态参数语法字符串和 FreeMarker 模板的参数替换语法冲突
    • 使用 <#noparse> 语法可以设置某些字符串不被 FreeMarker 解析,修改
b11c72672f3d45faa7e9bc45fb2b077d
<select id="listPostWithDelete" resultType="${basePackage}.springbootinit.model.entity.Post">
    select *
    from post
    where updateTime >= <#noparse>#{minUpdateTime}</#noparse>
</select>
  1. 再次运行命令,这次可以成功生成。更多命令测试 💯
# 不会生成 Elasticsearch 代码
./generator generate --needEs=false

# 控制文档是否生成
./generator generate --needDocs=false
872c0bdbac294256937f226b3903f120