關(guān)于JAR包版本沖突的幾種解決招數(shù)

概述

Javeer們一定遇到過(guò)NoSuchMethodError的錯(cuò)誤,一旦碰到這種錯(cuò)誤,必是JAR包版本沖突的問(wèn)題無(wú)疑,版本沖突分開(kāi)為以下兩種情況:

  1. 同構(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)多版本沖突”;

  2. 協(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包):

image

在生產(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

image

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

小結(jié)

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容