关于JAR包版本冲突的几个应对招数总结
程序员文章站
2022-04-15 19:35:53
...
概述
Javeer们一定遇到过NoSuchMethodError的错误,一旦碰到这种错误,必是JAR包版本冲突的问题无疑,版本冲突分开为以下两种情况:
构件版本冲突既可以在开发环境时发生,也有可能在开发环境下没有问题,部署到生产环境下才发生。这是因为JVM的类加载机制决定的,当JVM需要某个Class时,它先看JVM内部有没有这个Class,如果有就直接使用,如果没有才在类路径下加载新的JAR。如果JVM加载到A-x.jar,但实现上我们却需要A-y.jar,则恶魔就从瓶子里出来了。
解决招数
同构件多版本冲突
对于我们需要A-x.jar,JVM却加载到A-y.jar的情况,我们只要将类路径下那个A-y.jar移除,或者通过应用服务器的JAR优先加载顺序机制让A-x.jar优先于A-y.jar就OK了。对于TOMCAT,WAS等应用服务器,都有设置JAR的优先加载策略,如是以项目的类路径优先,还是应用服务器的共享类路径优先。如果是以共享类路径优先,假设共享类路径下有一个A-y.jar,则你项目WEB-INF/lib下的A-x.jar就加载不到。
所以解决这个问题的核心在于:找到运行期Class类到底是从哪个JAR包加载的。在开发环境下,可以使用断点查看类实例源自的JAR,下面提供了一个获取实例类所在位置的工具类:
在任何断点处动态执行该类就可看到类源自的JAR包了,找到JAR包你就可以分析到底是不是你想要的那个版本(下图是演示在IDE下通过ALT+F8打开Evaluate Expression对话框动态查看Class所在JAR包):
在生产环境下,你可以通过写LOG日志输出来查看,但这种方式需要你代码中有写日志的代码,可能涉及到重新编译和部署,肯定不是最好的方式。最好的是打开一个页面,通过输入类名获取类所在的路径信息,下面是一个完成此功能的JSP:
打开页面,通过URL参数指定类名,页面即可显示类加载自哪个JAR了,非常方便!
协作构件版本冲突
针对协作构件之间的版本冲突,其实就是要找到协作构件之间的匹配版本了,一般情况下,主构件的发布网站都会有相关帮助文档给出明确的说明,如Apache POI,其poi-ooxml构件需要依赖于poi-ooxml-schemas构件,版本的匹配关系说明如下:
poi-ooxml requires poi-ooxml-schemas. This is a substantially smaller version of the ooxml-schemas jar (ooxml-schemas-1.3.jar for POI 3.14 or later, ooxml-schemas-1.1.jar for POI 3.7 up to POI 3.13, ooxml-schemas-1.0.jar for POI 3.5 and 3.6). The larger ooxml-schemas jar is normally only required for development. Similarly, the ooxml-security jar, which contains all of the classes relating to encryption and signing, is normally only required for development. A subset of its contents are in poi-ooxml-schemas. This JAR is ooxml-security-1.1.jar for POI 3.14 onwards and ooxml-security-1.0.jar prior to that.
以上说明引用自apache的网站:http://poi.apache.org/overview.html
再如Mockito和PowerMock版本匹配也可以从powermock的网站中找到:
https://github.com/jayway/powermock/wiki/MockitoUsage
简单的方法当然是找搜索引擎了,度娘找这种不够了,谷哥不在,用bing吧,关键词把两个构件名写入搜索,如在bing中搜索:
小结
相对来说,同构件多版本冲突好解决些,找到源JAR看一眼不对换掉或更改应用服务器JVM 类加载顺序即可。但构件版本匹配冲突则不太好解决,一定要尽量上其官网查资料,不要做无谓的换包尝试哦。
以后在此维护新文章:
https://www.jianshu.com/u/d7f090245ddd
Javeer们一定遇到过NoSuchMethodError的错误,一旦碰到这种错误,必是JAR包版本冲突的问题无疑,版本冲突分开为以下两种情况:
- 同构件多版本冲突:类路径同时中存在多个相同构件的版本,如即存在poi-ooxml-3.11.jar,又存在poi-ooxml-3.9.jar,项目真正运行时需要的是3.11,而JVM加载到的是3.9,这种情况,我们称之为“同构多版本冲突”;
- 协作构件版本冲突:假设构件A依赖于构件B,A的x版本需要B的y版本,如果引入的是B的z版本,那么A和B就不能很好的搭档,冲突产生,此为“协作构件版本冲突”。
构件版本冲突既可以在开发环境时发生,也有可能在开发环境下没有问题,部署到生产环境下才发生。这是因为JVM的类加载机制决定的,当JVM需要某个Class时,它先看JVM内部有没有这个Class,如果有就直接使用,如果没有才在类路径下加载新的JAR。如果JVM加载到A-x.jar,但实现上我们却需要A-y.jar,则恶魔就从瓶子里出来了。
解决招数
同构件多版本冲突
对于我们需要A-x.jar,JVM却加载到A-y.jar的情况,我们只要将类路径下那个A-y.jar移除,或者通过应用服务器的JAR优先加载顺序机制让A-x.jar优先于A-y.jar就OK了。对于TOMCAT,WAS等应用服务器,都有设置JAR的优先加载策略,如是以项目的类路径优先,还是应用服务器的共享类路径优先。如果是以共享类路径优先,假设共享类路径下有一个A-y.jar,则你项目WEB-INF/lib下的A-x.jar就加载不到。
所以解决这个问题的核心在于:找到运行期Class类到底是从哪个JAR包加载的。在开发环境下,可以使用断点查看类实例源自的JAR,下面提供了一个获取实例类所在位置的工具类:
import java.io.File; import java.net.MalformedURLException; import java.net.URL; import java.security.CodeSource; import java.security.ProtectionDomain; public class ClassLocationUtils { public static String where(String className){ try { Class<?> theClazz = Class.forName(className); return where(theClazz); } catch (ClassNotFoundException e) { return "CLASS_NOT_FOUND:"+className; } } /** * 获取类所有的路径 * @param cls * @return */ public static String where(final Class cls) { if (cls == null)throw new IllegalArgumentException("null input: cls"); URL result = null; final String clsAsResource = cls.getName().replace('.', '/').concat(".class"); final ProtectionDomain pd = cls.getProtectionDomain(); if (pd != null) { final CodeSource cs = pd.getCodeSource(); if (cs != null) result = cs.getLocation(); if (result != null) { if ("file".equals(result.getProtocol())) { try { if (result.toExternalForm().endsWith(".jar") || result.toExternalForm().endsWith(".zip")) result = new URL("jar:".concat(result.toExternalForm()) .concat("!/").concat(clsAsResource)); else if (new File(result.getFile()).isDirectory()) result = new URL(result, clsAsResource); } catch (MalformedURLException ignore) {} } } } if (result == null) { final ClassLoader clsLoader = cls.getClassLoader(); result = clsLoader != null ? clsLoader.getResource(clsAsResource) : ClassLoader.getSystemResource(clsAsResource); } return result.toString(); } }
在任何断点处动态执行该类就可看到类源自的JAR包了,找到JAR包你就可以分析到底是不是你想要的那个版本(下图是演示在IDE下通过ALT+F8打开Evaluate Expression对话框动态查看Class所在JAR包):
在生产环境下,你可以通过写LOG日志输出来查看,但这种方式需要你代码中有写日志的代码,可能涉及到重新编译和部署,肯定不是最好的方式。最好的是打开一个页面,通过输入类名获取类所在的路径信息,下面是一个完成此功能的JSP:
<%@page contentType="text/html; charset=UTF-8"%> <%@page import="java.io.File,java.net.*,java.io.*"%> <%! public static URL getClassLocation(final Class cls) { if (cls == null)throw new IllegalArgumentException("null input: cls"); URL result = null; final String clsAsResource = cls.getName().replace('.', '/').concat(".class"); final ProtectionDomain pd = cls.getProtectionDomain(); // java.lang.Class contract does not specify if 'pd' can ever be null; // it is not the case for Sun's implementations, but guard against null // just in case: if (pd != null) { final CodeSource cs = pd.getCodeSource(); // 'cs' can be null depending on the classloader behavior: if (cs != null) result = cs.getLocation(); if (result != null) { // Convert a code source location into a full class file location // for some common cases: if ("file".equals(result.getProtocol())) { try { if (result.toExternalForm().endsWith(".jar") || result.toExternalForm().endsWith(".zip")) result = new URL("jar:".concat(result.toExternalForm()) .concat("!/").concat(clsAsResource)); else if (new File(result.getFile()).isDirectory()) result = new URL(result, clsAsResource); } catch (MalformedURLException ignore) {} } } } if (result == null) { // Try to find 'cls' definition as a resource; this is not // document.d to be legal, but Sun's implementations seem to //allow this: final ClassLoader clsLoader = cls.getClassLoader(); result = clsLoader != null ? clsLoader.getResource(clsAsResource) : ClassLoader.getSystemResource(clsAsResource); } return result; } %> <html> <head> <title>srcAdd.jar</title> </head> <body bgcolor="#ffffff"> 使用方法,className参数为类的全名,不需要.class后缀,如 srcAdd.jsp?className=java.net.URL <% try { String classLocation = null; String error = null; String className = request.getParameter("className"); classLocation = ""+getClassLocation(Class.forName(className)); if (error == null) { out.print("类" + className + "实例的物理文件位于:"); out.print("<hr>"); out.print(classLocation); } else { out.print("类" + className + "没有对应的物理文件。<br>"); out.print("错误:" + error); } }catch(Exception e) { out.print("异常。"+e.getMessage()); } %> </body> </html>
打开页面,通过URL参数指定类名,页面即可显示类加载自哪个JAR了,非常方便!
协作构件版本冲突
针对协作构件之间的版本冲突,其实就是要找到协作构件之间的匹配版本了,一般情况下,主构件的发布网站都会有相关帮助文档给出明确的说明,如Apache POI,其poi-ooxml构件需要依赖于poi-ooxml-schemas构件,版本的匹配关系说明如下:
引用
poi-ooxml requires poi-ooxml-schemas. This is a substantially smaller version of the ooxml-schemas jar (ooxml-schemas-1.3.jar for POI 3.14 or later, ooxml-schemas-1.1.jar for POI 3.7 up to POI 3.13, ooxml-schemas-1.0.jar for POI 3.5 and 3.6). The larger ooxml-schemas jar is normally only required for development. Similarly, the ooxml-security jar, which contains all of the classes relating to encryption and signing, is normally only required for development. A subset of its contents are in poi-ooxml-schemas. This JAR is ooxml-security-1.1.jar for POI 3.14 onwards and ooxml-security-1.0.jar prior to that.
以上说明引用自apache的网站:http://poi.apache.org/overview.html
再如Mockito和PowerMock版本匹配也可以从powermock的网站中找到:
https://github.com/jayway/powermock/wiki/MockitoUsage
简单的方法当然是找搜索引擎了,度娘找这种不够了,谷哥不在,用bing吧,关键词把两个构件名写入搜索,如在bing中搜索:
小结
相对来说,同构件多版本冲突好解决些,找到源JAR看一眼不对换掉或更改应用服务器JVM 类加载顺序即可。但构件版本匹配冲突则不太好解决,一定要尽量上其官网查资料,不要做无谓的换包尝试哦。
以后在此维护新文章:
https://www.jianshu.com/u/d7f090245ddd