这几天在重写Luit(一个java web框架)的配置解析部分,和servlet一样,luit支持且仅支持XML配置文件。为什么选择XML,而不是JSON,甚至是自己定义一种格式?理由如下:
- 我懒
- 大家都用xml
- xml的伸缩性满足要求
在正常情况下,我会按照如下方式解析配置:
- 格式检查 -> 元素合法性检查 -> 解析DOM -> 必要值的提取
由于现在采用的是XML这种通用文件格式,格式检查由XML类库(如dom4j)完成,元素合法性被延后到值提取的阶段完成。试想如下场景:父元素必须包含某一子元素,但开发者却未添加。我目前通过抛出异常,然后记录到日志中解决。
但是显而易见,这种方式并不理想。将错误提示尽可能提前是框架开发者的责任,比如让开发者在编写配置文件的时候就发现问题?我敢说,这是个迫切的需求。
说来也惭愧,之前对于XML的认知就仅仅是标签语言,知其然,但不知其所以然。但在日常使用中,我发现IDE是能够识别web.xml的定义的,那么这里一定有某种东西定义了整个文件。
它就是DTD,全称Document Type Definition,可以看做是xml文件的模板,通俗来讲,就是制定规则的。虽说wiki中有描述“DTD限制较多,使用较不便”,但用在目前的这种需求下,它够用了。
打开web.xml,我们能看到这样一行:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
这就是DTD文件,只不过它是外连的罢了,我们也可以将其内置在xml中,但我想应该没有多少人愿意这么做。
接下来我们来介绍下如何定义dtd,应该花不了各位多少时间。
0x00 XML组成
- 元素: XML的主要构建模块,如HTML中的html,head等
- 属性: 提供有关元素的额外信息,如script元素中的src
- 实体: 定义普通文本的变量,如
- PCDATA: 会被解析的字符数据
- CDATA: 不会被解析的字符数据
0x01 DTD声明语法
元素声明
<!ELEMENT 元素名称 类别>
<!ELEMENT 元素名称 (子元素1, 子元素2, ...)>
针对第一种语法,有两种既定的类型:
- EMPTY
- ANY
使用EMPTY,我们可以定义如下类型的元素
<br />
使用ANY,你可以在元素中嵌入任何内容,没有任何约束。
针对第二种语法,我们用一个例子来说明:
<book>
<name>xxx</name>
<price>20</price>
</book>
我们可以按照如下方式制定类型声明:
首先是book元素,它包括name和price两个子元素
<!ELEMENT book (name, price)>
然后递归到子元素,两个子元素均只包含PCDATA
<!ELEMENT name (#PCDATA)>
<!ELEMENT price (#PCDATA)>
补充一点内容,在(子元素, ...)中我们可以在子元素或者括号后使用类似正则表达式的符号:
? | 表示0或1个 |
* | 表示大于等于0个 |
+ | 表示大于等于1个 |
| | 表示或的关系 |
默认情况下,每个定义在括号内的子元素出现且只出现一次。我们可以用这几个符号修饰子元素或者括号,后者等同于修饰括号中的所有子元素。
用一个例子来说明下:
<!ELEMENT book (name | price)>
上述元素声明表示,book中非name既price,且子元素出现0或1次。
属性声明
<!ATTLIST 元素名称 属性名称 属性值类型 属性默认值>
相对而言,属性声明比较复杂,主要是属性值类型有点多,我们来一一说明。
属性值:
CDATA | 不被解析地字符数据 |
(值1|值2|...) | 值为列表中的其中一个 |
ID | 值为唯一的id |
IDREF | 值为另一个元素的id |
IDREFS | 值为其它id的列表(空格分隔) |
NMTOKEN | 值为合法的标记名称 |
IDREFS | 值为合法的标记名称的列表(空格分隔) |
IDREF | 值为另一个元素的id |
ENTITY | 值为一个实体 |
ENTITIES | 值为实体列表(空格分隔) |
NOTATION | 值为符号的名称 |
xml: | 值为一个预定义的xml值 |
默认值可以使用如下参数:
#REQUIRED | 属性值是必须的 |
#IMPLIED | 属性值不是必须的 |
#FIXED value | 属性值固定为value |
假如我们有如下xml文本:
<books>
<book name="book1" price="20" author="CodeSun" payment="cash" />
<book name="book2" author="CodeSun" payment="check" />
</books>
现有如下属性声明:
<!ATTLIST book
name CDATA #REQUIRED
price CDATA "free"
author CDATA #FIXED "CodeSun"
payment (cash|check) #REQUIERD
>
上述规则表明,必须指定name属性,price默认为free(如果未指定),author固定为CodeSun,payment只能非cache既check。这里给出的只是最基本的例子,属性列表可以写得很复杂,只不过最近脑力不足以想出更加复杂的例子了。
实体声明
<!ENTITY 实体名称 实体内容>
这个比较简单,相当于变量,还是用一个例子来说明,首先我们有如下实体声明:
<!ENTITY author "CodeSun">
那么我们可以按照如下方式使用这个实体:
<blog>
<name>dtd</name>
<author>&author;</author>
</blog>
注意,每次使用实体的时候,都必须在其前方加上‘&’,后方加上‘;’,以表示这是个实体,需要解析。
0x02 DTD的使用方式
大家一定认为这段应该放到最前面,我放在这里只不过为了督促大家看完本文。。。
添加DTD规则的方式为使用DOCTYPE,语法如下:
<!-- 下述语法为内嵌声明 -->
<!DOCTYPE 根元素 [声明内容]>
<!-- 下述语法为外连声明 -->
<!DOCTYPE 根元素 SYSTEM "url">
等等,你们是不是要问为何web.xml中使用外连dtd的时候用的是PUBLIC?
是这样的,外部DTD可以分为私有DTD和共有DTD,其中私有DTD使用SYSTEM,而公有DTD使用的则是PUBLIC,并且使用如下语法:
<!DOCTYPE 根元素 PUBLIC DTD名称 "url">
其中DTD名称格式为"注册//组织//类型 标签//语言",这里的“注册”是指组织是否由ISO注册,是则填写'+',否则填写'-'。好了那就让我们来看最上面的那段DTD名称:
- -//Sun Microsystems, Inc.//DTD Web Application 2.3//EN
Sun Microsystems, Inc.是制定此DTD的组织,不是由ISO注册的,类型为DTD,标签为Web Application 2.3,语言为英文。
最后,问题就来了,SYSTEM就够我用的了,为何要整得这么高大上呢?至少目前不需要。