# 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 解决这个问题。但感觉还不能做到游刃有余。希望以后越来越好