当使用xsltc.exe XslCompiledTransforms时,如何解决<xsl:import>和<xsl:include>元素的相对路径?

7
作为我们 Web 应用程序构建过程的一部分,我已经设置了我们的 XSLT 样式表在每次运行完整编译时使用 Microsoft 的 xsltc.exe 编译器进行构建。在本地开发期间,这很好用,因为代码是编译并托管在同一位置。然而,一旦将其放到构建服务器上,问题就出现了。
构建服务器会像我在本地做的那样编译 XSLT 样式表,但然后会运行一个脚本,将编译后的代码部署到我们内部暂存 Web 服务器上。一旦这些二进制文件从它们编译的位置移动,<xsl:import><xsl:include> 元素中的相对路径将不再正确解析,导致当运行 XSLT 样式表时出现如下异常。
Could not find a part of the path 'e:\{PATH}\xslt\docbook\VERSION'.
    at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)
    at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath)
    at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options, String msgPath, Boolean bFromProxy)
    at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize)
    at System.Xml.XmlUrlResolver.GetEntity(Uri absoluteUri, String role, Type ofObjectToReturn)
    at System.Xml.Xsl.Runtime.XmlQueryContext.GetDataSource(String uriRelative, String uriBase)

这是目前代码的大致概念:
var xslt = new XslCompiledTransform();
xslt.Load(typeof(Namespace.XslTransforms.CompiledXsltStylesheet));
xslt.Transform("input.xml", "output.xml");

目前我正在使用XslCompiledTransform.Load()方法,带有一个“Type”参数,来引入基于xsltc.exe预编译的XSLT样式表。从堆栈跟踪中可以看出,.NET框架正在使用XmlUrlResolver尝试解析这些外部样式表的实际位置,但我没有找到一种方法提供重写的XmlResolver实现,其中我可以传递指向Web服务器上这些样式表所在位置的新baseUri。

我认为我可以通过不再使用xsltc.exe进行预编译,并通过XmlReaders加载XSLT样式表来解决这个问题,因为这将让我使用其他XslCompiledTransform.Load()方法,这些方法具有一个参数,我可以提供自己的XmlResolver实现。但是,我喜欢预编译选项用于语法验证和性能,所以除非我绝对必须放弃它,否则我不想放弃它。

有没有一种方法可以使用xsltc.exe预编译这些XSLT样式表,但仍然提供一种在运行时明确声明基本URI以解决<xsl:include><xsl:import>元素的相对路径的方式?

导入/包含的样式表是否未随二进制文件一起部署(到“内部暂存 Web 服务器”)? - Zach Young
它们确实存在,但它们位于与编译它们的目录不同的目录中。 - Technetium
1
如果你在构建服务器上(编译样式表的地方)模仿暂存网页服务器的目录结构,这样做会有积极的影响吗? - Zach Young
强制文件系统采用特定的格式进行XSLT转换是一个相当严格的要求,我无法做到。我宁愿承受性能损失。不过,在SOA环境中制作XSLT Web服务的其他人可能会这样做。 - Technetium
3个回答

5

经过大量测试,我发现提供的代码在运行时会自动使用System.Xml.XmlUrlResolver 来解决<xsl:include><xsl:import>相对路径的问题是正确的。但是,当通过xsltc.exe将其放置到二进制文件中时,XmlUrlResolver的使用并不绑定到System.Xml.XslCompiledTransform上。 实际上,XmlResolver是由执行转换的System.Xml.XmlReader 上的System.Xml.XmlReaderSettingsXmlResolver属性选择的。一旦我在使用的XsltReaderSettings上设置了自定义的XmlResolver,我就能够控制相对路径的解析。

如果您想像我一样覆盖此XmlResolver,可以使用以下代码作为指南:
var customXmlResolver = new SomeCustomXmlResolver();  // Derives from XmlResolver
var xmlReaderSettings = new XmlReaderSettings {
  XmlResolver = customXmlResolver
};

var xslt = new XslCompiledTransform();
xslt.Load(typeof(Namespace.XslTransforms.CompiledXsltStylesheet));

using (var xmlReader = XmlReader.Create("input.xml", xmlReaderSettings)) {
  using (var xmlWriter = XmlWriter.Create("output.xml")) {
    xslt.Transform(xmlReader, null, xmlWriter, customXmlResolver);
  }
}

我仍在使用xsltc.exe来编译我的XSLT样式表,但是当我在Web服务器上加载这些已编译的样式表时,注入的SomeCustomXmlResolver会重写覆盖的ResolveUri()GetEntity()方法中的路径,以便可以找到基于<xsl:include><xsl:import>相对路径的引用文件。作为额外的奖励,通过在Transform()方法的末尾添加相同的XmlResolver,XML中的document()操作也将正确解析其相对路径。


你一定很困惑。你代码中的 XmlReader 仅用于读取 "input.xml" -- 而不是加载 XSLT 样式表。当执行 xslt.Load() 方法时,它根本没有引用任何 XmlReader。如果在执行 xsl:importxsl:include 时出现任何问题,Load() 方法会引发异常 -- 在你的情况下并没有发生这种情况 -- 而不使用任何对 XmlReader 的引用。 - Dimitre Novatchev
我并不困惑,Dimitre。我的代码是可行的,并且通过调试器逐步执行,我可以清楚地看到这些XSL元素的相对路径被传递到我在运行时分配的XmlResolver中。<xsl:include href="../VERSION"/><xsl:include href="param.xsl"/><xsl:include href="../lib/lib.xsl"/>等等。这就是正确的答案。 - Technetium
Technetium: 您至少有一个导入/包含的样式表使用带有相对或空URL的document()函数--这是真正的问题,您从未解释过。如果没有任何导入/包含的样式表引用document()函数,则根本没有问题。我将在明天找出一些空闲时间来构建证明这一点的示例。 - Dimitre Novatchev

2
有没有一种方法可以使用xsltc.exe预编译这些XSLT样式表,但仍然提供一种在运行时明确指定基本URI以便解析相对路径的和元素的方式?

XslCompiledTransform.CompileToType()

这个静态方法接受的参数之一是:
XmlResolver stylesheetResolver

啊,不错!这看起来非常有前途。我会试一下,并在我验证后将其标记为正确答案。 MSDN文档甚至说明xsltc.exe是此的包装器,因此仍然存在一个重要问题,即XmlResolver是否在编译时缓存baseUri,还是在运行时再次运行XmlResolver(这正是我想要的,也是基于堆栈跟踪,我认为会发生的)。 - Technetium
@Technetium:我认为在编译时早期修复解析器是没有意义的。这应该是你想要的。 - Dimitre Novatchev
就像我在高中时所做的那样,我在这里行动过早,因为我太兴奋了。实际上,这是一个错误的答案。虽然Dimitre正确地指出了此方法中提供的XmlResolver用于解析XSLT样式表在编译时的<xsl:include><xml:import>元素,但它在运行时不再使用。运行时使用的XmlResolver是由源XML文件的XmlReaderSettings选择的。有关详细信息,请参见我的答案。 - Technetium

0

我不知道这是否会破坏你的系统,但是你可以考虑以下方式:

  1. 使用 xsltc.exe 进行编译
  2. 部署二进制文件
  3. 使用 this Load() 加载二进制文件

或者你可以:

  1. 使用 import/include 指令部署所需的所有样式表
  2. 使用 this Load() 加载主样式表,并指定 import/include 的解析器

这样做似乎仍然能够在运行时获得“已编译”的样式表的好处。


我肯定可以直接从Web服务器的文件系统中访问这些XSLT样式表。即使主样式表使用xsltc.exe编译,为了使转换功能正常,它们也必须存在。然而,除非有关于您指定的Load()方法的某些我不理解的事情,否则每次在运行时加载样式表时都会编译它们,这是我试图避免的性能问题之一。这也意味着我必须将语法验证降级为单元测试状态。 - Technetium
哦,我以为 DLL 在一开始就被加载了一次。那样是行不通的。 - Zach Young

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接