Rust很適合寫命令列工具,特別是使用clap
crate 更加方便,這篇文章介紹使用rust寫一個jar包class衝突檢測的工具。專案地址: https://github.com/Aitozi/jar_conflict_detector
首先jar包class衝突的現象是多個jar包中有同名的class,並且class的md5還不一樣,那麼就意味著該class存在多個版本,那麼就存在衝突的可能。
思路比較簡單,就是遍歷每個jar包,記錄ClassName 和 對應 CRC 校驗碼 及 jar 包的對應關係。
通過clap
的derive api就可以快速定義個命令列的引數解析器。
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
#[arg(
short,
long = "jars",
required = true,
help = "The jar list joined by semicolon"
)]
jar_list: String,
#[arg(long, help = "Disable the crc check", action = clap::ArgAction::SetTrue)]
#[arg(default_value_t = false)]
disable_crc: bool,
#[arg(short, long, action = clap::ArgAction::Append, help = "The exclude package prefix")]
exclude: Vec<String>,
}
通過zip
讀取jar包中的entry, 過濾只處理.class
檔案,並從zip_file中讀取crc32
的後設資料,這樣可以避免讀取原始資料生成md5,可以大大加快處理速度。
中間編寫的時候遇到了一個常見的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週期相同,所以後續就檢測出同時存在可變和不可變參照了。
詳細解釋: https://users.rust-lang.org/t/borrow-check-understanding/94260/2
問題現象是當使用cargo build
打包出binary後,通過cp 到 /tmp/jcd
執行 會出現 Killed的情況,不是必現,但是當出現之後後續就一直會這樣,百思不得其解。
$ /tmp/jcd
[1] 16957 killed /tmp/jcd
後通過在rust user 論壇提問找到答案,不得不說回覆效率很高。
https://users.rust-lang.org/t/rust-command-line-tools-keeps-beeing-killed/94179
原因應該是和蘋果電腦上的 Code sign機制有關,在蘋果沒有解決這個問題之前,建議通過ditto
替代cp
命令來copy程式。
經過檢查系統紀錄檔確實有出現 Code Signature Invalid
的報錯
問題是發現在整合這個工具到內部的外掛框架中,整合過程中發現一個Jar包被另一個module依賴,經過shade外掛打包(沒有對相關class進行relocate) 後,生成的class crc32不同,被識別為會衝突的類。通過javap -v
檢視兩個class對比發現裡面的僅僅是一些constant pool 不同。
那麼懷疑就是maven-shade-plugin 做了什麼操作,翻閱了下程式碼,檢視了shade的處理流程.
看到以下這段,發現這不就是我遇到的問題麼。
查閱了相應的issue: https://issues.apache.org/jira/browse/MSHADE-391
在3.3.0 才解決,而我使用的版本正好是3.2.4。升級外掛重新生成校驗碼一致了。
最後再回到最初的目的,當我們通過工具檢測出衝突的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>
<exclude>org/apache/flink/fnexecution/**</exclude>
<exclde>org/slf4j/**</exclde>
</excludes>
</filter>
</filters>
如果這個衝突的class是執行時需要的,那麼可以通過relocation的方式給各自的外掛包中shade成帶特殊字首的class名,解決同名衝突。
<relocation>
<pattern>org.apache.http</pattern>
<shadedPattern>com.alipay.flink.sls.shaded.org.apache.http</shadedPattern>
</relocation>
本文來自部落格園,作者:Aitozi,轉載請註明原文連結:https://www.cnblogs.com/Aitozi/p/17426768.html