上周,我被要求用Java编写一些东西,该东西能够将单个30GB XML文件拆分为可配置文件大小的较小部分。 该文件的使用者将是一个中间件应用程序,该应用程序存在XML较大的问题。 在后台,它使用某种DOM解析技术,使它在一段时间后耗尽内存。 由于它是基于供应商的中间件,因此我们无法自行纠正。 最好的选择是创建一些预处理工具,该工具会先将大文件分成多个较小的块,然后再由中间件处理。
XML文件带有一个相应的W3C模式,该模式由强制性头部分和紧随其后嵌套有多个0 .. *数据元素的内容元素组成。 对于演示代码,我以简化形式重新创建了架构:
标头的大小可以忽略。 单个数据元素的重复也很小,可以说少于50kB。 由于数据元素重复的次数,XML太大了。 要求是:
- 分割后的XML的每一部分都应为语法有效的XML,并且每一部分还应针对原始模式进行验证
- 该工具应根据架构验证XML,并报告所有验证错误。 验证不得阻塞,并且不可在输出中跳过非验证元素或属性
- 对于标头,决定将其复制到每个新的输出文件中,而不是将其复制到每个新的输出文件中,并使用一些处理信息和一些默认值来重新生成该标头
因此,使用诸如Unix Split之类的二进制拆分工具是不可能的。 在固定数量的字节之后,这将拆分,从而确保XML损坏。 我不太确定,但是诸如Split之类的工具也不了解编码。 因此,在字节“ x”之后进行拆分不仅会导致在XML元素的中间进行拆分(例如),而且甚至会在字符编码序列的中间进行拆分(例如,在使用经过UTF8编码的Unicode时)。 显然,我们需要更智能的东西。
XSLT作为核心技术也是行不通的。 乍一看,可能会很想尝试:使用XSLT2.0,可以从单个输入文件创建多个输出文件。 甚至可以在转换时验证输入文件。 但是,细节始终是魔鬼。 否则,在Java中进行简单的操作(例如将验证错误写入单独的文件或检查当前输出文件的大小)可能需要自定义Java代码。 对于Xalan和Saxon来说,当然可以有这样的扩展,但是Xalan不是XSLT2.0实现,因此只剩下Saxon。 最后但并非最不重要的一点是,XSLT1.0 / 2.0是非流式的,这意味着它们会将整个源文档读入内存,因此这显然将XSLT排除在了可能性之外。
剩下的唯一选择就是Java XML解析。 当然,在这种情况下,理想的选择是StAX。 我不在这里进行SAX与StAX的比较,事实是StAX能够针对架构的身份进行验证(至少某些解析器可以)并且还可以编写XML。 而且,与SAX相比,API的使用要容易得多,因为基于pull的API提供了对迭代文档的更多控制,并且比SAX的推送方式更令人愉快。 好的,我们需要什么:
- 能够验证XML的StAX实现
- Oracle的JDK默认附带SJSXP作为StAX实现,但是此验证无效。
- 最好具有某种对象/ XML映射技术,用于(重新)创建标头,而不是手动摆弄元素并必须查找正确的数据类型/格式
- 显然是JAXB。
该代码有点大,无法在此处整体显示。 可以访问源文件,XSD和测试XML
了这里 GitHub上。 它具有Maven pom文件,因此您应该能够在选择的IDE中将其导入。 JAXB绑定编译器将自动编译模式,并将生成的源放在类路径上。
public void startSplitting() throws Exception {XMLStreamReader2 xmlStreamReader = ((XMLInputFactory2) XMLInputFactory.newInstance()).createXMLStreamReader(BigXmlTest.class.getResource("/BigXmlTest.xml"));PrintWriter validationResults = enableValidationHandling(xmlStreamReader);int fileNumber = 0;int dataRepetitions = 0;XMLStreamWriter xmlStreamWriter = openOutputFileAndWriteHeader(++fileNumber); // Prepare first file
第一行创建了StAX流读取器,这意味着我们正在使用游标API。 迭代器API使用XMLEventReader类。 类名中还有一个奇怪的“ 2”,它表示Woodstox的StAX 2功能,其中之一可能是对验证的支持。 从
在这里 :
StAX2 is an experimental API that is intended to extend basic StAX specifications
in a way that allows implementations to experiment with features before they
end up in the actual StAX specification (if they do). As such, it is intended
to be freely implementable by all StAX implementations same way as StAX, but
without going through a formal JCP process. Currently Woodstox is the only
known implementation.
可以在“ enableValidationHandling”中看到
源文件(如果需要)。 我将重点介绍重要的部分。 首先,加载XML模式:
XMLValidationSchema xmlValidationSchema = xmlValidationSchemaFactory.createSchema(BigXmlTest.class.getResource("/BigXmlTest.xsd"));
用于将可能的验证结果写入输出文件的回调;
public void reportProblem(XMLValidationProblem validationError) throws XMLValidationException {validationResults.write(validationError.getMessage()+ "Location:"+ ToStringBuilder.reflectionToString(validationError.getLocation(),ToStringStyle.SHORT_PREFIX_STYLE) + "\r\n");}
“ openOutputFileAndWriteHeader”将创建一个XMLStreamWriter(它又是游标API的一部分,迭代器API具有XMLEventWriter),我们可以将其输出或原始XML文件的一部分。 它还将使用JAXB创建我们的标头,并将其写入输出。 默认情况下,使用Schema编译器(xjc)生成JAXB对象。
private XMLStreamWriter openOutputFileAndWriteHeader(int fileNumber) throws Exception {XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newInstance();xmlOutputFactory.setProperty(XMLOutputFactory.IS_REPAIRING_NAMESPACES, true);XMLStreamWriter writer = xmlOutputFactory.createXMLStreamWriter(new FileOutputStream(new File(System.getProperty("java.io.tmpdir"), "BigXmlTest." + fileNumber + ".xml")));writer.setDefaultNamespace(DOCUMENT_NS);writer.writeStartDocument();writer.writeStartElement(DOCUMENT_NS, BIGXMLTEST_ROOT_ELEMENT);writer.writeDefaultNamespace(DOCUMENT_NS);HeaderType header = objectFactory.createHeaderType();header.setSomeHeaderElement("Something something darkside");marshaller.marshal(new JAXBElement<HeaderType>(new QName(DOCUMENT_NS, HEADER_ELEMENT, ""), HeaderType.class,HeaderType.class, header), writer);writer.writeStartElement(CONTENT_ELEMENT);return writer;}
在第3行,我们启用“修复名称空间”。 规格说明如下:
javax.xml.stream.isRepairingNamespaces:
Function: Creates default prefixes and associates them with Namespace URIs.
Type: Boolean
Default Value: False
Required: Yes
我从中了解到,处理默认名称空间是必需的。 事实是,如果未启用,则不会以任何方式编写默认名称空间。 在第6行,我们设置默认名称空间。 设置它实际上不会将其写入流。 因此,需要writeDefaultNamespace(第9行),但这只能在写入start元素之后才能完成。 因此,您必须在编写任何元素之前定义默认名称空间,但是您需要在编写第一个元素之后编写默认名称空间。 理由是StAX需要知道它是否必须为要写yes或no的根元素生成前缀。
在第8行,我们编写了root元素。 指示此元素所属的名称空间很重要。 如果您未指定前缀,则会为您生成一个前缀,或者,在本例中,将不会生成任何前缀,因为StAX知道我们已经设置了默认名称空间。 如果您要删除第6行的默认名称空间指示,则将为根元素添加前缀(带有随机前缀),例如:<wstxns1:BigXmlTest xmlns:wstxns1 =“ http:// www ...接下来,我们编写默认名称空间,它将被写入先前开始的元素(顺便说一句,为了对此顺序有更深入的了解,请参阅这篇不错的文章 )在第11-14行中,我们使用JAXB生成的模型创建标头,然后让我们的JAXB marshaller直接将其写到我们的StAX输出流。
重要提示: JAXB编组器以片段模式初始化,否则它将开始添加XML声明,这对于独立文档是必需的,当然,在现有文档中间是不允许的:
marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true);
附带说明一下:在此示例中,JAXB集成并不是真正有用,它会增加复杂性并占用更多代码行,然后仅使用XMLStreamWriter添加元素即可。 但是,如果您有一个更复杂的结构需要创建并合并到文档中,则具有自动对象映射非常方便。
因此,我们有启用验证的阅读器。 从我们开始遍历源文档的那一刻起,它将同时验证和解析。 然后,我们的writer已经编写了一个初始化的文档和标头,并准备接受更多数据。 最后,我们必须遍历源代码并将每个部分写入输出文件。 如果输出文件变大,我们将换一个新文件:
while (xmlStreamReader.hasNext()) {xmlStreamReader.next();if (xmlStreamReader.getEventType() == XMLEvent.START_ELEMENT&& xmlStreamReader.getLocalName().equals(DATA_ELEMENT)) {if (dataRepetitions != 0 && dataRepetitions % 2 == 0) { // %2 = just for testing: replace this by for example checking the actual size of the current output filexmlStreamWriter.close(); // Also closes any open Element(s) and the documentxmlStreamWriter = openOutputFileAndWriteHeader(++fileNumber); // Continue with next filedataRepetitions = 0;}// Transform the input stream at current position to the output streamtransformer.transform(new StAXSource(xmlStreamReader), new StAXResult(new FragmentXMLStreamWriterWrapper(new AvoidDefaultNsPrefixStreamWriterWrapper(xmlStreamWriter, DOCUMENT_NS))));dataRepetitions++;}
}
重要的一点是,我们不断迭代源文档,并检查是否存在Data元素的开头。 如果是这样,我们将相应的元素及其同级元素流式传输到输出。 在我们的简单示例中,我们没有兄弟姐妹,只有文本值。 但是,如果结构更复杂,则所有基础节点将自动复制到输出中。 每隔两个数据元素,我们将循环输出文件。 关闭编写器,并初始化一个新的编写器(当然,可以通过检查文件大小而不是%2来代替此检查)。 如果作家是关闭的,它将自动处理关闭打开的元素并最终关闭文档本身,而无需您自己这样做。 作为将节点从输入流传输到输出的机制,需要注意以下几点:
- 由于验证,我们不得不使用游标API,因此必须使用XSLT将节点及其兄弟节点传输到输出。 XSLT具有一些默认模板,如果您未专门指定XSL,则将调用这些模板。 在这种情况下,它将输入转换为给定的输出。
- 需要一个自定义的FragmentXMLStreamWriterWrapper ,我在JavaDoc中对此进行了记录。 再次将这个包装器包装在PreventDefaultNsPrefixStreamWriterWrapper中 。 最后一个原因是默认的XSLT模板无法识别源文档中的默认名称空间。 一分钟内提供更多信息(或搜索避免使用DefaultDefaultNsPrefixStreamWriterWrapper)。
- 您使用的转换器必须是Oracle JDK的内部版本。 在初始化转换器的地方,我们直接引用内部TransformerFactory的实例: com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl然后创建正确的转换器: Transformer = new TransformerFactoryImpl()。newTransformer(); 通常,您将使用TransformerFactory.newInstance()并使用classpath上可用的转换器。 但是,解析器和转换器可以通过提供META-INF /服务来安装自己。 如果另一个转换器(例如默认的Xalan,而不是重新打包的JDK版本)将在类路径上,则转换将失败。 原因是显然只有JDK内部版本才可以从StAXSource转换为StAXResult
- 转换器实际上将让我们的XMLStreamReader在迭代过程中继续。 因此,在处理完一个数据元素之后,理论上阅读器的光标将在下一个数据元素处就绪。 从理论上讲,如果格式化XML,则下一个事件类型可能是空格。 因此,在下一个Data元素实际准备就绪之前,它仍可能需要在while循环中对xmlStreamReader.next()进行一些迭代。
结果是我们有3个输出文件,每个输出文件都符合原始架构,每个文件都有2个数据元素:
要将大约30GB的XML(我在说我的原始工作分配XML具有更复杂的结构,而不是此处使用的演示XSD)拆分为大约500MB的部分,并花费了大约25分钟的时间。 为了测试内存使用率,我特意将Xmx设置为32MB。 从图中可以看出,内存消耗非常低,并且没有GV开销: 生活是美好的,但并非完全如此。 在那儿,我发现有些尴尬的事情需要小心。
在我的实际场景中,输入XML没有与之关联的名称空间,我很确定它永远不会。 这就是我坚持使用此解决方案的原因。 在演示中,这里只有一个名称空间,并且已经开始使设置更加脆弱。 问题不在于StAX:使用StAX处理名称空间非常简单。 您可以决定具有一个与该模式的目标名称空间相对应的默认名称空间(假设您的模式为elementFormDefault = qualified),并可以为该模式中导入的其他名称空间声明一些带前缀的名称空间。 当XSLT开始干扰输出流时,问题就开始出现(您可能已经注意到了)。 显然,它不会检查已经定义了哪些名称空间或发生其他事情。
结果是,它们通过使用其他前缀重新定义现有名称空间或重置默认名称空间和其他不需要的内容,使文档严重混乱。 如果您需要比默认模板更多的名称空间操作,则可能需要XSL。 如果输入文档使用默认名称空间,则XSLT也会触发异常。 它将尝试注册名称为“ xmlns”的前缀。 不允许这样做,因为xmlns保留用于指示默认名称空间,不能用作前缀。 我为此测试申请的解决方案是忽略任何前缀“ xmlns”,并忽略与xmlns前缀组合的目标名称空间的添加(这就是为什么要使用避免DefaultNsPrefixStreamWriterWrapper)。 前缀和名称空间都需要在PreventDefaultNsPrefixStreamWriterWrapper中进行匹配,因为如果您要使用的输入文档中没有默认名称空间,而是带有前缀(例如<bigxml:BigXmlTest xmlns:bigxml =“ http://…。”> <bigxml:Header …。),那么您就不能忽略添加名称空间(该组合将成为带有“ bigxml”前缀的目标名称空间),因为这只会产生数据元素的前缀而没有名称空间绑定,例如:
<?xml version='1.0' encoding='UTF-8'?>
<BigXmlTest xmlns="http://www.error.be/bigxmltest"><Header><SomeHeaderElement>Something something darkside</SomeHeaderElement></Header><Content><bigxml:Data>Data1</bigxml:Data><bigxml:Data>Data2</bigxml:Data></Content>
</BigXmlTest>
请记住,XML的生产者可以自由选择(还是在elementFormDefault =合格的情况下)选择使用默认命名空间还是为每个元素添加前缀。 该代码应该透明地能够处理这两种情况。 为方便起见,请使用PreventDefaultNsPrefixStreamWriterWrapper代码:
public class AvoidDefaultNsPrefixStreamWriterWrapper extends XMLStreamWriterAdapter {
...@Overridepublic void writeNamespace(String prefix, String namespaceURI) throws XMLStreamException {if (defaultNs.equals(namespaceURI) && "xmlns".equals(prefix)) {return;}super.writeNamespace(prefix, namespaceURI);}@Overridepublic void setPrefix(String prefix, String uri) throws XMLStreamException {if (prefix.equals("xmlns")) {return;}super.setPrefix(prefix, uri);}
最后,我还写了一个版本(点击
此处完全适用于GitHub),但这次使用的是StAX迭代器API。 您会注意到,不再需要繁琐的XSLT来流传输到输出。 只需将每个感兴趣的事件添加到输出中即可。 通过首先使用游标API验证输入,然后使用Iterator API解析输入,可以解决缺少验证的问题。 这将花费更长的时间,但是在大多数情况下仍然可以接受。 最重要的是:
while (xmlEventReader.hasNext()) {XMLEvent event = xmlEventReader.nextEvent();if (event.isStartElement() && event.asStartElement().getName().getLocalPart().equals(CONTENT_ELEMENT)) {event = xmlEventReader.nextEvent();while (!(event.isEndElement() && event.asEndElement().getName().getLocalPart().equals(CONTENT_ELEMENT))) {if (dataRepetitions != 0 && event.isStartElement()&& event.asStartElement().getName().getLocalPart().equals(DATA_ELEMENT)&& dataRepetitions % 2 == 0) { // %2 = just for testing: replace this by for example checking the actual size of the current// output filexmlEventWriter.close(); // Also closes any open Element(s) and the documentxmlEventWriter = openOutputFileAndWriteHeader(++fileNumber); // Continue with next filedataRepetitions = 0;}// Write the current event to outputxmlEventWriter.add(event);event = xmlEventReader.nextEvent();if (event.isEndElement() && event.asEndElement().getName().getLocalPart().equals(DATA_ELEMENT)) {dataRepetitions++;}}}}
在第2行,您将看到返回XMLEvent,其中包含有关当前节点的所有信息。 在第4行上,您看到使用此表单检查元素类型更容易(与其与常量进行比较,还可以使用对象模型)。 在第19行,要将元素从输入复制到输出,我们只需将Event添加到XMLEventWriter。
翻译自: https://www.javacodegeeks.com/2013/08/splitting-large-xml-files-in-java.html