惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tailwind CSS Blog
大猫的无限游戏
大猫的无限游戏
L
LINUX DO - 热门话题
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
雷峰网
雷峰网
aimingoo的专栏
aimingoo的专栏
博客园_首页
MongoDB | Blog
MongoDB | Blog
V
V2EX
GbyAI
GbyAI
量子位
Microsoft Azure Blog
Microsoft Azure Blog
有赞技术团队
有赞技术团队
G
Google Developers Blog
云风的 BLOG
云风的 BLOG
B
Blog
Microsoft Security Blog
Microsoft Security Blog
S
SegmentFault 最新的问题
O
OpenAI News
N
News and Events Feed by Topic
博客园 - Franky
爱范儿
爱范儿
Forbes - Security
Forbes - Security
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V2EX - 技术
V2EX - 技术
Application and Cybersecurity Blog
Application and Cybersecurity Blog
N
News and Events Feed by Topic
N
News | PayPal Newsroom
Schneier on Security
Schneier on Security
Cloudbric
Cloudbric
Security Archives - TechRepublic
Security Archives - TechRepublic
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Recent Commits to openclaw:main
Recent Commits to openclaw:main
人人都是产品经理
人人都是产品经理
P
Privacy International News Feed
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
B
Blog RSS Feed
阮一峰的网络日志
阮一峰的网络日志
D
DataBreaches.Net
Last Week in AI
Last Week in AI
罗磊的独立博客
Spread Privacy
Spread Privacy
Recent Announcements
Recent Announcements
The Cloudflare Blog
Google DeepMind News
Google DeepMind News
AWS News Blog
AWS News Blog
The Register - Security
The Register - Security
Y
Y Combinator Blog
J
Java Code Geeks
I
Intezer

博客园 - Aitozi

An Empirical Evaluation of Columnar Storage Formats Lance 写入链路:Merge Into、Compaction 与 Stable Row ID 从本地目录理解 Lance Dataset:Manifest、Fragment 与 Blob 论文解读:Lance 如何通过自适应结构编码提升列式存储随机访问 中国最大广告机器简史 学习Facebook,超越Meta|字节跳动 第3集 Paimon merge into 实现原理 Paimon Deletion Vector Paimon lookup store 实现 Flink Batch Hash Aggregate 理解 Paimon changelog producer 笔记工具 FlinkSQL类型系统 二叉堆原理与实现 SkipList原理与实现 Lakehouse: A New Generation of Open Platforms that Unify Data Warehousing and Advanced Analytics Delta Lake: High-Performance ACID Table Storage over Cloud Object Stores Paimon Compaction实现 Paimon读取流程 Paimon的写入流程 Calcite sql2rel 过程 rust 中 str 与 String; &str &String 好奇心: 保持对未知世界用不停息的热情 Apache hudi 核心功能点分析
用rust 写一个jar包 class冲突检测工具
Aitozi · 2023-05-23 · via 博客园 - Aitozi

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,可以大大加快处理速度。

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但是会有如下编译错误
image.png

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周期相同,所以就要求 zip immutable borrow ref 至少要保持到iterator结束,所以后续就检测出循环中同时存在可变和不可变引用了。
详细解释: https://users.rust-lang.org/t/borrow-check-understanding/94260/2

这个问题实际上和生命周期有关,阅读这篇基本就能想通这个问题 https://course.rs/basic/lifetime.html

命令行频繁被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的报错
image.png

相同的Class CRC和MD5却不一样

问题是发现在集成这个工具到内部的插件框架中,集成过程中发现一个Jar包被另一个module依赖,经过shade插件打包(没有对相关class进行relocate) 后,生成的class crc32不同,被识别为会冲突的类。通过javap -v 查看两个class对比发现里面的仅仅是一些constant pool 不同。
那么怀疑就是maven-shade-plugin 做了什么操作,翻阅了下代码,查看了shade的处理流程.
看到以下这段,发现这不就是我遇到的问题么。
image.png
查阅了相应的issue: https://issues.apache.org/jira/browse/MSHADE-391
在3.3.0 才解决,而我使用的版本正好是3.2.4。升级插件重新生成校验码一致了。
因此如果多个包中存在有maven shade 打包的或者不同jvm打包的class可能会存在一些常量池不对之类的情况,所以这个冲突检测可能检测出并不冲突的class,因为没有真实的去比较class的signature.

解决冲突的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名,解决同名冲突。

<relocation>
    <pattern>org.apache.http</pattern>
    <shadedPattern>com.aitozi.shaded.org.apache.http</shadedPattern>
</relocation>

发布命令行工具到crates.io

cargo login ${token}
cargo publish 

这样发布之后其他用户就可以通过cargo install来下载并编译, 安装你的程序。不过这样需要安装的用户上进行编译和install的过程。

注意这里我第一次上传的时候有一个typo,而crates.io 上包一经发布就不可以删除了,因此发布包的时候一定要check下包名信息。

发布二进制文件

更好的是直接发布二进制文件,rust-cli book的教程上提供的一个travis-ci的 trust模板已经过时了,很久没在维护了,所以不建议参考参考 trust模板: https://github.com/japaric/trust
我这里参考的是 https://github.com/japaric/trust/issues/136 中提到的一个样例,直接使用github ci来进行发布,这个模板实际上是通过cross 工具来实现编译多个平台的包。

权限问题

image.png
模板中默认使用了github.token来鉴权采 参考,这个不需要自己去创建,每一个workflow都会注入,但是默认权限好像不够,不知道原作者为啥可以跑, 可能修改了默认的GITHUB_TOKEN的权限。
也有其他人report了这个问题:https://github.com/taiki-e/create-gh-release-action/issues/13
通过添加以下权限声明解决

permissions:
	contents: write

通过Github Actions,以后需要发布一个多平台的binary 就非常简单只需要push 一个tag即可

git tag v0.0.1  
git push origin v0.0.1

自动化流程就会创建出一个release以及多平台的binary
image.png