ape-frame spring脚手架demo,模仿start.spring.io,文件压缩

ape-frame 脚手架

ape-frame 脚手架,在跟随 “经典鸡翅”学习ape-frame脚手架之后,一直想添加一个前端页面,就像start.spring.io一样,不过一直没什么时间弄,但是想啥来啥,实习公司的这个数字化部门刚成立一年,而且带我java开发的leader才进来三四个月,没有符合公司项目结构的脚手架,就让我来写了,写完之后,又给ape-frame写一个,代码在ape-frame下的ape-server gitee地址

  • bean
public class UserOptions {
    //是否使用系统默认值
    private Integer isDefault;
    //maven parent
    private Long parentId;
    //packageName
    private String packaging;
    //maven groupId
    private String groupId;
    private String name;
    private String description;
    //maven artifactId
    private String artifactId;
    //maven version
    private String version;
    //dependencies id
    private List<Long> ids;
    //application name
    private String application;
}
  • 创建一个抽象类,声明一个最基本的start方法,模板策略
  • 使用java自带的zip工具
import airport.cargos.com.spring.starter.bean.UserOptions;
import airport.cargos.com.spring.starter.conf.UserProperty;
import com.alibaba.fastjson.JSONObject;
import com.google.protobuf.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;

import java.io.*;
import java.nio.file.Paths;
import java.util.List;
import java.util.zip.CRC32;
import java.util.zip.CheckedOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

@Slf4j
public abstract class AbstractGeneratorHandler<T> {
    @Autowired
    private UserProperty userProperty;
    private static String tempDir = "temp";
	// 一开始的逻辑错误,导致这个start方法有点不好,因为一开始不知道zip可以直接写入数据,导致生成一个普通文件夹又根据这个文件夹压缩一遍,增加了磁盘IO操作
    public T start(UserOptions userOptions) throws ServiceException, IOException {
        // 生成项目文件
        String filePath = generateProjectFiles(userOptions);
        // 创建压缩文件
        String zipFilePath = zipCompress(filePath);
        // 将压缩文件直接返回给用户
        return returnZipToUser(zipFilePath);
    }
//创建项目结构可以自行扩展
    protected String generateProjectFiles(UserOptions userOptions) throws FileNotFoundException {
        // 获取项目根目录
        String projectRoot = System.getProperty("user.dir") + File.separator + tempDir;

        String project = "project_" + System.currentTimeMillis();
        String src = projectRoot + File.separator + project + File.separator + "src";
        // 生成src目录及其子目录
        generateDirectory(projectRoot, project);
        generateDirectory(projectRoot + File.separator + project, "src");
        generateDirectory(src, "main");
        generateDirectory(src, "test");
        generateDirectory(src + File.separator + "main", "java");
        generateDirectory(src + File.separator + "main", "resources");

        String packPath = src + File.separator + "main" + File.separator + "java" +
                File.separator + userOptions.getPackaging().replace(".", File.separator);
        //生成package
        //生成基本包
        generateDirectory(packPath, "controller");
        generateDirectory(packPath, "service");
        generateDirectory(packPath, "mapper");
        generateDirectory(packPath, "bean");
        generateDirectory(packPath, "conf");
        generateDirectory(packPath, "base");
        generateDirectory(packPath, "constant");
        // 生成Application.Java文件
        generateResourceFile(packPath, "Application.java", generateApplicationContent(userOptions.getPackaging()));

        // 生成application.yml文件
        generateResourceFile(src + File.separator + "main" + File.separator + "resources",
                "application.yml", generateYamlContent(userOptions));
        generateResourceFile(src, "pom.xml", generatePomContent(userOptions));
        return projectRoot + File.separator + project;
    }

    private void generateDirectory(String parentPath, String directoryName) {
        File directory = new File(parentPath + File.separator + directoryName);
        if (!directory.exists()) {
            if (directory.mkdirs()) {
                log.info("directory generated successfully: {}", directory.getAbsolutePath());
            }
        }
    }
//生成文件
    private void generateResourceFile(String parentPath, String fileName, String content) throws FileNotFoundException {
        String filePath = parentPath + File.separator + fileName;
        try (PrintWriter writer = new PrintWriter(filePath)) {
            writer.println(StringUtils.hasLength(content) ? content : "empty");
            log.info("file generated successfully: {}", filePath);
        }
    }
//启动类内容
    private String generateApplicationContent(String packageName) {
        // 生成 Application.java 文件的内容,这里简单示例一下
        return "package " + packageName + ";

" +
                "import org.springframework.boot.SpringApplication;
" +
                "import org.springframework.boot.autoconfigure.SpringBootApplication;
" +
                "@SpringBootApplication" +
                "public class Application {
" +
                "    public static void main(String[] args) {
" +
                "        //配置异步日志上下文选择器,不配置的话,等于没开异步
" +
                "        System.setProperty("log4jContextSelector","org.apache.logging.log4j.core.async.AsyncLoggerContextSelector");
" +
                "        SpringApplication.run(Application.class);
" +
                "    }
" +
                "}
";
    }
//yml内容
    private String generateYamlContent(UserOptions userOptions) {
        // 生成 application.yml 文件的内容,这里简单示例一下
        return "server:
" +
                "  port: 8081" +
                "spring:
" +
                "  application:
" +
                "    name: " + userOptions.getApplication() + "
";
    }
//pom 内容
    private String generatePomContent(UserOptions userOptions) {
        String pom = "<?xml version="1.0" encoding="UTF-8"?>
" +
                "<project xmlns="http://maven.apache.org/POM/4.0.0"
" +
                "         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
" +
                "         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
";
        pom += appendParent(userOptions);
        pom += "    <modelVersion>4.0.0</modelVersion>
" +
                "    <groupId>" + userOptions.getGroupId() + "</groupId>
" +
                "    <artifactId>" + userOptions.getArtifactId() + "</artifactId>
" +
                "    <version>" + userOptions.getVersion() + "</version>
";
        pom += StringUtils.hasLength(userOptions.getName()) ? "    <name>" + userOptions.getName() + "</name>" : "";
        pom += StringUtils.hasLength(userOptions.getDescription()) ? "    <description>" + userOptions.getDescription() + "</description>" : "";
        pom += "
" +
                "    <properties>
" +
                "        <java.version>1.8</java.version>
" +
                "        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
" +
                "        <maven.compiler.source>8</maven.compiler.source>
" +
                "        <maven.compiler.target>8</maven.compiler.target>
" +
                "        <maven.plugin.version>3.8.0</maven.plugin.version>
" +
                "    </properties>
" +
                "
";
        pom += appendDependency(userOptions);
        pom += "
" +
                "    <build>
" +
                "        <plugins>
" +
                "            <plugin>
" +
                "                <groupId>org.springframework.boot</groupId>
" +
                "                <artifactId>spring-boot-maven-plugin</artifactId>
" +
                "                <version>2.7.17</version>
" +
                "                <configuration>
" +
                "                    <mainClass>
" +
                "                        " + userOptions.getPackaging() + ".Application
" +
                "                    </mainClass>
" +
                "                </configuration>
" +
                "                <executions>
" +
                "                    <execution>
" +
                "                        <goals>
" +
                "                            <goal>repackage</goal>
" +
                "                        </goals>
" +
                "                    </execution>
" +
                "                </executions>
" +
                "            </plugin>
" +
                "            <plugin>
" +
                "                <groupId>org.apache.maven.plugins</groupId>
" +
                "                <artifactId>maven-compiler-plugin</artifactId>
" +
                "                <version>${maven.plugin.version}</version>
" +
                "                <configuration>
" +
                "                    <source>${maven.compiler.source}</source>
" +
                "                    <target>${maven.compiler.target}</target>
" +
                "                    <encoding>UTF-8</encoding>
" +
                "                </configuration>
" +
                "            </plugin>
" +
                "        </plugins>
" +
                "        <resources>
" +
                "            <resource>
" +
                "                <directory>src/main/resources</directory>
" +
                "                <filtering>true</filtering>
" +
                "            </resource>
" +
                "            <resource>
" +
                "                <directory>src/main/java</directory>
" +
                "                <includes>
" +
                "                    <include>**/*.xml</include>
" +
                "                </includes>
" +
                "            </resource>
" +
                "        </resources>
" +
                "    </build>" +
                "</project>";
        return pom;
    }
//根据所选依赖的id添加依赖内容
    public String appendDependency(UserOptions userOptions) {
        List<JSONObject> dependencyList = dependencies(userOptions);
        String dependencies = "    <dependencies>
";
        for (JSONObject dependency : dependencyList) {
            String version = dependency.getString("version");
            dependencies += "        <dependency>
" +
                    "            <groupId>" + dependency.getString("groupId") + "</groupId>
" +
                    "            <artifactId>" + dependency.getString("artifactId") + "</artifactId>
";
            dependencies += StringUtils.hasLength(version) ? "            <version>" + version + "</version>" : "" +
                    "        </dependency>
";
        }
        dependencies += "    </dependencies>";
        return dependencies;
    }
//添加parent
    public String appendParent(UserOptions userOptions) {
        JSONObject parent = parent(userOptions);
        if (parent == null) {
            return "";
        }
        String parentStr = "    <parent>
" +
                "        <artifactId>" + parent.getString("artifactId") + "</artifactId>
" +
                "        <groupId>" + parent.getString("groupId") + "</groupId>
" +
                "        <version>" + parent.getString("version") + "</version>
" +
                "    </parent>";
        return parentStr;
    }

	//service实现之后,去相应数据库查数据
    //包含业务逻辑 设置为抽象方法
    public abstract List<JSONObject> dependencies(UserOptions userOptions);

    public abstract JSONObject parent(UserOptions userOptions);

    // 在实际应用中,你需要根据你的服务框架和需求将压缩文件返回给用户
    // 例如,如果是Web应用,可以使用 HttpServletResponse 输出文件内容
    protected abstract T returnZipToUser(String file) throws IOException;

//文件压缩
    public static String zipCompress(String project) throws ServiceException, IOException {
        String hallFilePath = project;
        int i = hallFilePath.lastIndexOf(".");
        hallFilePath = i == -1 ? hallFilePath : hallFilePath.substring(0, i);
        compress(Paths.get(hallFilePath).toString(), hallFilePath + ".zip");
        log.info("zip generated successfully: {}", hallFilePath + ".zip");
        return hallFilePath + ".zip";
    }

    public static void compress(String fromPath, String toPath) throws IOException, ServiceException {
        File fromFile = new File(fromPath);
        File toFile = new File(toPath);
        if (!fromFile.exists()) {
            throw new ServiceException(fromPath + "不存在!");
        }
        try (FileOutputStream outputStream = new FileOutputStream(toFile);
             CheckedOutputStream checkedOutputStream = new CheckedOutputStream(outputStream, new CRC32());
             ZipOutputStream zipOutputStream = new ZipOutputStream(checkedOutputStream)) {
            String baseDir = "";
            compress(fromFile, zipOutputStream, baseDir);
        }
    }

    private static void compress(File file, ZipOutputStream zipOut, String baseDir) throws IOException {
        if (file.isDirectory()) {
            compressDirectory(file, zipOut, baseDir);
        } else {
            compressFile(file, zipOut, baseDir);
        }
    }

    private static void compressFile(File file, ZipOutputStream zipOut, String baseDir) throws IOException {
        if (!file.exists()) {
            return;
        }
        if (StringUtils.hasLength(baseDir)) {
            baseDir = baseDir.substring(baseDir.indexOf(File.separator), baseDir.length());
        }
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
            ZipEntry entry = new ZipEntry(baseDir + file.getName());
            zipOut.putNextEntry(entry);
            int count;
            byte[] data = new byte[1024];
            while ((count = bis.read(data)) != -1) {
                zipOut.write(data, 0, count);
            }
        }
    }

    private static void compressDirectory(File dir, ZipOutputStream zipOut, String baseDir) throws IOException {
        File[] files = dir.listFiles();
        if (ArrayUtils.isNotEmpty(files)) {
            for (File file : files) {
                compress(file, zipOut, baseDir + dir.getName() + File.separator);
            }
        }
    }
}
  • service 继承 抽象类重写抽象方法
package airport.cargos.com.spring.starter.service;

import airport.cargos.com.spring.starter.bean.Dependency;
import airport.cargos.com.spring.starter.bean.ProjectZipPath;
import airport.cargos.com.spring.starter.bean.UserOptions;
import airport.cargos.com.spring.starter.constant.Result;
import airport.cargos.com.spring.starter.constant.StateEnum;
import airport.cargos.com.spring.starter.handler.AbstractGeneratorHandler;
import airport.cargos.com.spring.starter.mapper.DependencyMapper;
import airport.cargos.com.spring.starter.mapper.ProjectZipPathMapper;
import com.alibaba.fastjson.JSONObject;
import com.google.protobuf.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.*;
import java.util.stream.Collectors;

@Service
@Slf4j
public class GeneratorService extends AbstractGeneratorHandler<Object>  {

    @Autowired
    private DependencyMapper dependencyMapper;
    @Autowired
    private ProjectZipPathMapper projectZipPathMapper;

//下载文件接口调用该方法
    public Object startDownload(UserOptions userOptions) {
        try {
             super.start(userOptions);
             log.info("response success");
        } catch (ServiceException e) {
            log.error("serviceException: {}",e.getMessage(),e);
        } catch (FileNotFoundException e) {
            log.error("FileNotFoundException {}", e.getMessage(),e);
        }catch (IOException e) {
            log.error("IoException: {}",e.getMessage(),e);
        } catch (Exception e){
            log.error("Exception: {}",e.getMessage(),e);
        }
        return null;
    }

//response 返回 zip文件
    @Override
    protected Result returnZipToUser(String filePath) throws IOException {
        String fileName = filePath.substring(filePath.lastIndexOf(File.separator) + 1);

        HttpServletResponse response = ((ServletRequestAttributes) (RequestContextHolder.getRequestAttributes())).getResponse();
        //指定内容为文件类型,浏览器提供下载操作
        response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
        //以附件的方式展示
        response.setHeader("Content-Disposition", "attachment; filename="+fileName);

        try(InputStream inputStream = new FileInputStream(filePath);
            ServletOutputStream outputStream = response.getOutputStream()){
            byte[] buffer = new byte[1024];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
            }
        }
        return null;
    }
//查询依赖
    @Override
    public List<JSONObject> dependencies(UserOptions userOptions) {
        List<Long> parentIds = new ArrayList<>();
        List<JSONObject> jsonObjects = new ArrayList<>();
        if(CollectionUtils.isEmpty(userOptions.getIds())){
            return jsonObjects;
        }
        List<Dependency> dependencies = dependencyMapper.selectBatchIds(userOptions.getIds()).stream().filter(dependency -> !StateEnum.IS.getCode().equals(dependency.getIsParent())).collect(Collectors.toList());

        Long parentId = userOptions.getParentId();
        while(parentId !=null && parentId>0){
            Dependency dependency = dependencyMapper.selectById(parentId);
            if(Objects.isNull(dependency)){
                parentId = dependency.getParentId();
                if(parentId!=0){
                    parentIds.add(parentId);
                }
            }
        }
        JSONObject jsonObject = null;
        for(Dependency dependency: dependencies){
            jsonObject=new JSONObject();
            jsonObject.put("groupId",dependency.getGroupId());
            jsonObject.put("artifactId",dependency.getArtifactId());
            if(!parentIds.contains(dependency.getParentId())){
                jsonObject.put("version",dependency.getVersion());
            }
            jsonObjects.add(jsonObject);
        }
        return jsonObjects;
    }
//填充parent信息
    @Override
    public JSONObject parent(UserOptions userOptions) {
        Long parentId = userOptions.getParentId();
        Dependency dependency = null;
        JSONObject jsonObject = null;
        if(parentId!=null&&parentId!=0){
            jsonObject = new JSONObject();
            dependency = dependencyMapper.selectById(parentId);
            jsonObject.put("groupId",dependency.getGroupId());
            jsonObject.put("artifactId",dependency.getArtifactId());
            jsonObject.put("version",dependency.getVersion());
        }
        return jsonObject;
    }

// 获取当前配置下载码接口调用该方法
    public String getCode(UserOptions userOptions) throws IOException, ServiceException {
        // 生成项目文件
        String filePath = generateProjectFiles(userOptions);
        // 创建压缩文件
        String zipFilePath = zipCompress(filePath);
        //将路径存储到数据库
        String uuid = UUID.randomUUID().toString().replace("-", "");
        ProjectZipPath projectZipPath = new ProjectZipPath();
        projectZipPath.setUuid(uuid);
        projectZipPath.setPath(zipFilePath);
        projectZipPath.setCreateTime(new Date());
        projectZipPath.setModifyTime(new Date());
        projectZipPathMapper.insert(projectZipPath);
        return uuid;
    }
//根据下载码下载接口调用该方法
    public void downloadByUuid(String uuid) throws IOException {
        //根据uuid找到路径
        ProjectZipPath projectZipPath = projectZipPathMapper.selectById(uuid);
        if(projectZipPath==null){
            throw new FileNotFoundException("not found file by the uuid");
        }
        returnZipToUser(projectZipPath.getPath());
    }
}
  • 不足,待优化

    • 多余的IO操作:生成文件夹后压缩该文件夹的逻辑应该改成生成压缩文件
    • 只实现start.spring.io的自定义基础信息和查询依赖(自己数据库管理的),配置分享(只使用下载码uuid简单代替),没有实现自定义文件信息功能
    • 压缩文件的方法应该放到ape-common-tool包下
  • 解决方案

    • 直接将需要生成的文件内容输入到压缩文件中,省略生成文件的这个操作
    private void generateResourceFile(ZipOutputStream zipOut, String parentPath, String fileName, String content) throws IOException {
    String filePath = parentPath + File.separator + fileName;
    ZipEntry entry = new ZipEntry(filePath);
    zipOut.putNextEntry(entry);
    try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(content.getBytes());
         BufferedInputStream bis = new BufferedInputStream(byteArrayInputStream)) {
        byte[] buffer = new byte[1024];
        int count;
        while ((count = bis.read(buffer)) != -1) {
            zipOut.write(buffer, 0, count);
        }
    }
    zipOut.closeEntry();
    }