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>

用rust 写一个jar包 class冲突检测工具的更多相关文章

  1. Maven将代码及依赖打成一个Jar包的方式

    Maven可以使用mvn package指令对项目进行打包,如果使用java -jar xxx.jar执行运行jar文件,会出现"no main manifest attribute, in ...

  2. Jmeter用BeanShell Sampler调用java写的jar包进行MD5加密

    [前言] 在工作中,有时候我们请求的参数可能需要加密,比如登录接口中的密码做了加密操作,今天我就给大家介绍一种方法:Jmeter用BeanShell Sampler调用java写的jar包进行MD5加 ...

  3. 将多个jar包重新打包成一个jar包

    我介绍的方法是使用java命令来操作的,所以首先的安装jdk,这个就自己搞定吧. 提取jar包 为了将多个jar包打包成一个jar包,首先要将每个jar包的内容提取出来放到一个文件夹下,具体的操作命令 ...

  4. 一个jar包冲突引起的StackOverflowError

    项目运行中错误信息:java.lang.IllegalStateException: Unable to complete the scan for annotations for web appli ...

  5. maven将项目及第三方jar打成一个jar包

    pom.xml中添加如下配置 把依赖包和自己项目的文件打包如同一个jar包(这种方式对spring的项目不支持) <build> <plugins> <plugin> ...

  6. maven 引用另一个jar包 需要先打包在仓库里面 并在pom里面配置 才可以引用

    maven 引用另一个jar包 需要先打包在仓库里面 并在pom里面配置 才可以引用

  7. 从零开始写一个npm包及上传

    最近刚好自己需要写公有npm包及上传,虽然百度上资料都能找到,但是都是比较零零碎碎的,个人就来整理下,如何从零开始写一个npm包及上传. 该篇文件只记录一个大概的流程,一些细节没有记录. tips:  ...

  8. 多个module实体类集合打一个jar包并上传至远程库

    本章内容主要分享多个module中的实体类集合生成到一个jar包中,并且发布到远程库:这里采用maven-assembly-plugin插件的功能来操作打包,内容不长却贴近实战切值得拥有,主要节点内容 ...

  9. 从零开始写一个npm包,一键生成react组件(偷懒==提高效率)

    前言 最近写项目开发新模块的时候,每次写新模块的时候需要创建一个组件的时候(包含组件css,index.js,组件js),就只能会拷贝其他组件修改名称 ,但是写了1-2个后发现效率太低了,而且极容易出 ...

  10. 创建Jmeter中使用的jar包中的工具类方法

    1. 在IDEA中新建一个maven工程. 2. 编写工具类方法,如加密算法.此处以加法为例. package DemoTest; public class DemoClass{ public int ...

随机推荐

  1. odoo 开发入门教程系列-安全-简介

    安全-简介 前一章中我们已经创建了第一个打算用于存储业务数据的表.在odoo这样的一个商业应用中,第一个考虑的问题就是谁(Odoo 用户(或者组用户))可以访问数据.odoo为指定用户组用户提供了一个 ...

  2. Rancher 系列文章-K3s Traefik MiddleWare 报错-Failed to create middleware keys

    概述 书接上回:<Rancher 系列文章-K3S 集群升级>, 我们提到:通过一键脚本升级 K3S 集群有报错. 接下来开始进行 Traefik 报错的分析和修复, 问题是: 所有 Tr ...

  3. 剑指 offer 第 24 天

    第 24 天 数学(中等) 剑指 Offer 14- I. 剪绳子 给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m.n都是整数,n>1并且m>1),每段绳子的长度记为 k[ ...

  4. 第三部分:Spdlog 日志库的实现原理

    ! https://zhuanlan.zhihu.com/p/617432495 Spdlog 是一个快速.异步的 C++ 日志库,被广泛应用于 C++ 项目中.在这篇文章中,我们将探讨 Spdlog ...

  5. 如何单机部署多个 MySQL 8.0 实例 ?

    在服务器资源有限的情况下,可利用该方案快速搭建各类 mysql 架构方案.各 MySQL 实例共享一个 mysqld 主程序,但各实例数据目录是独立的,存放在不同的文件夹中:好了.废话不多说,直接上干 ...

  6. kafka rebalance你真的了解吗

    介绍 今天主要分享一下 kafka 的 rebalance,在 kafka 中,rebalance 是一个十分重要的概念,很多时候引发的一些问题可能都是由于 rebalance 引起的,rebalan ...

  7. WPF 界面布局、常用控件入门教程实例 WPF入门学习控件快速教程例子 WPF上位机、工控串口通信经典入门

    WPF(Windows Presentation Foundation)是一种用于创建 Windows 桌面应用程序的框架,它提供了丰富的控件库和灵活的界面布局,可以创建现代化的用户界面.下面是 WP ...

  8. github打不开或者打开慢方法

    github最近打不开,很久之前遇到过,但是忘记怎么解决了,查找相关资料后,今天记录在此,以备不时之需. 记住3个关键网址 github网址查询:The world's leading softwar ...

  9. Java设计模式 —— 面向对象设计原则

    1 设计模式概述 1.1 设计模式的定义与分类 设计模式的定义 Design patterns are descriptions of communicating objects and classe ...

  10. Gradio入门到进阶全网最详细教程[一]:快速搭建AI算法可视化部署演示(侧重项目搭建和案例分享)

    Gradio入门到进阶全网最详细教程[一]:快速搭建AI算法可视化部署演示(侧重项目搭建和案例分享) 常用的两款AI可视化交互应用比较: Gradio Gradio的优势在于易用性,代码结构相比Str ...