在Java / Maven中处理“Xerces hell”?

Dealing with “Xerces hell” in Java/Maven?

在我的办公室里,仅仅提到Xerces这个词就足以激起开发者的杀伤力。粗略地看一下其他Xerces问题,似乎表明几乎所有Maven用户在某个时候都会被这个问题"触动"。不幸的是,了解这个问题需要一点关于行刑史的知识…

历史

  • XECES是Java生态系统中使用最广泛的XML解析器。几乎每一个用Java编写的库或框架都使用XECES(如果不是直接的话)。

  • 官方二进制文件中包含的Xercesjar直到今天还没有版本化。例如,Xerces2.11.0实现jar的名称是xercesImpl.jar,而不是xercesImpl-2.11.0.jar

  • Xerces团队不使用Maven,这意味着他们不使用Maven上传官方版本到Maven Central。

  • Xerces以前是作为单个jar(xerces.jar发布的),但被分成两个jar,一个包含API(xml-apis.jar和一个包含这些API(xercesImpl.jar的实现)。许多老的maven pom仍然宣称依赖于xerces.jar。在过去的某个时候,Xerces也以xmlParserAPIs.jar的形式发布,这也是一些老的POM所依赖的。

  • 那些将JAR部署到Maven存储库的人分配给XML API和XerceImpl JAR的版本通常是不同的。例如,XML API的版本可能是1.3.03,XerceImpl的版本可能是2.8.0,即使两者都来自Xerces2.8.0。这是因为人们经常用XML API JAR实现的规范版本来标记它。这里有一个非常好的,但不完全的分解。

  • 为了使问题复杂化,XECES是JRE中包含的Java API(JAXP)的引用实现中使用的XML解析器。实现类在com.sun.*名称空间下重新打包,这使得直接访问它们很危险,因为它们在某些JRE中可能不可用。但是,并非所有Xerces功能都通过java.*javax.*API公开;例如,没有任何API公开Xerces序列化。

  • 除了混乱之外,几乎所有servlet容器(jboss、jetty、glassfish、tomcat等)都在一个或多个/lib文件夹中随Xerces一起装运。

问题冲突解决

因为上面的一些原因,或者可能全部原因,很多组织在其POMS。如果您有一个小应用程序,并且只使用Maven Central,这实际上不是问题,但是对于Artifactory或Nexus代理多个存储库(JBoss、Hibernate等)的企业软件来说,这很快就会成为问题:

xml-apis proxied by Artifactory

例如,A组织可能将xml-apis发布为:

1
2
3
<groupId>org.apache.xerces</groupId>
xml-apis</artifactId>
<version>2.9.1</version>

同时,B组织可以发布与以下内容相同的jar

1
2
3
<groupId>xml-apis</groupId>
xml-apis</artifactId>
<version>1.3.04</version>

虽然B的jar比A的jar低,但马文不知道他们是同一件艺术品,因为他们有不同的因此,它不能执行冲突解决,而且两者都不能jars将作为已解决的依赖项包括在内:

resolved dependencies with multiple xml-apis

类加载器地狱

如上所述,jre在jaxp ri中带有xerces。虽然最好将所有Xerces-Maven依赖项标记为s或,但您所依赖的第三方代码可能与您使用的JDK的JAXP中提供的版本一起工作,也可能不工作。此外,您还可以在servlet容器中装运Xerces JAR。这给您留下了许多选择:是否删除servlet版本并希望容器在JAXP版本上运行?离开servlet版本,希望您的应用程序框架在servlet版本上运行,这样更好吗?如果上面概述的一个或两个未解决的冲突设法进入到您的产品中(在大型组织中很容易发生),您很快就会发现自己身处类加载器的地狱,想知道类加载器在运行时选择的是哪个版本的Xerces,以及它是否会在Windows和Linux中选择相同的JAR(可能不会)。

解决?

我们已经尝试将所有Xerces Maven依赖项标记为,但这很难执行(特别是在大型团队中),因为这些工件有很多别名(xml-apisxercesxercesImplxmlParserAPIs等)。此外,我们的第三方libs/frameworks可能无法在JAXP版本或servlet容器提供的版本上运行。

我们怎样才能最好地用Maven解决这个问题?我们是否必须对依赖项进行如此细粒度的控制,然后依赖于分层类加载?是否有某种方法可以全局排除所有Xerces依赖项,


有2.11.0罐(和源罐!)自2013年2月20日起,马文中心的Xerces!见Maven Central的Xerces。我想知道他们为什么没有解决https://issues.apache.org/jira/browse/xercesj-1454…

我曾经用过:

1
2
3
4
5
<dependency>
    <groupId>xerces</groupId>
    xercesImpl</artifactId>
    <version>2.11.0</version>
</dependency>

所有的依赖关系都已经很好地解决了——甚至是正确的xml-apis-1.4.01

最重要的(以及过去不明显的)是,Maven Central的罐子和官方的Xerces-J-bin.2.11.0.zip发行版中的罐子是一样的。

但是,我找不到xml-schema-1.1-beta版本,因为附加的依赖关系,它不能是Maven classifier版本。


坦率地说,我们遇到的几乎所有东西在JAXP版本中都可以正常工作,所以我们总是排除xml-apisxercesImpl


您可以将MavenEnforcer插件与禁止的依赖规则一起使用。这将允许您禁止所有不想要的别名,只允许您想要的别名。如果违反这些规则,项目的Maven构建将失败。此外,如果此规则适用于企业中的所有项目,则可以将插件配置放入企业父POM中。

见:

  • http://maven.apache.org/plugins/maven-enforcer-plugin/
  • http://maven.apache.org/enforcer/enforcer-rules/banneddependences.html

我知道这并不能准确回答问题,但对于来自谷歌的PPL,恰好使用Gradle进行依赖性管理:

我设法解决了Gradle的所有Xerces/Java8问题:

1
2
3
4
configurations {
    all*.exclude group: 'xml-apis'
    all*.exclude group: 'xerces'
}


我想你需要回答一个问题:

是否存在应用程序中所有内容都可以使用的xerces*.jar?

如果不是这样的话,你基本上是被拧了,必须使用OSGi之类的工具,这样你就可以同时加载不同版本的库。请注意,它基本上将JAR版本问题替换为类加载器问题…

如果存在这样的版本,您可以让您的存储库返回所有依赖项的版本。这是一个丑陋的黑客,最终会在类路径中多次使用相同的Xerces实现,但比使用多个不同版本的Xerces要好。

您可以排除对Xerces的每个依赖项,并将其添加到您要使用的版本中。

我想知道您是否可以编写某种版本解析策略作为Maven的插件。这可能是最好的解决方案,但如果可行的话,需要进行一些研究和编码。

对于运行时环境中包含的版本,您必须确保在考虑服务器的lib文件夹之前,将其从应用程序类路径中删除,或者先考虑应用程序jar进行类加载。

所以总结一下:这是一个烂摊子,不会改变。


这里还有一个尚未探讨的选项:将maven中的xerces依赖项声明为可选:

1
2
3
4
5
6
<dependency>
   <groupId>xerces</groupId>
   xercesImpl</artifactId>
   <version>...</version>
   <optional>true</optional>
</dependency>

基本上,这样做的目的是强制所有依赖项声明它们的Xerces版本,否则它们的项目将无法编译。如果他们想覆盖这种依赖关系,我们欢迎他们这样做,但是他们将拥有潜在的问题。

这为下游项目创造了强有力的激励:

  • 做出积极的决定。他们是使用相同版本的Xerces还是使用其他版本的Xerces?
  • 实际上测试它们的解析(例如,通过单元测试)和类加载,同时不要使它们的类路径混乱。

并非所有开发人员都跟踪新引入的依赖项(例如,使用mvn dependency:tree)。这种方法将立即引起他们的注意。

它在我们的组织中工作得很好。在它被介绍之前,我们曾经生活在同一个地狱里。


您应该首先进行调试,以帮助识别XML地狱的级别。在我看来,第一步是增加

1
2
3
-Djavax.xml.parsers.SAXParserFactory=com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl
-Djavax.xml.transform.TransformerFactory=com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl
-Djavax.xml.parsers.DocumentBuilderFactory=com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl

到命令行。如果可以,那么就开始排除库。如果没有,则添加

1
-Djaxp.debug=1

到命令行。


每一个Maven项目都应该停止依赖于Xerces,他们可能不是真的。XML API和IMPL自1.4以来一直是Java的一部分。不需要依赖XECES或XML API,就像说您依赖Java或Swing。这是含蓄的。

如果我是一个Maven RePo的老板,我会编写一个脚本来递归删除XRESE依赖项,并写一个Read Me,称这个RPO需要Java 1.4。

任何真正中断,因为它通过XORACE直接引用Or.ApHACK导入需要一个代码修复,使其达到Java 1.4级(并且自2002以来已经完成)或通过认可的LIBS在JVM级的解决方案,而不是在Maven中。


我的朋友很简单,举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
<dependency>
            <groupId>xalan</groupId>
            xalan</artifactId>
            <version>2.7.2</version>
            <scope>${my-scope}</scope>
            <exclusions>
                <exclusion>
                    <groupId>xml-apis</groupId>
                    xml-apis</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

如果要签入终端(本例中为Windows控制台),则Maven树没有问题:

1
mvn dependency:tree -Dverbose | grep --color=always '(.* conflict\|^' | less -r

显然,xerces:xml-apis:1.4.01已经不在maven中心了,但这正是xerces:xercesImpl:2.11.0所指的。

这对我很有用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
  <groupId>xerces</groupId>
  xercesImpl</artifactId>
  <version>2.11.0</version>
  <exclusions>
    <exclusion>
      <groupId>xerces</groupId>
      xml-apis</artifactId>
    </exclusion>
  </exclusions>
</dependency>
<dependency>
  <groupId>xml-apis</groupId>
  xml-apis</artifactId>
  <version>1.4.01</version>
</dependency>

除了排除之外,模块化依赖性将有帮助。

对于一个平面类加载(独立应用程序)或半层次(jboss as/eap 5.x),这是一个问题。

但是对于OSGi和JBoss模块这样的模块化框架,这不再是什么痛苦了。图书馆可以独立使用他们想要的任何图书馆。

当然,仍然建议只使用一个实现和版本,但是如果没有其他方法(使用更多libs的额外特性),那么模块化可能会节省您的时间。

jboss模块的一个很好的例子就是jboss as 7/eap 6/wildfly 8,它最初是为它开发的。

模块定义示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.1" name="org.jboss.msc">
    <main-class name="org.jboss.msc.Version"/>
    <properties>
        <property name="my.property" value="foo"/>
    </properties>
    <resources>
        <resource-root path="jboss-msc-1.0.1.GA.jar"/>
    </resources>
    <dependencies>
        <module name="javax.api"/>
        <module name="org.jboss.logging"/>
        <module name="org.jboss.modules"/>
        <!-- Optional deps -->
        <module name="javax.inject.api" optional="true"/>
        <module name="org.jboss.threads" optional="true"/>
    </dependencies>
</module>

与OSGi相比,JBoss模块更简单、更快。虽然缺少某些特性,但对于大多数项目(大多数)来说,它已经足够由一个供应商控制,并且允许惊人的快速引导(由于并行依赖关系解析)。

注意,Java 8正在进行模块化的工作,但AFIK主要是对JRE本身进行模块化,而不确定它是否适用于应用程序。