Java实现上传Nexus库
因为有部分开发人不懂 maven 配置,上传私库 jar 包混乱。多模块开发只要某个 jar 包上传缺因不懂配置,将所有 jar 包上传等其它非功能性因素,需要我这个系统开发这个功能(编不下去了,我只知道有这个需求,并且我得干┭┮﹏┭┮)
前言
这需求看起来真简单,用起来也简单,idea 打开 maven 点击 deploy 即可。所以我将使用和 idea 一样的方式通过 mvn 命令上传 Nexus 私库。而不是使用 Nexus 的 Api 。且使用上肯定无法达到 idea 那样方便。
Maven 上传 Nexus 库命令
上传 jar / pom 文件
mvn deploy:deploy-file //上传命令使用的模块
-DgroupId= com.cn // 上传后所在的文件夹路径 .变/
-DartifactId= demo // 上传后模块所在的文件夹
-Dversion= 1.0.0 // 上传后的模块版本
-Dpackaging=jar // 上传文件类型 jar或者pom
-Dfile= E:\\demo.jar // 需要上传的文件地址
-DpomFile= E:\\pom.xml // 如果上传 pom 文件则必须增加此项。
-Durl= http://127.0.0.1:8081/#browse/browse:release // 上传的私库地址
-DrepositoryId= release //对应 setting.xml 文件内的 servers下的server下的id
--settings E:\\maven\\conf\\setting.xml //可以指定指定的 setting.xml
以上就是 maven 的 deploy-file 模块的常用命令。全部命令请参考 官方文档。增加了 pomFile 属性,是可以减少groupId artifactId version三个参数的定义的。因为 pom.xml 文件里面有。但有些 pom.xml 文件内没有 groupId 默认继承父模块,或者 version 版本号为变量以便维护全局统一,这时则需要增加以上参数,否则会上传版本号为变量的模块。
实现思路与代码
完整的代码不利于讲解,零碎的代码不利于复制粘贴组合增加时间成本。但如果你不懂,那么看似你在节约时间,实则可能在浪费时间
准备一个实体包
该包存储 mvn 命令上传所需的变量值
import lombok.Data;
import java.util.List;
/**
* <P>
* maven上传数据
* </p>
*
* @author 昔日织
* @since 2021-07-07
*/
@Data
public class MavenUploadData {
/** 组id */
private String groupId;
/** 工件id */
private String artifactId;
/** 版本 */
private String version;
/** 文件类型 */
private String packaging;
/** 文件路径 */
private String filePath;
/** pom文件路径 */
private String filePomPath;
/** 私有库url */
private String url;
/** 库id与账号id相同 */
private String repositoryId;
/** 备用配置文件路径 */
private String settingPath;
/** 命令执行结果 */
private List<String> commands;
}
实现 jar 包的解压获取信息
尽量减少用户使用上的额外操作,所以需要读取 jar 包内的 pom.xml 文件以获取需要的信息
// 将传入进来的File对象file变量,通过ZipFile进行解压。找到出pom.xml文件,获取该文件的 InputStream
try (ZipFile zipFile = new ZipFile(file)) {
Enumeration<? extends ZipEntry> entries = zipFile.entries();
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
if (entry.getName().endsWith("pom.xml")) {
InputStream inputStream = zipFile.getInputStream(entry);
break;
}
}
} catch (IOException ex) {
System.out.println(ex.toString());
}
解析 pom.xml 文件内容
因为已经拿到了 InputStream 。所以获取内容的方式可以使用 Scanner 也可以使用其他流读取方式,思路都是先转字符串,然后对字符串进行匹配截取
//Scanner
StringBuilder stringBuilder = new StringBuilder(10240);
String groupId;
String artifactId;
String version;
boolean hasParent = false;
boolean enterParent = false;
// zipFile和entry 是前面所说的变量
try (Scanner scanner = new Scanner(zipFile.getInputStream(entry), "US-ASCII")) {
while (scanner.hasNextLine()) {
String line = scanner.nextLine();
if (line.contains("parent")) {
enterParent = !enterParent;
hasParent = true;
} else if (line.contains("<groupId>")) {
groupId = line;
} else if (!enterParent && line.contains("<artifactId>")) {
artifactId = line;
} else if (line.contains("<version>")) {
version = line;
} else if (!hasParent && groupId != null && artifactId != null && version != null) {
break;
} else if (line.contains("<dependencies>") || line.contains("<dependency>") || line.contains("<properties>") || line.contains("<profiles>") || line.contains("<plugins>")) {
break;
}
}
if (groupId != null && artifactId != null && version != null) {
System.out.println("<dependency>");
System.out.println(groupId);
System.out.println(artifactId);
System.out.println(version);
System.out.println("</dependency>");
} else {
stringBuilder.append("pom.xml解析异常,当前jar文件是" + file.getCanonicalPath() + ",解析失败的文件是" + entry.getName());
}
}
以上是使用 Scanner 方式获取数据,除了上面代码,数据获取后还需要去除空,截取 version 等主体内容才可使用。
我最后使用的是 Hutool 工具包中的 XmlUtil,该工具包蛮好用的,而且后期的文件操作使用起来也很方便,建议使用。
/**
* pom xml 文件解析
*
* @param inputStream 输入流
* @return {@link MavenUploadData }
*/
public MavenUploadData pomXml(InputStream inputStream){
Document document = XmlUtil.readXML(inputStream);
Element documentElement = document.getDocumentElement();
Element groupId = XmlUtil.getElement(documentElement, "groupId");
Element artifactId = XmlUtil.getElement(documentElement, "artifactId");
Element version = XmlUtil.getElement(documentElement, "version");
Element parent = XmlUtil.getElement(documentElement, "parent");
if(groupId == null){ //判断外面是否有该节点,没有则使用父级的
groupId = XmlUtil.getElement(parent,"groupId");
}
if(version == null){ //判断外面是否有该节点,没有则使用父级的
version = XmlUtil.getElement(parent,"version");
}
MavenUploadData mavenUploadData = new MavenUploadData();
mavenUploadData.setGroupId(groupId.getTextContent());
mavenUploadData.setArtifactId(artifactId.getTextContent());
mavenUploadData.setVersion(version.getTextContent());
return mavenUploadData;
}
生成用户的 setting.xml 文件
不同用户权限不同,且 nexus 上记录不同,不能使用一个统一账号上传,这样不便于管理。所以需要使用用户的账号和密码,但 deploy-file 模块并没有设置账号密码功能。仅有 --settings 可用。所以我们需要创建一份包含用户账号的 settings.xml 文件。
首先在模块目录 resources 下创建 template 目录,在目录下放置一个模块 settings.xml
<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<servers>
<server>
<id>release</id>
<username></username>
<password></password>
</server>
<server>
<id>snapshot</id>
<username></username>
<password></password>
</server>
</servers>
</settings>
之后使用时复制一份然后设置指定用户名和密码
/**
* 创建属于该用户的 setting 文件
*
* @param name 用户名
* @param password 密码
* @return
*/
public File createSetting(String name, String password){
File file1 = new File("./almp-file/maven/"+name+"/settings.xml");
FileUtil.copy(settingXml, file1, true);
Document document = XmlUtil.readXML(file1);
Element servers = XmlUtil.getElement(document.getDocumentElement(),"servers");
List<Element> elementList = XmlUtil.getElements(servers, "server");
elementList.parallelStream().forEach(p->{
XmlUtil.getElement(p,"username").setTextContent(name);
XmlUtil.getElement(p,"password").setTextContent(password);
});
FileWriter fileWriter = null;
try {
fileWriter = new FileWriter(file1);
XmlUtil.write(document,fileWriter,"GB2312",1);
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return file1;
}
模块 pom.xml 文件
当上传 pom.xml 的时候,当文件内含有 modules 字段且里面有定义一些子模块 module 时。则使用命令上传时会同时上传该 module 定义的模块。而用户只给了一个父 pom 。我们也只要上传一个父 pom。但命令里也没有这个配置。且若上传时没有这些模块,上传将会失败。(Api 上传没问题┭┮﹏┭┮)
这时我们需要模拟出这些 maven 子模块且不要它们触发上传。所以仅需创建这个 module 设置名字的文件夹,在下面放置一个 pom.xml 文件即可。
首先在模块目录 resources 下创建 template 目录,在目录下放置一个模块 pom.xml
<?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/maven-v4_0_0.xsd">
<properties>
<maven.deploy.skip>true</maven.deploy.skip>
</properties>
</project>
和前面的 settings.xml 文件一同在 Spring 启动时放置到 jar 包外目录
移动模板文件地址
当项目构建为 jar 包后,resources 目录下的文件则不能使用 File 直接定位进而操作。要使用 org.springframework.core.io.ClassPathResource 类读取,并且只能使用 inputStream 方式对文件读取。不能试图获取文件所在路径。否则获取的路径无法定位到文件。
为了后期的操作,我将在 jar 包启动的时候将该文件提取到 jar 包外的目录中。
import cn.hutool.core.io.FileUtil;
private File settingXml = new File(CommonEnum.NEXUS_MAVEN_PATH + "settings.xml");
private File pomXml = new File(CommonEnum.NEXUS_MAVEN_PATH + "pom.xml");
@PostConstruct //spring 启动完成执行
public void initStaticFile(){
ClassPathResource classPathResource = new ClassPathResource("template/settings.xml");
ClassPathResource classPathResource1 = new ClassPathResource("template/pom.xml");
InputStream inputStream = null;
try {
inputStream = classPathResource.getInputStream(); //获取文件流
FileUtil.touch(settingXml); //使用hutool的FileUtil工具创建这个文件(存在不创建)
FileUtil.writeFromStream(inputStream,settingXml); //将流写入这个文件内(覆盖写入)
inputStream.close();//关闭流
inputStream = classPathResource1.getInputStream(); //获取文件流
FileUtil.touch(pomXml); //使用hutool的FileUtil工具创建这个文件(存在不创建)
FileUtil.writeFromStream(inputStream,pomXml);//将流写入这个文件内(覆盖写入)
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
inputStream.close(); //关闭流
} catch (IOException e) {
e.printStackTrace();
}
}
}
执行 mvn 上传命令
/**
* 上传的命令
*
* @param mavenUploadData maven上传数据
* @return
*/
public List<String> uploadCommand(MavenUploadData mavenUploadData){
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("mvn deploy:deploy-file ");
stringBuilder.append(String.format("-DgroupId=%s ",mavenUploadData.getGroupId()));
stringBuilder.append(String.format("-DartifactId=%s ",mavenUploadData.getArtifactId()));
stringBuilder.append(String.format("-Dversion=%s ",mavenUploadData.getVersion()));
stringBuilder.append(String.format("-Dpackaging=%s ",mavenUploadData.getPackaging()));
stringBuilder.append(String.format("-Dfile=%s ",mavenUploadData.getFilePath()));
stringBuilder.append(String.format("-Durl=%s ",mavenUploadData.getUrl()));
stringBuilder.append(String.format("-DrepositoryId=%s ",mavenUploadData.getRepositoryId()));
if(StrUtil.isNotBlank(mavenUploadData.getFilePomPath())){
stringBuilder.append(String.format("-DpomFile=%s ",mavenUploadData.getFilePomPath()));
}
stringBuilder.append(String.format("--settings %s ",mavenUploadData.getSettingPath()));
ArrayList<String> strings = new ArrayList<>();
strings.add(stringBuilder.toString());
// ApolloConfigUtil 是使用 Apollo 作为配置中,因为不同环境的操作系统不一样,目录字符串也不同
ApolloConfigUtil bean = SpringUtils.getBean(ApolloConfigUtil.class);
String path = bean.getStr("appinfo.package", "");
// CommandUtil 是另一篇关于执行操作系统命令的工具类。
List<String> command = CommandUtil.command(path + "almp-file", strings);
// 命令执行会携带一些cmd窗口自带字符去除
List<String> progress = command.stream().skip(4L).filter(p -> !p.contains("Progress") || !p.contains("/")).collect(Collectors.toList());
return progress.stream().limit(progress.size()-2).collect(Collectors.toList());
}
Java 执行操作系统命令文章地址 ...
其他代码及理解
Spring 的配置
// spring 可上传文件大小设置
spring.servlet.multipart.max-file-size = 300MB
spring.servlet.multipart.max-request-size = 300MB
Controller 接口设置
除了 MultipartFile 对象与 password 和 name。还增加了 groupId 和 version 。用于某些应用打包后 pom.xml 文件的 groupId 没有和 version 值为变量。项目本身可以通过 flatten-maven-plugin 包将打包后的变量转换为定义好的值。
@PostMapping("/uploading")
@ApiOperation("jar包上传nexus")
public BaseResult uploadingNexus(MultipartFile multipartFile,String password,String groupId,String version,String name){
File file = null;
try {
file = MultipartFileToFile.multipartFileToFile(multipartFile, "./almp-file/maven/"+name+"/");
} catch (Exception e) {
return new BaseResult(AlmpServiceResultCodeEnum.ERROR);
}
return iAppInfoService.uploadFile(file,password,groupId,version,name);
}
MultipartFile 转 File
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
/**
* @ClassName MultipartFileToFile
* @Description MultipartFile转fie
* @Author TongGuoBo
* @Date 2019/6/19 13:48
**/
public class MultipartFileToFile {
/**
* MultipartFile 转 File
*
* @param file
* @param path 文件所在目录
* @throws Exception
*/
public static File multipartFileToFile(MultipartFile file,String path) throws Exception {
File toFile = null;
if (file.equals("") || file.getSize() <= 0) {
file = null;
} else {
InputStream ins = null;
ins = file.getInputStream();
File file1 = new File(path);
if(!file1.exists()){
file1.mkdirs();
}
toFile = new File(path + file.getOriginalFilename());
inputStreamToFile(ins, toFile);
ins.close();
}
return toFile;
}
//获取流文件
private static void inputStreamToFile(InputStream ins, File file) {
try {
OutputStream os = new FileOutputStream(file);
int bytesRead = 0;
byte[] buffer = new byte[8192];
while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.close();
ins.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
总结
因为感觉自己写的还不够好,所以只能放出关键代码及设计思路。对 InputStream 的操作有点懵懂懵懂的感觉,还需要多加努力。曾因在多个线程同时触发上传时导致 setting 文件报 IOError 错。因为这些文件都临时文件,要删除的,所以出现这种问题。虽然之后尝试了 synchronized 和 AtomicInteger 解决这个问题。但感觉还不能做到游刃有余。希望以后越来越好