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. 对Javaweb的相关练习之利用.jsp文件和.java文件将输入的数据存储到指定的数据库中

    练习分析 import javax.servlet.*; import javax.servlet.annotation.WebServlet; import javax.servlet.http.* ...

  2. ShardingSphere 数据分片之 Sharding-JDBC 深入理解

    更多内容,前往 IT-BLOG MySQL 的存储单位是 page[16kb],索引使用 B+Tree,深度为3(3次 IO便能查出数据).为了提高查询速度,存储单元中都存储的是索引的指针.MySQL ...

  3. SpringBoot——Swagger2 接口规范

    更多内容,前往 IT-BLOG 如今,REST和微服务已经有了很大的发展势头.但是,REST规范中并没有提供一种规范来编写我们的对外 REST接口 API文档.每个人都在用自己的方式记录 api文档, ...

  4. 【事故】记一次意外把企业项目放到GitHub并被fork,如何使用DMCA下架政策保障隐私

    前言 缘由 在一个月黑风高的夜晚,正准备休息的我突然接到之前外包老总的亲切问候.一顿输出才知道三年前为了搭建流程化部署,将甲方的测试代码放到github上后忘记删除.现在被甲方的代码扫描机制扫到,并且 ...

  5. OPP前三次作业总结

    OPP前三次作业总结 目录 前言: 第一次OOP训练: 7-7 有重复的数据 设计与分析: 具体代码 踩坑心得 改进建议 7-8 从一个字符串中移除包含在另一个字符串中的字符 设计与分析: 具体代码 ...

  6. 推荐三款 Mac 上的理财神器 iCompta、Rublik、UctoX

    今天推荐三款理财神器,像个人的话可以使用 iCompta(个人财务管理)一款软件就好了,但有些朋友可能有关注汇率的需求,可以使用 Rublik(汇率动态),还有一些小伙伴可能有自己的公司等原因财务量较 ...

  7. vue 之 computed方法自带缓存踩坑1

    使用场景:ant-vue 穿梭框使用 页面使用computed方法处理组织结构数据,退出页面时,对加载数据做了set null 操作,再次进入页面时,穿梭框只显示数据,无法做左右穿梭功能. 原因:co ...

  8. 提交docker镜像

    docker commit -m="提交的描述信息" -a="作者" 容器id 目标镜像名:[TAG]

  9. 解Bug之路-应用999线升高

    前言 监控指标诚然是发现问题于微末之时的极佳手段,但指标往往有其表达的极限.在很多情况下,单独看一个黄金指标并不能表征系统的健康程度,反而有可能被其迷惑,进而忽略相关问题.(本文所提及的Linux K ...

  10. 为什么C++语言性能优越?

    面试时被问到这个问题,发现自己一直以来理所当然的认为C++快,却没有具体分析原因.下面简单总结一下为什么快. 当我们编写程序时,我们需要将程序转换为计算机可以理解的机器语言.不同的语言有不同的执行机制 ...