用Java分割大型XML文件

上周,我被要求用Java编写一些东西,该东西能够将单个30GB XML文件拆分为可配置文件大小的较小部分。 该文件的使用者将是一个中间件应用程序,该应用程序存在XML较大的问题。 在后台,它使用某种DOM解析技术,使它在一段时间后耗尽内存。 由于它是基于供应商的中间件,因此我们无法自行纠正。 最好的选择是创建一些预处理工具,该工具会先将大文件分成多个较小的块,然后再由中间件处理。

XML文件带有一个相应的W3C模式,该模式由强制性头部分和紧随其后嵌套有多个0 .. *数据元素的内容元素组成。 对于演示代码,我以简化形式重新创建了架构:
图式 模式(1) 标头的大小可以忽略。 单个数据元素的重复也很小,可以说少于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开销: bigxmltest-vm 生活是美好的,但并非完全如此。 在那儿,我发现有些尴尬的事情需要小心。

在我的实际场景中,输入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。

参考:来自Koen Serneels –技术博客博客的JCG合作伙伴 Koen Serneels 分离Java中的大型XML文件 。

翻译自: https://www.javacodegeeks.com/2013/08/splitting-large-xml-files-in-java.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/367530.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

信号与线性系统翻转课堂笔记9——傅里叶变换概念

信号与线性系统翻转课堂笔记9——傅里叶变换 The Flipped Classroom9 of Signals and Linear Systems 对应教材&#xff1a;《信号与线性系统分析&#xff08;第五版&#xff09;》高等教育出版社&#xff0c;吴大正著 一、要点 &#xff08;1&#xff0c;重点&#xff09;…

from 下拉框多个值提交_Git commit 多行信息提交

git commit可接受多个消息标志(-m)来允许多行提交原文地址&#xff1a;https://www.stefanjudis.com/today-i-learned/git-commit-accepts-several-message-flags-m-to-allow-multiline-commits/原文作者&#xff1a;Stephan Schneider在命令行上使用git时&#xff0c;您可能已…

处理缓慢的资源泄漏

使用Java监视器查找资源泄漏 查找缓慢的资源泄漏是使应用程序服务器长时间保持正常运行的关键。 在这里&#xff0c;我解释了如何使用Java监视器来发现缓慢的资源泄漏&#xff0c;以及如何验证它们是实际的泄漏&#xff0c;而不仅仅是额外的预分配到某些HTTP连接器或数据库池中…

jquery简单实现点击弹出层效果实例

先看效果图&#xff1a;完整例子&#xff1a; <!-- 渐变弹出层 --><div id"race"><a href"#">点击</a></div><div id"racePop" class"raceShow">这里是弹出层效果</div> <script type&q…

Openfire源码阅读(一)

本篇先分析openfire源码的主要流程&#xff0c;模块细节后续再继续分析&#xff1b; 一、简介&#xff1a; Openfire是开源的实时协作服务器&#xff08;RTC&#xff09;&#xff0c;它是基于公开协议XMPP&#xff08;RFC-3920&#xff09;&#xff0c;并在此基础上实现了XMPP-…

php 查询方法all,获取多条:all静态方法

查询多条数据&#xff1a;all( )方法all方法与前节课学习的get方法都是静态方法&#xff0c;可用模型类直接访问2. 源码&#xff1a;/*** 查找所有记录* access public* param mixed $data 主键列表或者查询条件(闭包)* param array|string $with 关联预查询* param b…

[译文]过犹不及,别再在编程中高射炮打蚊子

原文链接&#xff1a;Anyway,stop recommending bazookas to kill flies in programming. 众成翻译地址&#xff1a;过犹不及&#xff0c;别再在编程中高射炮打蚊子 译者注&#xff1a;翻译这篇吐槽的文章&#xff0c;主要是为了自省~日常工作中确实会犯类似的错误&#xff0…

Java中的for循环

上一章呢我们学习了一下java中的while循环和do while循环 现在我们来了解一下另外一种循环 for循环 for循环是编程语言中一种开界的循环语句&#xff0c;而循环语句 由循环体及循环的终止条件两部分组成&#xff0c;for循环其在各种编程语言中的实现与表达有所出入&#xff0…

SpringFox swagger2 and SpringFox swagger2 UI 接口文档生成与查看

依赖&#xff1a; <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 --> <dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version> <…

matlab期末复习资料,MATLAB期末复习习题及答案

MATLAB期末复习习题及答案13&#xff0c; ysin(x)&#xff0c;x从0到2 &#xff0c; x0.02 &#xff0c;求y的最大值、最小值、均值和标准差。(应用max,min,mean,std) 14&#xff0c; 参照课件中例题的方法&#xff0c;计算表达式z 10x3 y5e xcontour, hold on, quiver)15&…

多核可扩展计数器

到处都需要计数器&#xff0c;例如&#xff0c;查找应用程序的关键KPI&#xff0c;应用程序的负载&#xff0c;服务的请求总数&#xff0c;用于查找应用程序吞吐量的一些KPI等。 由于所有这些需求&#xff0c;并发复杂性也增加了&#xff0c;这使这个问题变得有趣。 如何实现…

三年前端,面试思考(二)

为什么还有&#xff08;二&#xff09; 没有想到上一篇 《三年前端&#xff0c;面试思考》 有这么多前端同学看到。 在评论区也有很多鼓励和质疑的声音&#xff0c;而且群里面交流的同学两天就达到了700人。 群里有同学问了很多问题&#xff0c;同时希望我再分享一些面试技巧…

51单片机auxr寄存器_MCS-51单片机有几个工作寄存器

工作寄存器有4组&#xff0c;每组都是8个工作寄存器R0~R7&#xff0c;通过PSW中的RS1、RS0两位来选择使用哪一组&#xff0c;如果不选&#xff0c;默认是选择第0组。RS1RS0组合为00时&#xff0c;选中第0组工作寄存器&#xff0c;R0~R7地址为00H~07H;RS1RS0组合为01时&#xff…

matlab中quat2angle,RPY_Euler_Quaternion_AngleAxis角度转化:Matlab、Python、Halc

RPY_Euler_Quaternion_AngleAxis角度转化&#xff1a;Matlab、Python、HalcRPY_Euler_Quaternion_AngleAxis角度转化&#xff1a;Matlab、Python、Halcon版本UR协作机器人和Franka机器人导出的位姿为angleVector&#xff0c;三个量表示&#xff0c;在Matlab中angleVector是四个…

基本注射/资格赛,范围

这是上周解决的DI / CDI基础知识的延续-在本文中&#xff0c;我将讨论基础注入&#xff0c;限定词和范围。 在上一个主题中&#xff0c;我们提供了有关DI / CDI概念的大量信息&#xff0c;我们还讨论了如何使用注释加载这些bean或类-这构成了对象的组成并创建了关于如何进行采…

100*100的 canvas 占多少内存?

题目 100*100的 canvas 占多少内存&#xff1f; 在 三年前端&#xff0c;面试思考 中提到了一个题目&#xff0c;非常有新意&#xff0c;这里分享一下当时面试的思考过程。 解题思路 其实真正的答案是多少我并不清楚&#xff0c;面试过程中面试官也不期待一个准确的答案&am…

1t硬盘怎么分区最好_这下尴尬了,电脑硬盘分区常见误区,移动硬盘分区方法...

大家买了新电脑硬盘要不要分区呢&#xff1f;像以往咱们买了新电脑一般会分4个区&#xff0c;C、D、E、F&#xff0c;方便更合理的分类使用&#xff0c;比如把工作放为D盘&#xff0c;娱乐影音放为E盘&#xff0c;游戏放为F盘&#xff0c;C盘为系统盘。不过渐渐地发现&#xff…

用Spring长轮询Tomcat

就像喜剧演员弗兰基 豪威尔 &#xff08; Frankie Howerd&#xff09;所说的“哦&#xff0c;小姐小姐” &#xff0c;但足够多的英国影射和双重诱惑&#xff0c;因为长轮询雄猫对隔壁的闷气不是某种性偏见&#xff0c;这是一种技术&#xff08;或更像是一种骇客&#xff09;由…

exchange 删除邮件

一 批量删除特定主题的邮件1.1 批量删除所有数据库中特定主题的邮件1) 群发了几封主题为“backup”的邮件&#xff1b; 2) 当前操作账号需要满足如下需求&#xff1a; a)该账号需属于Exchange Server 管理员角色以及源服务器和目标服务器的本地 Administrator组&#xff1b; b)…

js点击取消按钮关闭当前弹框_UI设计中“取消按钮”的分析详解

按钮&#xff0c;无论是在 Web 还是 App 上都被广泛地使用&#xff0c;而很少有设计师会注意到按钮当中的细节&#xff0c;导致在设计过程中出现一些低级的错误&#xff0c;使得用户在完成任务的过程中产生阻碍&#xff0c;无法顺利达成目的。在许多优秀的产品中&#xff0c;关…