如何将 jar 转换为 rsyncable jar?

How to convert jar to rsyncable jar?

我有一个由 Gradle Shadow 插件生成的 fat/uber JAR。我经常需要通过网络发送胖 JAR,因此,我只发送文件的增量而不是大约 40 MB 的数据对我来说很方便。 rsync 是一个很好的工具。但是,我的源代码中的一个小改动会导致最终的 fat JAR 发生很大变化,因此 rsync 并没有发挥它应有的作用。

我可以将 fat JAR 转换为 rsync 友好的 JAR 吗?

我的想法solution/workarounds:

可能相关的问题:

据我所知,rsyncable gzip 的工作原理是每 8192 字节的压缩数据重置霍夫曼树并填充到字节边界。这避免了对压缩的远程副作用(rsync 处理移位的数据块,如果它们至少是字节对齐的话)

从这个意义上说,包含小文件(小于 8192 字节)的 jar 已经可以 rsyncable,因为每个文件都是单独压缩的。作为测试,您可以使用 jar 的 -0 选项(无压缩)来检查它是否有助于 rsync,但我认为它不会。

要提高 rsyncability,您需要(至少):

  • 确保文件以相同的顺序存储。
  • 确保与未更改文件关联的元数据也未更改,因为每个文件都有一个本地文件头。例如,.class 个文件的最后修改时间有问题。
    我不确定 jar,但 zip 允许额外的字段,其中一些可能会阻止 rsync 匹配,例如unix 扩展的最后访问时间。

编辑:我用以下命令做了一些测试:

FILENAME=SomeJar.jar

rm -rf tempdir
mkdir tempdir

unzip ${FILENAME} -d tempdir/

cd tempdir

# set the timestamp to 2000-01-01 00:00
find . -print0 | xargs --null touch -t 200001010000

# normalize file mode bits, maybe not necessary
chmod -R u=rwX,go=rX .

# sort and zip files, without extra
find . -type f -print | sort | zip ../${FILENAME}_normalized  -X -@

cd ..
rm -rf tempdir

删除 jar/zip 中包含的第一个文件时的 rsync 统计信息:

total: matches=1973  hash_hits=13362  false_alarms=0 data=357859
sent 365,918 bytes  received 12,919 bytes  252,558.00 bytes/sec
total size is 4,572,187  speedup is 12.07

当删除第一个文件并修改每个时间戳时:

total: matches=334  hash_hits=124326  false_alarms=4 data=3858763
sent 3,861,473 bytes  received 12,919 bytes  7,748,784.00 bytes/sec
total size is 4,572,187  speedup is 1.18

所以有显着差异,但没有我预期的那么大。

似乎更改文件模式也不会影响传输(可能是因为它存储在中央目录中?)

让我们退一步;如果您不创建大罐子,这将不再是问题。

因此,如果您单独部署依赖项 jar,并且不将它们打包到单个 fat jar 中,那么您也解决了这里的问题。

为此,假设您有:

  • /foo/yourapp.jar
  • /foo/lib/guava.jar
  • /foo/lib/h2.jar

然后,将以下条目放入 yourapp.jarMETA-INF/MANIFEST.MF 文件中:

Class-Path: lib/guava.jar lib/h2.jar

现在您只需 运行 java -jar yourapp.jar 它就会起作用,获取依赖项。您现在可以使用 rsync 单独传输这些文件; yourapp.jar 会小很​​多,而且你的依赖 jar 通常不会改变,所以在 rsyncing 时也不会花费太多时间。

我是ware 这并没有直接回答实际提出的问题,但我敢打赌,这个问题出现的次数超过 90%,而不是 fatjarring 是合适的答案。

注意:Ant、Maven、Guava 等可以负责放入正确的清单条目。如果您的 jar 的意图不是 运行 它,而是,例如,它是一个 war 对于 web servlet 容器,它们有自己的规则来指定依赖 jar 所在的位置。

有两种方法可以做到这一点,这两种方法都涉及关闭压缩。 Gradle 先用 jar 方法关闭它...

你可以使用 gradle(这个答案实际上来自 OP)

shadowJar {
    zip64 true
    entryCompression = org.gradle.api.tasks.bundling.ZipEntryCompression.STORED
    exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA'
    manifest {
        attributes 'Main-Class': 'com.my.project.Main'
    }
}

jar {
    manifest {
        attributes(
                'Main-Class': 'com.my.project.Main',
        )
    }
}

task fatJar(type: Jar) {
    manifest.from jar.manifest
    classifier = 'all'
    from {
        configurations.runtime.collect { it.isDirectory() ? it : zipTree(it) }
    } {
        exclude "META-INF/*.SF"
        exclude "META-INF/*.DSA"
        exclude "META-INF/*.RSA"
    }
    with jar
}

这里的关键是压缩已经关闭,即

org.gradle.api.tasks.bundling.ZipEntryCompression.STORED

您可以在此处找到文档

https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/bundling/ZipEntryCompression.html#STORED

是的,您可以在新存档上将其速度提高约 40%,在您已经 rsync 的 jar 存档上将速度提高 200% 以上。诀窍是不要压缩罐子 您可以利用 rsyncs 分块算法。

我使用以下命令压缩了一个包含很多 class 个文件的目录...

jar cf0 uncompressed.jar .
jar cf  compressed.jar   .

这创建了以下两个 jar...

-rw-r--r--  1 rsync jar    28331212 Apr 13 14:11 ./compressed.jar
-rw-r--r--  1 rsync jar    38746054 Apr 13 14:10 ./uncompressed.jar

请注意,未压缩的 Jar 的大小大约大 10MB。

然后我使用以下命令对这些文件进行 rsync 并为它们计时。 (注意,即使对压缩文件打开压缩也没有什么效果,我稍后会解释)。

压缩罐

time rsync -av -e ssh compressed.jar jar@rsync-server.org:/tmp/

building file list ... done
compressed.jar

sent 28334806 bytes  received 42 bytes  2982615.58 bytes/sec
total size is 28331212  speedup is 1.00

real  0m9.208s
user  0m0.248s
sys 0m0.483s

未压缩的 Jar

time rsync -avz -e ssh uncompressed.jar jar@rsync-server.org:/tmp/

building file list ... done
uncompressed.jar

sent 11751973 bytes  received 42 bytes  2136730.00 bytes/sec
total size is 38746054  speedup is 3.30

real  0m5.145s
user  0m1.444s
sys 0m0.219s

我们的速度提高了近 50%。这至少加快了 rsync 和 我们得到了很好的提升,但是后续的 rsync 有一个小的变化呢? 已制作。

我从重新创建的大小为 170 字节的目录中删除了一个 class 文件 罐子修剪它们是这个大小..

-rw-r--r--  1 rsycn jar  28330943 Apr 13 14:30 compressed.jar
-rw-r--r--  1 rsync jar  38745784 Apr 13 14:30 uncompressed.jar

现在的时间非常不同。

压缩罐

building file list ... done
compressed.jar

sent 12166657 bytes  received 31998 bytes  2217937.27 bytes/sec
total size is 28330943  speedup is 2.32

real  0m5.435s
user  0m0.378s
sys 0m0.335s

未压缩的 Jar

building file list ... done
uncompressed.jar

sent 220163 bytes  received 43624 bytes  175858.00 bytes/sec
total size is 38745784  speedup is 146.88

real  0m1.533s
user  0m0.363s
sys 0m0.047s

所以我们可以使用这种方法大大加快 rsync 大型 jar 文件的速度。其原因与信息论有关。当您压缩数据时,它实际上会从数据中删除所有常见的信息,即您留下的内容看起来非常像随机数据,最好的压缩器会删除更多此类信息。对任何数据和大多数压缩算法的微小更改都会对数据输出产生巨大影响。

Zip 算法有效地使 rsync 更难找到服务器和客户端之间相同的校验和,这意味着它需要传输更多数据。当你解压缩它时,你让 rsync 做它擅长的事情,发送更少的数据来同步这两个文件。

我在build.gradle中替换了我原来的配置代码:

shadowJar {
    zip64 true
    entryCompression = org.gradle.api.tasks.bundling.ZipEntryCompression.STORED
    exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA'
    manifest {
        attributes 'Main-Class': 'com.my.project.Main'
    }
}

jar {
    manifest {
        attributes(
                'Main-Class': 'com.my.project.Main',
        )
    }
}

task fatJar(type: Jar) {
    manifest.from jar.manifest
    classifier = 'all'
    from {
        configurations.runtime.collect { it.isDirectory() ? it : zipTree(it) }
    } {
        exclude "META-INF/*.SF"
        exclude "META-INF/*.DSA"
        exclude "META-INF/*.RSA"
    }
    with jar
}

(使用此处发布的解决方案 )

最终的 fatJar(即 56 MB)比 Shadow 插件为我生成的(即 35 MB)大得多。但是,最终的 jar 似乎是 rsyncable(当我对源代码进行微小更改时,rsync 仅传输非常少量的数据)。

请注意,我对 Gradle 的了解非常有限,所以这只是我的观察,可能会进一步改进。