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