`

关于JAR包版本冲突的几个应对招数总结

阅读更多
概述

   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

  • 大小: 43.1 KB
  • 大小: 119.3 KB
  • 大小: 42.9 KB
分享到:
评论
1 楼 飞天奔月 2016-08-03  
程序员必须要会 fangqian,技术问题 还是需要 google的

相关推荐

Global site tag (gtag.js) - Google Analytics