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(); }