概述
Javeer們一定遇到過(guò)NoSuchMethodError的錯(cuò)誤,一旦碰到這種錯(cuò)誤,必是JAR包版本沖突的問(wèn)題無(wú)疑,版本沖突分開(kāi)為以下兩種情況:
同構(gòu)件多版本沖突
類路徑同時(shí)中存在多個(gè)相同構(gòu)件的版本,如即存在poi-ooxml-3.11.jar,又存在poi-ooxml-3.9.jar,項(xiàng)目真正運(yùn)行時(shí)需要的是3.11,而JVM加載到的是3.9,這種情況,我們稱之為“同構(gòu)多版本沖突”;-
協(xié)作構(gòu)件版本沖突:假設(shè)構(gòu)件A依賴于構(gòu)件B,A的x版本需要B的y版本,如果引入的是B的z版本,那么A和B就不能很好的搭檔,沖突產(chǎn)生,此為“協(xié)作構(gòu)件版本沖突”。
構(gòu)件版本沖突既可以在開(kāi)發(fā)環(huán)境時(shí)發(fā)生,也有可能在開(kāi)發(fā)環(huán)境下沒(méi)有問(wèn)題,部署到生產(chǎn)環(huán)境下才發(fā)生。這是因?yàn)镴VM的類加載機(jī)制決定的,當(dāng)JVM需要某個(gè)Class時(shí),它先看JVM內(nèi)部有沒(méi)有這個(gè)Class,如果有就直接使用,如果沒(méi)有才在類路徑下加載新的JAR。如果JVM加載到A-x.jar,但實(shí)現(xiàn)上我們卻需要A-y.jar,則惡魔就從瓶子里出來(lái)了。
解決招數(shù)
同構(gòu)件多版本沖突
對(duì)于我們需要A-x.jar,JVM卻加載到A-y.jar的情況,我們只要將類路徑下那個(gè)A-y.jar移除,或者通過(guò)應(yīng)用服務(wù)器的JAR優(yōu)先加載順序機(jī)制讓A-x.jar優(yōu)先于A-y.jar就OK了。對(duì)于TOMCAT,WAS等應(yīng)用服務(wù)器,都有設(shè)置JAR的優(yōu)先加載策略,如是以項(xiàng)目的類路徑優(yōu)先,還是應(yīng)用服務(wù)器的共享類路徑優(yōu)先。如果是以共享類路徑優(yōu)先,假設(shè)共享類路徑下有一個(gè)A-y.jar,則你項(xiàng)目WEB-INF/lib下的A-x.jar就加載不到。
所以解決這個(gè)問(wèn)題的核心在于:找到運(yùn)行期Class類到底是從哪個(gè)JAR包加載的。在開(kāi)發(fā)環(huán)境下,可以使用斷點(diǎn)查看類實(shí)例源自的JAR,下面提供了一個(gè)獲取實(shí)例類所在位置的工具類:
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();
}
}
在任何斷點(diǎn)處動(dòng)態(tài)執(zhí)行該類就可看到類源自的JAR包了,找到JAR包你就可以分析到底是不是你想要的那個(gè)版本(下圖是演示在IDE下通過(guò)ALT+F8打開(kāi)Evaluate Expression對(duì)話框動(dòng)態(tài)查看Class所在JAR包):

在生產(chǎn)環(huán)境下,你可以通過(guò)寫LOG日志輸出來(lái)查看,但這種方式需要你代碼中有寫日志的代碼,可能涉及到重新編譯和部署,肯定不是最好的方式。最好的是打開(kāi)一個(gè)頁(yè)面,通過(guò)輸入類名獲取類所在的路徑信息,下面是一個(gè)完成此功能的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參數(shù)為類的全名,不需要.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 + "實(shí)例的物理文件位于:");
out.print("<hr>");
out.print(classLocation);
}
else {
out.print("類" + className + "沒(méi)有對(duì)應(yīng)的物理文件。<br>");
out.print("錯(cuò)誤:" + error);
}
}catch(Exception e)
{
out.print("異常。"+e.getMessage());
}
%>
</body>
</html>
打開(kāi)頁(yè)面,通過(guò)URL參數(shù)指定類名,頁(yè)面即可顯示類加載自哪個(gè)JAR了,非常方便!
協(xié)作構(gòu)件版本沖突
針對(duì)協(xié)作構(gòu)件之間的版本沖突,其實(shí)就是要找到協(xié)作構(gòu)件之間的匹配版本了,一般情況下,主構(gòu)件的發(fā)布網(wǎng)站都會(huì)有相關(guān)幫助文檔給出明確的說(shuō)明,如Apache POI,其poi-ooxml構(gòu)件需要依賴于poi-ooxml-schemas構(gòu)件,版本的匹配關(guān)系說(shuō)明如下:
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.
以上說(shuō)明引用自[pache的網(wǎng)站http://poi.apache.org/overview.html
再如Mockito和PowerMock版本匹配也可以從powermock的網(wǎng)站中找到:
https://github.com/jayway/powermock/wiki/MockitoUsage

簡(jiǎn)單的方法當(dāng)然是找搜索引擎了,度娘找這種不夠了,谷哥不在,用bing吧,關(guān)鍵詞把兩個(gè)構(gòu)件名寫入搜索,如在bing中搜索:

小結(jié)
相對(duì)來(lái)說(shuō),同構(gòu)件多版本沖突好解決些,找到源JAR看一眼不對(duì)換掉或更改應(yīng)用服務(wù)器JVM 類加載順序即可。但構(gòu)件版本匹配沖突則不太好解決,一定要盡量上其官網(wǎng)查資料,不要做無(wú)謂的換包嘗試哦。