Nexus3 的 Maven 仓库合并方案,不止迁移那么简单

原创
07/15 15:33
阅读数 3.7K

1、前言

本文可能是全网唯一一篇具有可操作性的 Nexus3 仓库合并方案。运维过 Nexus3 的同学肯定知道,Nexus3 的迁移非常方便,只需要将 sonatype-work 目录整体打包迁移即可,不止官方有操作教程,网上也有大量的博文案例。可搜遍全网也没找到有合并 Nexus3 的 Maven 仓库的相关案例,所以博主深入 nexus-public 的源码,最后摸索出来了一个可实操的 Maven 仓库合并方案记录在此,供有需要的人参考。

本文软件版本

2、需求场景

假设现有私服A、私服B,目标是将私服A 的所有 maven 仓库合并到私服B 。然后原先使用私服A 的项目只需要切换到私服B 的地址,就可以拉到所有内部依赖,完成项目构建

3、方案概览

  • 1、在私服B 中创建服务A 的所有 maven 仓库的代理
  • 2、在私服B 中创建一个 group 类型的仓库 maven-public,包含自身仓库和私服A 代理仓库
  • 3、收集私服A 的所有 maven 元数据,也就是 maven 坐标,使用私服B 的 maven-public 仓库拉取一次,通过 proxy 代理到私服A,拉取成功后,所以项目依赖就都从私服A 同步到私服B 了

方案比较简单,但是难点在第三步,收集 maven 元数据。如果不考虑历史版本的依赖的话,可以跳过第三步,直接让应用接 maven-public 仓库也是可以的,但是历史版本的依赖同步不过来

3.1、maven 仓库类型

实际操作前先了解下 Nexus 的 maven 仓库的三种类型。Nexus 中的 maven 仓库有三种类型,本文方案之所以能够实施,得益于 proxy 类型的仓库。能够优雅的迁移使用,得益于 group 类型的仓库。下面简单介绍下这三种类型仓库

3.1.1、 hosted 类型仓库:

自有的本地仓库,用来上传私有的 jar 包依赖,Nexus 安装好后,默认会创建两个 hosted 类型的私有仓库,分别如下:

maven-releases 用来管理可以上线使用的 jar 依赖,maven-snapshots 用来管理开发中的 jar 依赖,除了使用上的定位不一样,具体的实际区别在于:

  • maven-releases :每个版本的依赖只能发布一次,想要更新依赖,只能打新的版本
  • maven-snapshots :同一个版本的依赖可以多次发布,拉取依赖时,总能拉到最新发布的

3.1.2、proxy 类型仓库:

proxy 类型仓库用来代理第三方的仓库地址,好处是通过 proxy 仓库拉到的第三方仓库依赖,也会在本地缓存下来,下次拉,直接从本地本地仓库下载了。速度会快很多。例如 aliyun-maven 仓库就是代理的中央仓库,通过 aliyun-maven 仓库拉取依赖,就会快很多,因为这个依赖很可能被别人拉取过,已经在 aliyun-maven 的仓库里缓存了。同理,为了加速依赖的拉取,私服仓库也可以套娃 aliyun-maven 仓库,创建一个 aliyun-maven 仓库的代理,通过下面的 group 类型仓库聚合下,暴露给开发人员使用,项目只要拉过一次依赖,下次使用就会快到爆炸

3.1.3、 group 类型仓库:

group 类型仓库专门用来聚合其他仓库的,本身没有依赖存储管理能力。通过 group 仓库,可以多合一。本来应用需要配置四五个仓库地址才能拉到项目所有的依赖,通过 group 聚合后,只需要配置 group 仓库一个仓库地址就好了。

4、创建私服A 的代理

打开创建仓库表单

选择 maven proxy 仓库

填写 私服A 的 maven 仓库地址

5、创建 group 类型仓库

创建仓库的步骤和上面是类似的,只是在选择仓库类型的时候选择 maven group 类型的仓库,然后将代理私服A 的 nexusA 仓库、自有的 maven-releases 都添加进来

聚合的多个仓库在拉取依赖时,寻找顺序有优先级,需要把最先期望查找的仓库地址,顺序调到最上面

6、同步仓库依赖

合并私服A 和私服B 关键在于将私服A 的所以私有依赖全部迁移到私服B,当前使用的依赖很好处理,通过项目正常使用就会同步过来。难点在于历史版本的私有依赖,要一步到位全部迁移到私服B。同步的原理上面已经说了,通过 proxy 代理,拉取一次就可以了。那同步仓库依赖的目标就变成了获取所有私有依赖的坐标元数据,maven 或 gradle 类型的都可以,只要拿到这个数据就完工了。

6.1 、 获取 gradle 坐标数据

Nexus 项目中使用到了三个数据存储,elasticsearch 、orient、blob 。仓库的元数据信息存放在了 orient 数据库中。关于 orient 信息如下:

OrientDB-Github :https://github.com/orientechnologies/orientdb

OrientDB是一个开源多模型NoSQL DBMS,支持原生图形、文档全文、反应性、地理空间和面向对象的概念。它是用 Java 编写的,速度惊人:它可以在普通硬件上每秒存储 220,000 条记录。没有昂贵的运行时 JOIN,连接作为记录之间的持久指针进行管理。您可以在几毫秒内遍历数千条记录。支持无模式、全模式和模式混合模式。拥有基于用户和角色的强大安全分析系统,并支持查询语言中的SQL。多亏了SQL层,关系领域的技术人员可以直接使用它。

Nexus 中的数据存储软件都是嵌入式的(最新的 Nexus 版本已支持外部的 DBMS 数据库软件),Orient 也是一样,Nexus 启动时会创建 3 个 Orient 实例(component、config、security),用户名和密码默认为 admin。我们需要的 maven 坐标数据都存放在 component db 中,有两种方式可以连接 OrientDB ,获取里面的数据。

方式一:直接连接到实例文件,导出来的数据是个压缩包,里面是 json 类型的数据

/opt/jdk1.8.0_141/bin/java -jar /opt/sonatype/nexus/lib/support/nexus-orient-console.jar

CONNECT plocal:/nexus-data/db/component admin admin

export database component-export

方式二: 开启外部访问的监听器(默认都是关闭的),有两种类型的监听器 http、binary 。在 etc/nexus-default.properties 里配置,然后使用 ./nexus restart 重启下,如

nexus.orient.httpListenerEnabled=true
nexus.orient.binaryListenerEnabled=true

分别会开启 binary 的 2424 端口,http 的 2480 端口。两个端口分别可以使用如下方式链接管理,

最终我们使用 orientdb-studio 的方式查看 component 里的数据结构,使用 orientdb-jdbc 的方式编写查询组装 maven 坐标的脚本应用。component 的数据结构如下:

最终只需要将数据组装成如下的 gradle 添加依赖的格式即可,如:

    compile 'io.jsonwebtoken:jjwt-api:0.11.2'

6.2、 同步数据的脚本

/**
 * @author kl (http://kailing.pub)
 * @since 2021/7/15
 */
public class Main {

    private static final List<String> NEXUS_REPOSITORY = new ArrayList<>();

    static {
        //需要同步的仓库列表
        NEXUS_REPOSITORY.add("test");
        NEXUS_REPOSITORY.add("tap-mvn");
        NEXUS_REPOSITORY.add("maven-releases");
        NEXUS_REPOSITORY.add("maven-snapshots");
        NEXUS_REPOSITORY.add("tds-server");
        NEXUS_REPOSITORY.add("tdsserver-releases");
        NEXUS_REPOSITORY.add("tdsserver-snapshots");
    }

    public static void main(String[] args) throws SQLException, IOException {
        Properties info = new Properties();
        info.put("user", "admin");
        info.put("password", "admin");
        String jdbcUrl = "jdbc:orient:remote:localhost:2424/component";

        Connection conn = DriverManager.getConnection(jdbcUrl, info);
        Statement stmt = conn.createStatement();

        String querySql = "select * from component";
        ResultSet rs = stmt.executeQuery(querySql);
        StringBuilder metadata = new StringBuilder();
        AtomicInteger integer = new AtomicInteger(rs.getFetchSize());
        System.out.println("开始处理,总计【" + integer + "】条记录");
        while (rs.next()) {
            ODocument bucket = (ODocument) rs.getObject("bucket");
            String repository = bucket.field("repository_name").toString();
            OTrackedMap<OTrackedMap<String>> attributes = (OTrackedMap<OTrackedMap<String>>) rs.getObject("attributes");
            OTrackedMap<String> mavenValue = attributes.get("maven2");
            String groupId = mavenValue.get("groupId");
            String artifactId = mavenValue.get("artifactId");
            String version = mavenValue.get("version");
            String packaging = mavenValue.get("packaging");
            System.out.println("当前处理第【" +integer.decrementAndGet()+ "】条记录");
            if (!"pom".equals(packaging) && Objects.nonNull(packaging) && NEXUS_REPOSITORY.contains(repository)) {

                metadata.append(
                        "compile " + "'" + groupId + ":" + artifactId + ":" + version + "@" + packaging + "'")
                        .append("\r");
            }
            System.out.println();
        }
        File file = new File("./nexus.gradle");
        OIOUtils.writeFile(file, metadata.toString());
        rs.close();
        stmt.close();
        System.out.println("处理完成");
    }
}

脚本跑完后,会在当前所在目录下生成一个 nexus.gradle 的文件,文件内容大致如,这个应该不用解释了:

compile 'cn.jiguang.sdk.plugin:xiaomi:3.5.4@aar'
compile 'taptap.plugin:common-res:0.1.1@aar'
compile 'com.taptap.xxljob:xxl-job-core:2.3.2-2-test@jar'
compile 'cn.jiguang.sdk.plugin:vivo:3.5.4@aar'
compile 'taptap.plugin:common-res:0.1.0@aar'
compile 'com.taptap.xxljob:xxl-job-spring-boot-starter:2.3.2-2-test@jar'
compile 'cn.jiguang.sdk.plugin:meizu:3.5.4@aar'
compile 'com.huawei.hms:push:3.0.3.301@aar'

6.3、脚本升级直接拉取

上面的脚本最终会生成所有依赖版本的坐标元数据,适合小规模的私服仓库,如果是大仓库,gradle 没法加载,存在局限性。所以升级了下脚本。在  component db 实例里有还有这样一张 asset 表,数据格式如:

而我们通过 maven 和 gradle 在拉取依赖时,实际访问的 url 格式如下:

https://localhost:8081/repository/maven-public/com/taptap/uploadapk/1.0/uploadapk-1.0.jar

通过仓库地址和 asset 中的 name 字段,很容易拼出一个直接拉取的 url,包括 md5 、sha1、pom 等都有,就有了如下的脚本:

/**
 * @author kl (http://kailing.pub)
 * @since 2021/7/15
 */
public class Main {

    private static final List<String> NEXUS_REPOSITORY = new ArrayList<>();
    private static final RestTemplate restTemplate = new RestTemplate();
    private static final ExecutorService executor = Executors.newFixedThreadPool(50);
    private static final String BASE_URL = "https://localhost:8081/repository/maven-public/";

    static {
        //需要同步的仓库列表
        NEXUS_REPOSITORY.add("test");
        NEXUS_REPOSITORY.add("tap-mvn");
        NEXUS_REPOSITORY.add("maven-releases");
        NEXUS_REPOSITORY.add("maven-snapshots");
        NEXUS_REPOSITORY.add("tds-server");
        NEXUS_REPOSITORY.add("tdsserver-releases");
        NEXUS_REPOSITORY.add("tdsserver-snapshots");
    }

    public static void main(String[] args) throws SQLException{
        Properties info = new Properties();
        info.put("user", "admin");
        info.put("password", "admin");
        String jdbcUrl = "jdbc:orient:remote:localhost:2424/component";

        Connection conn = DriverManager.getConnection(jdbcUrl, info);
        ResultSet rs = conn.createStatement().executeQuery("select * from asset");
        
        AtomicInteger counter = new AtomicInteger(rs.getFetchSize());
        System.out.println("开始处理,总计【" + counter + "】条记录");
        while (rs.next()) {
            ODocument bucket = (ODocument) rs.getObject("bucket");
            String repository = bucket.field("repository_name").toString();
            if(NEXUS_REPOSITORY.contains(repository)){
                String name = rs.getString("name");
                String taskUrl = BASE_URL + name;
                executor.execute(new DownloadMavenTask(taskUrl,counter));
            }else {
                System.out.println("【"+ repository + "】仓库无需同步,已过滤,当处理位置【"+counter.decrementAndGet()+"】");
            }
        }
        rs.close();
        System.out.println("处理完成");
    }

    static class DownloadMavenTask implements Runnable {

        private final String url;
        private final AtomicInteger counter;
        public DownloadMavenTask(String url,AtomicInteger counter) {
            this.url = url;
            this.counter= counter;
        }
        @Override
        public void run() {
            try {
                restTemplate.getForObject(url,String.class);
            }catch (Exception ex){
                System.err.println("下载异常 url:" + url);
            }
            System.out.println("当前处理完第【" + counter.decrementAndGet() + "】条记录");
        }
    }
}

7、结语

一开始的方案是配置好 proxy ,同步到当前使用的依赖就好了,但是历史依赖也很重要啊,那尝试下呗,中间反正几度想放弃迁移历史所有依赖,最终还是坚持搞出来了。从 Nexus 到orient 到 orientdb-studio 到 orientdb-jdbc。 逼一逼自己总会有新思路

 

展开阅读全文
打赏
2
6 收藏
分享
加载中
先顶再看,尤其是外网maven复制到内网的时候这么做很完美
07/16 10:17
回复
举报
KL博主博主
多谢,现在脚本还在升级,估计下午会更新,现在的模式小规模的数据行得通,大规模的 gradle 就直接加载不了了,现在准备直接自己拉了
07/16 10:35
回复
举报
更多评论
打赏
2 评论
6 收藏
2
分享
返回顶部
顶部