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