包管理原则
摘要
坊间传闻java web开发人员写了那么多代码,但是其实一半代码都在处理NPE。总是在加班,却大部分时间都在处理包冲突,类加载不了的bug。这些问题总是让新老程序员都很抓狂,有很多的工具可以辅助我们解决这些问题(maven helper插件,arthas等)但是有没有一些原则可以遵循,在源头上避免这些问题的发生呢。
问题
经常遇到的问题有ClassNotFoundException
通过Class.forName()
或者loadClass()
方法加载类时,当classpath中又找不到这个类,就会抛这个错误。
这个错误一般比较好排查,编译程序时就抛出来了。然后引入对应的jar包,或者刷新classpath就可以解决
NoClassDefFoundError
类在编译的时候存在,但是运行的时候不存在。
NoSuchMethodError
找不到对应的方法,运行时才会抛错,这个错误在日常开发经常遇到,线上诸多bug都是来源于此。
发生的原因就是多个包依赖了不一样版本的另外一个包,比如A,B都依赖了C包,A依赖C1,B依赖C2,工程中加载了C1,但是C1中某个类缺少了C2版本的这个类的某个方法,这时候运行时,B依赖的C2方法被调用到了,就会报这个错。
既然这些问题这么头疼,且难以排查,还容易造成线上故障,那平时在开发过程中如何避免这些问题。
做好二方包的管理
重中之中就是做好二方包的管理,第三方包都是公开发布出来的,相对还是比较靠谱的,更多的是二方包的问题。二方包发布的频率比较高,开发人员的水平又参差不齐,是需要严格把控管理的。
-
线上禁止snapshot包
snapshot包是万恶之源,内容随时会被人修改,所以线上环境,是绝对不能允许snapshot包存在,否则就是一颗定时炸弹。 -
向下兼容
版本号是3位: 主版本号.次版本号.修订号
做了不兼容的更新要升级主版本号。在二方包管理时,尽量不要接口层面的方法,重命名POJO类字段等不兼容的改动,因为二方包的发布频次高,版本比较多,维护的代价比较高。很多包发布了几年,还是0.xx.xx的版本。有不兼容的改动,比如需要搭配版本时候,维护的成本都会让人奔溃。 -
尽量少的依赖
发布出来的二方包尽量少的依赖,二次依赖。对于发布出去的包小心检查,不要引入不必要的依赖。
比如有些业务repo,需要发布rpc-client给其他服务。对外的client与内部代码通过不同module管理,那么这时候就不要共用一份parent pom.xml。进行版本管理,因为内部工程使用的和外部不一样。
不同领域的client也可以拆分出来,单独进行jar包版本管理。 -
changelog
发布出来的版本必须要有changelog,做了什么改动。必须依赖什么版本的二方包也需要写清楚。 - 优雅”copy”开源包
对于外部开源包,有些需要包装一下在公司里面用。切记做好以下两点- 记录下开源版本上做了什么更改,否则后续人维护的就是一坨屎,
- 不要使用同一个包名,类名。后续别人用开源版本实现时,就会出现不同jar包有相同同一个Class,这时候就不是包冲突解决了,就会出现不确定的加载顺序,因为同一个类只会被jvm加载一次,但是加载的是哪个jar的类,就和包依赖的路径,文件名的顺序等有关。出现不同的机器加载的类不一样,十分难排查。
使用包的原则
上面说完了发布包的原则,那么使用包的原则呢。
- 了解maven仲裁机制
maven仲裁机制就是maven的依赖机制,按顺序分别是以下三点
-
优先按照依赖管理元素中指定的版本声明进行仲裁
-
若无版本声明,则按照“短路径优先”的原则(Maven2.0)进行仲裁,即选择依赖树中路径最短的版本
-
若路径长度一致,则按照“第一声明优先”的原则进行仲裁,即选择POM中最先声明的版本
- 统一基础包管理
因为间接依赖有很多的不确定性,所以需要将基础版本的包统一使用原则1<dependencyManagement>
来管理,这样这些包的版本就不会被你无意间引入的某个二方包版本给覆盖掉。常见的基础包有中间件的包,日志,util,序列化等包。
几个bad case
case 1: 那是我刚来公司的时候,第一个功能上线就翻跟头了,本地windows环境,测试linux环境都通过测试了。但是上预发的时候出了问题。排查了通宵也没搞定,差点发布延期。最后问题根源就是“开源包”乱使用的原因。项目中依赖了公司改造的某个mongo client
jar包,将类copy过来了,使用了相同的package,但是以不同的jar deploy。我来了以后,在这个工程中使用了开源的mongo client
,然后中招了。
case2: A包是一个业务基础包,非公共基础包。然后B,C业务依赖A。A包有些设计不合理,一个新来的大刀阔斧的各种重构了,升了一个版本,发布了一个release包出去,完美。然后其他业务方就炸了。因为B使用了新版本,C没有使用新版本。一个工程中引用了B,C包后,包冲突,就会发生运行时错误,NoSuchMethodError
或者NoClassDefFoundError
。这时候业务方既不能升A包的版本,也不能降A的版本,可不就炸了吗。