用rust 寫一個jar包 class衝突檢測工具

2023-05-24 06:01:00

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_indexby_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

命令列頻繁被Killed問題

問題現象是當使用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的報錯

相同的Class CRC和MD5卻不一樣

問題是發現在整合這個工具到內部的外掛框架中,整合過程中發現一個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應該怎麼解決呢。
首先我們需要判斷這個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>