Rust很适合写命令行工具,特别是使用clap
crate 更加方便,这篇文章介绍使用rust写一个jar包class冲突检测的工具。
首先jar包class冲突的现象是多个jar包中有同名的class,并且class的md5还不一样,那么就意味着该class存在多个版本,那么就存在冲突的可能。
思路比较简单,就是遍历每个jar包,记录ClassName 和 对应 CRC 校验码 及 jar 包的对应关系。
通过clap
的derive api就可以快速定义个命令行的参数解析器。
struct Args { | |
jar_list: String, | |
disable_crc: bool, | |
exclude: Vec<String>, | |
} | |
通过zip
读取jar包中的entry, 过滤只处理.class
文件,并从zip_file中读取crc32
的元数据,这样可以避免读取原始数据生成md5,可以大大加快处理速度。
Borrow checker的问题
中间编写的时候遇到了一个常见的rust borrow checker的问题。
以下代码为例
fn main() { | |
let path = “/tmp/a.jar”; | |
let jar = File::open(path).unwrap(); | |
let mut zip = ZipArchive::new(jar).unwrap(); | |
for name in zip.file_names() { | |
let entry = zip.by_name(name); | |
println!(“name: {}, size: {}”, name, entry.unwrap().size()); | |
} | |
} |
我是想通过遍历ZipArchive#file_names
然后根据文件名获取ZipFile
但是会有如下编译错误
pub fn file_names(&self) -> impl Iterator<Item = &str> { | |
self.shared.names_map.keys().map(|s| s.as_str()) | |
} |
/// Search for a file entry by name | |
pub fn by_name<‘a>(&‘a mut self, name: &str) -> ZipResult<ZipFile<‘a>> { | |
Ok(self.by_name_with_optional_password(name, None)?.unwrap()) | |
} |
但是用以下的方式就没有问题
let path = “/tmp/a.jar”; | |
let jar = File::open(path).unwrap(); | |
let mut zip = ZipArchive::new(jar).unwrap(); | |
for i in 0..zip.len() { | |
let entry = zip.by_index(i).unwrap(); | |
println!(“name: {}, size: {}”, entry.name(), entry.size()); | |
} |
这里我比较奇怪的是从方法签名上看 len()
和 file_names()
都会发生immutable borrow,而后面by_index
和 by_name
都会发生mutable borrow。为什么会一个可以通过检查,一个不行。
pub fn len(&self) -> usize { | |
self.shared.files.len() | |
} |
len
函数实际的签名应该是fn len<'a>(&'a self) -> usize
返回值是usize,所以函数调用完成后就不再和借用有关了。所以 immutable borrow 就结束了。
而file_names
实际签名是fn file_names<'a>(&'a self) -> impl Iterator<Item = &'a str> {…}
返回值的生命周期和 入参的 immutable ref周期相同,所以就要求 zip immutable borrow ref 至少要保持到iterator结束,所以后续就检测出循环中同时存在可变和不可变引用了。
命令行频繁被Killed问题
问题现象是当使用cargo build
打包出binary后,通过cp 到 /tmp/jcd
执行 会出现 Killed的情况,不是必现,但是当出现之后后续就一直会这样,百思不得其解。
$ /tmp/jcd | |
[1] 16957 killed /tmp/jcd |
后通过在rust user 论坛提问找到答案,不得不说回复效率很高。
原因应该是和苹果电脑上的 Code sign机制有关,在苹果没有解决这个问题之前,建议通过ditto
替代cp
命令来copy程序。
经过检查系统日志确实有出现 Code Signature Invalid
的报错
相同的Class CRC和MD5却不一样
问题是发现在集成这个工具到内部的插件框架中,集成过程中发现一个Jar包被另一个module依赖,经过shade插件打包(没有对相关class进行relocate) 后,生成的class crc32不同,被识别为会冲突的类。通过javap -v
查看两个class对比发现里面的仅仅是一些constant pool 不同。
那么怀疑就是maven-shade-plugin 做了什么操作,翻阅了下代码,查看了shade的处理流程.
看到以下这段,发现这不就是我遇到的问题么。
解决冲突的Class
最后再回到最初的目的,当我们通过工具检测出冲突的class应该怎么解决呢。
首先我们需要判断这个class是否是运行时所需要的。
如果不是所需要的那么我们就应该直接排掉他,排除有两种手段(这里针对的是maven shade的打包方式),如果在dependency tree中可以看到相应package的依赖,那么可以直接通过如下的白名单 include 或者 exclude 掉某个 artifact。
<artifactSet combine.self=“override”> | |
<includes> | |
<include>commons-dbcp:commons-dbcp</include> | |
<include>commons-pool:commons-pool</include> | |
<include>mysql:mysql-connector-java</include> | |
</includes> | |
</artifactSet> |
但是不排除这个依赖包本身就是fatjar,那么直接通过这种方式就排不掉这个依赖,可以通过filters 配置文件 粒度的匹配过滤
<filters> | |
<filter> | |
<artifact>*:*</artifact> | |
<excludes> | |
<exclude>META-INF/*.SF</exclude> | |
<exclude>META-INF/*.DSA</exclude> | |
<exclude>META-INF/*.RSA</exclude> | |
<exclude>javax/**</exclude> | |
<exclde>org/slf4j/**</exclde> | |
</excludes> | |
</filter> | |
</filters> | |
如果这个冲突的class是运行时需要的,那么可以通过relocation的方式给各自的插件包中shade成带特殊前缀的class名,解决同名冲突。