Java 应用部署包优化经验分享

时间:2024-01-21 16:26:57

背景

最近接手了一个 2018 年的老项目,因为太久远了,功能上的代码不敢乱动,虽然是老项目,但最近一年也在持续加功能,功能不稳定,于是我就进入了救火式改 Bug 的状态。

功能不能妄动,但是这个项目还有一个问题,打包模块打出的全量包部署不起来。拿到这个项目的部署包,400 多兆,网速慢的情况下,下载、上传都得好半天。分析了一下部署包,决定先优化一下,本文记录这个 Java 应用的部署包优化过程。

优化主要是清理 Java 依赖,内容有:

  1. 无用依赖
  2. 测试相关的依赖
  3. 相同 jar 的不同版本
  4. 有冲突的 jar
  5. 容器自带、但是项目无用的包
  6. 第三方组件中的无用文件,如 docs、.cmd 、NOTICES、src 源码等

无用依赖包

项目创建初期的 pom 文件大概是从别的旧项目拷贝过来的,没有做过清理,里面有一些引用包但是工程中没有用到的。比如 ,ftpserver-core、sshd-core,注释掉这些引用后,项目编译能通过,打包后生成的 lib 包中也没有这些模块,说明就是无用的,可以清理掉。

此外,项目初期引 jar 的时候,有必要搞明白引入的包是实现什么功能的,项目是否用得到。如果不确定能否用到,可以只在 maven 父工程的依赖管理中定义,子模块需要的时候再引入。

测试相关的依赖包

maven 项目引入模块时,虽然 scope 设置为 test,但是打包的时候,这些 jar 还是会被加入到第三方依赖 lib 目录下。所以在整理项目部署包的时候,需要手动剔除掉各种测试相关的依赖包。

主要有 junit、自动化测试框架、第三方测试工具类等,搜索出来:
在这里插入图片描述
这些都可以清理掉。

相同 jar 的不同版本

部署包中存在一些名称相同、版本号不一样的 jar ,需要手动清理。

比如 netty 的低版本和 netty-all 高版本,如果引用了 netty-all ,就可以清理掉 netty 低版本了,netty-moduleX 开头的低版本=netty-all 高版本,都引入就存在冗余了:
在这里插入图片描述
还有 JDK 的 tools 包:
在这里插入图片描述
这些都是磁盘蛀虫,项目部署包中没有,而且两个文件都是一样的只是版本不同。 JDK 中已经有了,如果真的要用,用 JDK/jre/ext 下的就可以了。

有冲突的包

Java 框架发展过程中,有一些相互冲突的包,是不应该同时引入的。同时引入,而且能正常运行,只能说是幸运。

比如,servlet-api-2.5.jar 和 javax.servlet-api-3.1.0.jar。servlet-api-2.5.jar 这个版本,可以直接清理掉。

容器引用但是项目完全无用的包

比如项目没有用到 websocket 功能,但是使用的容器自带了这些包:
在这里插入图片描述
清理掉,积少成多,能少则少!

多模块公共 jar 共享

这个项目组件比较少,一个后台、一个前端,但是两个模块有公共的 jar ,梳理出来后,公共包有几十兆。而项目源码包也两个模块共同的包,每次发布补丁的时候都要同时更新两个组件的依赖。

所以,彻底的优化方案是,对项目模块的 jar 进行分类,按当前工程分为四个 jar 包目录:

  1. commonLib:所有模块公共引用的包
  2. moduleALib:模块 A 引用的包
  3. moduleBLib:模块 B 引用的包
  4. dynamicLib:应用中支持动态上传的包

计算模块 A 和模块 B 公共依赖的方法,用 Shell 脚本就可以完成:

进入 moduleA 全量包目录,ll|grep -v 总量|awk '{print $NF}' > /home/alib.log
进入 moduleB 全量包目录,ll|grep -v 总量|awk '{print $NF}' > /home/blib.log
file1="/home/alib.log" #第一个文件名
file2="/home/blib2.log" #第二个文件名
#通过comm命令获取公共行 
common_lines=$(comm -12 <(sort "$file1") <(sort "$file2")) 
echo "$common_lines" > /home/commlib.log

计算出公共包后,就可以将模块 A、B 全量包中的公共文件移除到公共目录了

进入 moduleA 全量包目录,cat /home/commlib.log |xargs -I file mv file /home/commonlib
进入 moduleB 全量包目录,cat /home/commlib.log |xargs -I file mv file /home/commonlib

这样就得到了整个应用的最终依赖包:
在这里插入图片描述
整个应用的依赖包放在一起集中管理,目录清晰,更新方便。目录结构规划好之后了,就需要优化启动脚本了,应用通过 -cp 参数将依赖包目录下所有的 jar 文件拼接起来、然后启动的,很多 Java 工程都是用这个方式启动的,比如 Kafka、IDEA 启动某个主类。
在这里插入图片描述
这种设置 Java 类路径的方法,有一个大问题,就是如果依赖包过多时,进程的启动命令会拼接的很长,比如上面这个,一屏都看不到这个进程的全貌。

有三种方法可以改善这个问题:

  1. -cp 拼接路径可以用通配符-cp /xx/lib/*:/lib/*
  2. -Djava.ext.dirs:这是普通 Java 应用的参数。
  3. -Dloader.path:SpringBoot 引用的启动参数。

这个工程是原生的 SpringMVC 项目,尝试了第二种方法,但是找不到主类,最终选择了第一种方法。

修改应用中组件 A、B 的启动脚本,将拼接 -cp 参数的部分直接改为当前应用部署包中 lib 目录:

模块 A 的启动脚本中拼接依赖的地方 moduleALib = moduleALib+commonLib
CLASSPATH=${APP_HOME}/lib/commonLib/*:${APP_HOME}/lib/moduleALib/*
同理修改模块 B 的启动脚本。

第三方组件的无关文件

最后一点可以优化的是第三方组件中的无关文件了,部署包中显然用不上。
主要有:

  1. docs :组件说明文档。
  2. src :源码。
  3. LICENCES 文件。
  4. NOTICES 文件。
  5. cmd 启动脚本,目标是 Linux ,显然用不到 cmd 脚本。
  6. tools ,一些用来调试的工具。

启示录

经过这一些列的操作后,部署包从 400 多兆减少到了 178M,使用精简之后的部署包运行时,如果启动失败,再排查缺什么 jar ,就加上。还是比较顺利的,5轮报错后,程序就正常启动了。没有表面的错误,其他功能有没有影响,还需要继续观察。

最后一步,以精简之后的目录结构调整打包脚本,保证项目源码打出的全量包是可用的,顺手写一个补丁包打包模块。这极大方便了部署包的准备工作,按之前的流程,要拿到第一版的部署包,将项目打包出来的 6个 jar ,逐个替换部署包对应目录的文件。让工程的打包模块真正能打包,能极大减少人工操作。

部署包优化其实是个费力不讨好的事情,中途搞一半有点弄不下去了,担心优化过度后项目跑不起来了怎么办!况且,项目源码存在这么多年、经手人都多少波了,也没有人考虑过这种问题,而且豪横的项目组磁盘资源根本不是事儿,这点优化是否有必要呢?

谁让我碰到了呢!作为一个还算有点工匠精神的超级熟练程序员,真的忍不了这些问题。优化还是有成效的,至少方便自己了,经过一轮改造后,部署、发包就方便多了。

其实本文记录的工作应该是项目开发完成后,发布部署包时就应该做的工作,虽然部署包越来越大是趋势,例如:Kafka 从第一个版本到最新版本,大小几乎翻了一倍;随便下一个应用几百兆。但也值得思考,我们发布的应用是不是可以更紧凑呢,里面真的这个应用需要的文件吗?