Properties、XML 与日志:配置如何读取,问题如何定位
明确配置编码、层级解析与日志接口的职责。
继续上次小节 →Java 进阶线 · 线路目录
Java 进阶线
对象与类型设计3 站
文件、配置与并发4 站
网络、框架与复习3 站
本页导读
day11|特殊文件:XML、Properties 与日志框架
day08–day10 一直在处理”普通文件”的搬运(字节、字符、缓冲、序列化),本日换了一类目标:有格式的特殊文件。Properties 属性文件负责存”键值对式配置”,XML 负责存”有层次、有关系的复杂数据”,日志技术则解决”程序跑起来以后,怎么把过程记下来、怎么定位 bug”。学完后应能回答:属性文件的三个特点是什么、Properties 为什么说”是 Map 但一般不当 Map 用”、load() 和 store() 各配哪一路 IO、XML 的语法红线有哪些(文档声明在第一行、根标签唯一、正确嵌套)、特殊字符和 CDATA 分别解决什么问题、Dom4j 的”文档对象模型”是怎么一层层往下取数据的、DTD 和 Schema 的区别、为什么要用日志框架而不是 System.out.println、Logback 至少要引哪三个模块、logback.xml 为什么必须放 src 下、日志级别怎么控制输出。
本日 PPT 目录(第 7 页,第 13/22/32/37/42/46/50 页重复出现作为进度锚点):特殊文件:Properties 属性文件 → 特殊文件:XML 文件(概述 / 读取 XML 文件中的数据 / 补充知识:约束 XML 文件的编写[了解])→ 日志技术(概述 / 日志技术体系、Logback 日志框架的概述 / Logback 快速入门 / Logback 设置日志级别)。
四点说明(供复习时核对):
- 本日共 54 页幻灯片。抽取文本里多处”方法/API”页只剩
[表格]占位、没有表体(第 10、11、26、28、41、51 页),本文按课件的题目与配套示例工程还原,还原处标注”(表格按课件题目与配套示例整理)”。 - PPT 抽出的 39 张图片已逐张查看:其中约 29 张是装饰性大字标题图(“日志技术""项目""什么是日志?""什么是日志级别?""为什么要学习日志级别?""不建议用 dom4j,然后用 IO 流写出去!“等)、卡通人物与表情包(“肿么办啊""不合适""好期待,好期待""OK”女生、黑色手指图标)、以及手机 App 截图(淘宝、顺丰),与知识点无关,按规范舍弃。有信息量的 10 张已压缩归档内嵌。
- 代码取自配套示例工程,包名为
com.itheima.d1_properties/d2_xml/d3_logback;PPT 上的代码是拆散在多个文本框里的截图,本文按语义拼回并统一 4 空格缩进。 - 课件第 47 页把创建 Logger 写成
LoggerFactory.getLogger("类名");配套代码里写的是LoggerFactory.getLogger("Test.class")(传的是字符串字面量)。两种写法都能拿到 Logger 对象,实际开发中更常见的是传Class对象,即LoggerFactory.getLogger(Test.class)。
1. 为什么要用特殊文件
课件第 2 页并排列出三个文件:普通文件.txt、属性文件.properties、XML文件.xml;第 3 页给出判断:普通 .txt 是 NO!,属性文件和 XML 文件是 Yes!!!。原因在截图里看得很清楚。
普通文件.txt 里的内容是”格式随意”的:
普通的属性文件、里面的内容可以随便写,格式随意!
以下是一些用户的用户名和密码
admin=12343232zhangsan=23232liergou=xiaohua
wangmazi=23223quandaner=123
程序要读这段数据,就得自己写正则去”猜”哪里是用户名、哪里是密码——因为文件本身没有告诉程序”格式”是什么。第 4 页把需求换成”存储多个用户的:用户名、密码、家乡、性别”,问题更明显:用 .properties 只能写成一堆靠编号区分的同名键(name1、password2、address2、sex3),用 .xml 就可以靠嵌套标签把”一个用户”这个整体表达清楚。
课件给特殊文件总结的用途只有两条(第 4 页):
- 存储有关系的数据,作为系统的配置文件
- 作为信息进行传输
第 5 页说明本专题的学习目标,也是后面三章的主线:了解它们的特点和作用 → 学习使用程序读取它们里面的数据 → 学习使用程序把数据存储到这些文件里。
2. Properties 属性文件
2.1 属性文件的三个特点
课件第 8 页把属性文件的规则压成三条:
- 都只能是键值对;
- 键不能重复;
- 文件后缀一般是
.properties结尾。
具体长这样(第 2 页截图):
# 这是注释,以下都是一些用户名和密码信息
# 属性文件的内容都是一些键值对信息,每行都是一个键值对
# 属性文件的后缀:一般都是 .properties 结尾的
admin=123456
liergouzi=小花
zhangsan=666888
lisi=iloveu
全蛋儿=1314520
# 开头是注释行,键=值 是一行数据。
2.2 Properties 是什么:是 Map,但一般不当 Map 用
课件第 9 页把 Properties 放进 Map 家族:
| 层次 | 类型 |
|---|---|
| 接口 | Map |
| 实现类 | HashMap、LinkedHashMap、TreeMap、Properties、… |
课件的原话是:
Properties是一个 Map 集合(键值对集合),但是我们一般不会当集合使用。 核心作用:Properties是用来代表属性文件的,通过Properties可以读写属性文件里的内容。
记住这层关系:因为它是 Map,所以 put/get/containsKey/forEach 都能用;但实际写代码时只用它和属性文件打交道的那几个专属方法。
2.3 使用 Properties 读取属性文件里的键值对数据
课件第 10 页的标题是”使用 Properties 读取属性文件里的键值对数据”。第 12 页单独抽出两个关键签名:
- 可以加载属性文件中的数据到
Properties对象中来:void load(Reader reader) - 可以存储
Properties属性集的键值对数据到属性文件中去:void store(Writer writer, String comments)
读取相关的方法(表格按课件题目与配套示例整理):
| 方法 | 说明 |
|---|---|
Properties() | 创建一个空的属性集对象,代表一个属性文件 |
void load(Reader reader) | 加载属性文件(字符流)中的数据到属性集对象中 |
void load(InputStream inStream) | 加载属性文件(字节流)中的数据 |
String getProperty(String key) | 根据键取值(不要用 Map 的 get() 去取,见示例注释) |
Set<String> stringPropertyNames() | 拿到属性文件里的全部键 |
boolean containsKey(Object key) | 判断是否存在某个键(继承自 Map/Hashtable) |
配套示例 PropertiesDemo1:
package com.itheima.d1_properties;
import java.io.FileInputStream;
import java.util.Properties;
import java.util.Set;
public class PropertiesDemo1 {
public static void main(String[] args) throws Exception {
// 目标:Properties 读取属性文件中的键值对数据
// 1、创建属性集对象,代表一个属性文件
Properties properties = new Properties();
System.out.println(properties);
// 2、加载属性文件信息到属性集对象中去
properties.load(new FileInputStream("day11-special-file-log-code\\src\\users.properties"));
System.out.println(properties);
// 3、根据键取值
// System.out.println(properties.get("赵敏")); // 不推荐
System.out.println(properties.getProperty("赵敏"));
// 4、遍历方式一:先拿全部键,再逐个取值
Set<String> keys = properties.stringPropertyNames();
for (String key : keys) {
String value = properties.getProperty(key);
System.out.println(key + "==>" + value);
}
// 5、遍历方式二:Map 的 forEach
properties.forEach((k, v) -> System.out.println(k + "===>" + v));
}
}
对应的 users.properties 里存的是用户信息:
# 下面是一个属性文件的各种信息,存储的是用户名和姓名
admin=123456
赵敏=wuji
张无忌=zhaomin
殷素素=cuishan
灭绝=zhouzhiruo
课件强调的一点藏在示例注释里:取值要用 getProperty(String),而不是 Map 的 get(Object)。
2.4 使用 Properties 把键值对数据写出到属性文件里去
课件第 11 页的标题是”使用 Properties 把键值对数据写出到属性文件里去”。写出相关的方法(表格按课件题目与配套示例整理):
| 方法 | 说明 |
|---|---|
Object setProperty(String key, String value) | 往属性集里放入一个键值对(继承自 Hashtable,等价于 put) |
void store(Writer writer, String comments) | 把属性集的键值对写回属性文件,comments 是写进文件头部的注释 |
配套示例 PropertiesDemo2:
package com.itheima.d1_properties;
import java.io.FileWriter;
import java.util.Properties;
public class PropertiesDemo2 {
public static void main(String[] args) throws Exception {
// 目标:掌握使用 Properties 把键值对数据写出去到属性文件中去
// 1、创建属性集对象
Properties properties = new Properties();
properties.setProperty("admin", "66666");
properties.setProperty("玄冥二老", "wangfei");
properties.setProperty("金毛狮王", "成昆");
properties.setProperty("小昭", "wuji");
System.out.println(properties);
// 2、存储到文件
properties.store(new FileWriter("day11-special-file-log-code/src/users2.properties"),
"i saved many users! give me 100!");
}
}
写出的 users2.properties 长这样:
#i saved many users! give me 100!
#Fri Mar 29 09:33:09 SGT 2024
admin=66666
小昭=wuji
玄冥二老=wangfei
金毛狮王=成昆
两个细节值得注意:
store()会自动在被写出的文件头部加一行#当前时间的注释(第二行的时间戳不是我们写的),第一行才是我们自己传的comments。- 课件示例里中文正常显示,但真实环境用
store(Writer)写中文时,Properties.store()默认会把非 Latin-1 字符转义成\uXXXX的形式(Properties规范只支持 ISO-8859-1 字符集)。(补充说明)配套工程里的users.properties就是这种被转义过的形式:# \u4E0B\u9762\u662F...、\u8D75\u654F=wuji。读回来时load()会自动还原,不影响使用。
2.5 属性文件改写的完整套路
课件第 12 页的提问是”Properties 的作用?具体如何使用?“配套示例 PropertiesTest3 演示了”读 → 改 → 存回同一文件”的闭环,这也是属性文件最常见的用法:
package com.itheima.d1_properties;
import java.io.FileReader;
import java.io.FileWriter;
import java.util.Properties;
public class PropertiesTest3 {
public static void main(String[] args) throws Exception {
// 1、创建 Properties 对象
Properties properties = new Properties();
// 2、加载属性文件中的键值对数据
properties.load(new FileReader("day11-special-file-log-code\\src\\people.txt"));
// 3、判断是否存在李芳,存在就把其值改成 18
if (properties.containsKey("李芳")) {
properties.setProperty("李芳", "18");
}
// 4、把属性文件对象,重新存入到属性文件中去
properties.store(new FileWriter("day11-special-file-log-code\\src\\people.txt"),
"give me 100 fen!");
}
}
load() 这一侧同样有重载:配套的 PropertiesDemo1 传的是 FileInputStream(字节流),PropertiesTest3 传的是 FileReader(字符流),两者都能加载属性文件——说明属性文件这一层已经和 day09/day10 的 IO 流体系对接上了(字节流适合任何文件,字符流适合文本文件)。
3. XML 文件
3.1 概述
课件第 14 页给出的定义:
- XML(全称 EXtensible Markup Language,可扩展标记语言)本质是一种数据的格式,可以用来存储复杂的数据结构,和数据关系。
3.2 XML 的特点
课件第 15 页列了五条:
- XML 中的
<标签名>称为一个标签或一个元素,一般是成对出现的。 - XML 中的标签名可以自己定义(可扩展),但必须要正确的嵌套。
- XML 中只能有一个根标签。
- XML 中的标签可以有属性。
- 如果一个文件中放置的是 XML 格式的数据,这个文件就是 XML 文件,后缀一般要写成
.xml。
3.3 XML 的创建
课件第 16 页:“就是创建一个 XML 类型的文件,要求文件的后缀必须使用 xml,如 hello_world.xml。“在 IDEA 里就是对着 src 右键 → New → File,然后在 New File 对话框里输入 hello_world.xml。
3.4 XML 的语法规则
课件第 17 页把规则集中在一页:
- XML 文件的后缀名为
xml,文档声明必须是第一行:
<?xml version="1.0" encoding="UTF-8" ?>
其中 version:XML 默认的版本号码,该属性必须存在;encoding:本 XML 文件的编码。
- XML 中可以定义注释信息:
<!-- 注释内容 --> - XML 中书写
<、&等字符可能会出现冲突导致报错,此时可用如下特殊字符替代:
| 实体 | 代表的字符 | 含义 |
|---|---|---|
< | < | 小于 |
> | > | 大于 |
& | & | 和号 |
' | ' | 单引号 |
" | " | 引号 |
- XML 中还可以写一个叫 CDATA 数据区的东西:
<![CDATA[ …内容… ]]>,里面的内容可以随便写(不再需要对<、&做转义)。
配套示例 demo1-helloworld.xml 把这几条规则全用上了:
<?xml version="1.0" encoding="UTF-8" ?>
<!-- 以下是一个新的xml的内容 -->
<users>
<user id="1">
<name>张全蛋</name>
<sex>男</sex>
<age>32</age>
<hobby>炸机</hobby>
<sql>
select * from tb_student where age >= 18 && age <= 35
</sql>
</user>
<user id="2" desc="这是一个脱口秀演员">
<name>何广智</name>
<sex>男</sex>
<age>28</age>
<hobby>脱口秀</hobby>
<sql>
<![CDATA[
select * from tb_student where age >= 18 && age <= 35
]]>
</sql>
</user>
</users>
同一个 SQL 片段,第一个 user 里用实体写,第二个 user 里直接塞进 CDATA 区——两种合法写法。
3.5 XML 的作用与应用场景
课件第 18、19 页重复同一句话:本质是一种数据格式,可以存储复杂的数据结构,和数据关系;应用场景是”经常用来作为系统的配置文件;或者作为一种特殊的数据结构,在网络中进行传输”。
第 19 页给的传输示例很直观——一条物流/行程信息用 XML 表达:
<?xml version="1.0" encoding="UTF-8"?>
<data>
<sender>张三</sender>
<receiver>李四</receiver>
<src>北京</src>
<current>武汉</current>
<dest>广州</dest>
</data>
3.6 小结(第 20、21 页)
- XML 是什么? 全称 EXtensible Markup Language,是一种可扩展的标记语言。
- XML 的作用? 用于进行存储数据和传输数据;作为软件的配置文件。
- XML 的组成格式要求? 文件后缀一般是
xml,文档声明必须是第一行;必须存在一个根标签,有且只能有一个;可以定义注释信息<!-- 注释内容 -->;标签必须成对出现(<name></name>);必须能够正确嵌套。
4. 使用程序读取 XML 文件中的数据(Dom4j)
4.1 为什么不自己写解析代码
课件第 23 页说得很直接:
注意:程序员并不需要自己写原始的 IO 流代码来解析 XML,难度较大!也相当繁琐! 其实,有很多开源的、好用的解析 XML 的框架,最知名的是 Dom4j(第三方研发的)。
也就是说,读 XML 的正解是”程序 → 解析器(框架)→ XML 文件”:解析器负责把标签、属性、文本都变成 Java 对象,我们只负责从对象里取数。
4.2 使用 Dom4j 的前置步骤(导包)
课件第 24 页给出的步骤:
- 下载 Dom4j 框架,官网下载;
- 在项目中创建一个文件夹:
lib; - 将
dom4j-2.1.3.jar文件复制到lib文件夹; - 在 jar 文件上点右键,选择
Add as Library→ 点击OK; - 在类中导包使用。
配套工程的 类库/ 下放的是 dom4j-2.1.4.jar(版本号略有出入,用法一致)。
4.3 解析思想:文档对象模型
课件第 25 页用一棵”标签树”解释 Dom4j 的思想——XML 的每个部分都对应一个 Java 对象:
| XML 组成 | 对应的 Java 对象 | 说明 |
|---|---|---|
| 整个文档 | Document | 整个文档 |
| 标签 | Element | 元素 |
标签上的 id="1" | Attribute | 属性 |
| 标签里的文字 | 子元素、文本 | 由 Element 提供方法取出 |
| 解析入口 | SAXReader | Dom4j 提供的解析器,可以认为是代表整个 Dom4j 框架 |
课件用的示例数据:
<?xml version="1.0" encoding="UTF-8" ?>
<users>
<user id="1">
<name>张无忌</name>
<sex>男</sex>
<地址>光明顶</地址>
<password>minmin</password>
<data> 3 < 2 && 5 > 4 </data>
<data1>
<![CDATA[
3 < 2 && 5 > 4
]]>
</data1>
</user>
<user id="2">
<name>敏敏</name>
<sex>女</sex>
<地址>光明顶</地址>
<password>wuji</password>
<data> 3 < 2 && 5 > 4 </data>
<data1>
<![CDATA[
3 < 2 && 5 > 4
]]>
</data1>
</user>
</users>
4.4 得到 Document 对象
课件第 26 页的要点(表格按课件题目与配套示例整理):
| 类型 | 方法 | 说明 |
|---|---|---|
SAXReader | SAXReader() | 创建 Dom4j 提供的解析器对象 |
SAXReader | Document read(String url) | 把 XML 文件读成一个 Document 文档对象(还有 read(File)、read(InputStream) 重载) |
Document | Element getRootElement() | 拿到根标签对应的元素对象 |
4.5 Element 提供的方法
课件第 27、28 页连着两页讲 Element 的方法,并配了一段完整代码截图:
Element 常用方法(表格按课件题目与配套示例整理):
| 方法 | 返回值 | 说明 |
|---|---|---|
String getName() | 元素(标签)名 | 如根标签返回 contactList |
List<Element> elements() | 全部子元素 | 一级子元素 |
List<Element> elements(String name) | 指定名字的子元素集合 | 如 elements("contact") |
Element element(String name) | 单个子元素 | 默认拿第一个匹配的子元素 |
Attribute attribute(String name) | 属性对象 | 可再调 getName()、getValue() |
String attributeValue(String name) | 属性值 | 等价于 attribute(name).getValue() |
String elementText(String name) | 子元素的文本值 | 通过父元素直接取 |
String elementTextTrim(String name) | 子元素的文本值 | 去掉前后空格 |
String getText() | 元素自身的文本值 | 先拿到元素对象再取 |
String getTextTrim() | 元素自身的文本值 | 去掉前后空格 |
配套示例 Dom4JTest1 把这些方法串了一遍:
package com.itheima.d2_xml;
import org.dom4j.Attribute;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.util.List;
public class Dom4JTest1 {
public static void main(String[] args) throws Exception {
// 目标:解析 XML 文件:使用 Dom4j 框架
// 1、创建一个 SaxReader 解析器对象
SAXReader saxReader = new SAXReader();
// 2、把 xml 文件读成一个 Document 文档对象
Document document = saxReader.read("day11-special-file-log-code\\src\\Contact.xml");
// 3、文档对象中包含了 XML 的全部数据,提供了方法获取数据
Element rootElement = document.getRootElement();
System.out.println(rootElement.getName());
// 4、提取子元素对象
// List<Element> sonEles = rootElement.elements();
List<Element> sonEles = rootElement.elements("contact");
for (Element sonEle : sonEles) {
System.out.println(sonEle.getName());
}
// 指定获取单个子元素对象
Element userEle = rootElement.element("user");
System.out.println(userEle.elementText("name"));
Element contactEle = rootElement.element("contact"); // 默认拿第一个 contact!
System.out.println(contactEle.elementText("name"));
// 5、提取子元素的属性对象
Attribute idAttr = contactEle.attribute("id");
System.out.println(idAttr.getName());
System.out.println(idAttr.getValue());
// 直接拿属性值
System.out.println(contactEle.attributeValue("id"));
// 6、文本值
// 通过父元素拿到子元素文本值
System.out.println(contactEle.elementText("name"));
System.out.println(contactEle.elementTextTrim("name")); // 去掉前后空格
// 先拿到元素对象,再提取其文本值
Element emailEle = contactEle.element("email");
System.out.println(emailEle.getText());
System.out.println(emailEle.getTextTrim()); // 去掉前后空格
}
}
elementText() 与 elementTextTrim() 的区别,用案例数据一看就明白——Contact.xml 里第一条记录的名字前后带空格:
<?xml version="1.0" encoding="UTF-8"?>
<contactList>
<contact id="1">
<name> 潘金莲 </name>
<gender>女</gender>
<email> panpan@itcast.cn</email>
</contact>
<contact id="2">
<name>武松</name>
<gender>男</gender>
<email>wusong@itcast.cn</email>
</contact>
<contact id="3">
<name>武大狼</name>
<gender>男</gender>
<email>wuda@itcast.cn</email>
</contact>
<user>
<name>dlei</name>
</user>
</contactList>
这里还故意放了两个”坑”:<name> 潘金莲 </name> 用来验证 Trim 系列方法;多出来的 <user> 标签用来验证 elements("contact") 会按名字过滤,而 element("user") 能越过前面的 contact 精确取到。
4.6 解析案例:把联系人封装成 List 集合
课件第 30 页的需求:“利用 Dom4J 框架,将 contacts.xml 文件中的联系人数据解析出来,封装成 List 集合,并遍历输出。”
按课件案例的数据形态,contact 上带 id、vip 属性:
<?xml version="1.0" encoding="UTF-8"?>
<contactList>
<contact id="1" vip="true">
<name>张无忌</name>
<gender>男</gender>
<email>wuji@itcast.cn</email>
</contact>
<contact id="2" vip="false">
<name>小昭</name>
<gender>女</gender>
<email>xiaozhao@itcast.cn</email>
</contact>
<contact id="3" vip="false">
<name>灭绝师太</name>
<gender>女</gender>
<email>miejue@itcast.cn</email>
</contact>
</contactList>
封装用的实体类:
package com.itheima.d2_xml;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Contact {
private int id;
private String name;
private String email;
private char gender;
}
解析并封装的代码(配套示例 Dom4JTest2):
package com.itheima.d2_xml;
import org.dom4j.Document;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.util.ArrayList;
import java.util.List;
public class Dom4JTest2 {
public static void main(String[] args) throws Exception {
// 目标:解析 XML 文件:使用 Dom4j 框架
// 1、创建一个 SaxReader 解析器对象
SAXReader saxReader = new SAXReader();
// 2、把 xml 文件读成一个 Document 文档对象
Document document = saxReader.read("day11-special-file-log-code\\src\\Contact.xml");
// 3、文档对象中包含了 XML 的全部数据,提供了方法获取数据
Element rootElement = document.getRootElement();
// 4、准备一个联系人集合存储联系人对象
List<Contact> contacts = new ArrayList<>();
// 5、提取全部一级联系人元素对象
List<Element> sonEles = rootElement.elements("contact");
// 6、遍历子元素对象
for (Element sonEle : sonEles) {
// 7、每个子元素是一个联系人对象,创建联系人对象,封装数据
Contact contact = new Contact();
contact.setId(Integer.valueOf(sonEle.attributeValue("id")));
contact.setName(sonEle.elementTextTrim("name"));
contact.setGender(sonEle.elementTextTrim("gender").charAt(0));
contact.setEmail(sonEle.elementTextTrim("email"));
// 8、存入到集合中去
contacts.add(contact);
}
System.out.println(contacts);
}
}
4.7 用程序把数据写出到 XML 文件中
课件第 31 页的提问是”如何使用程序把数据写出到 XML 文件中去?“,紧接着的两页大字给出结论:“不建议用 dom4j 做”、“推荐直接把程序里的数据拼接成 XML 格式”,然后(用 IO 流)写出去。
配套示例 Dom4JTest3 就是”拼字符串 + PrintStream”的写法:
package com.itheima.d2_xml;
import java.io.PrintStream;
public class Dom4JTest3 {
public static void main(String[] args) throws Exception {
// 目标:写一个 xml 的数据出去
StringBuilder sb = new StringBuilder();
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n");
sb.append("<user>\r\n");
sb.append("\t<name>").append("张三").append("</name>\r\n");
sb.append("\t<age>").append("12").append("</age>\r\n");
sb.append("\t<sex>").append("男").append("</sex>\r\n");
sb.append("</user>\r\n");
PrintStream ps = new PrintStream("day11-special-file-log-code/src/users3.xml");
ps.println(sb);
ps.close();
}
}
写出的结果:
<?xml version="1.0" encoding="UTF-8"?>
<user>
<name>张三</name>
<age>12</age>
<sex>男</sex>
</user>
5. 补充知识:约束 XML 文件的编写(了解)
5.1 什么是约束
课件第 33 页:“什么是约束 XML 文件的书写?就是限制 XML 文件只能按照某种格式进行书写。”
第 34 页进一步说明:约束文档就是专门用来限制 xml 书写格式的文档,比如限制标签、属性应该怎么写。约束文档分两类:DTD 文档和 Schema 文档。
5.2 DTD 的使用
课件第 35 页的需求:“利用 DTD 约束文档,约束一个 XML 文件的编写。“步骤:
- 编写 DTD 约束文档,后缀必须是
.dtd; - 在需要编写的 XML 文件中导入该 DTD 约束文档;
- 然后 XML 文件就必须按照 DTD 约束文档指定的格式进行编写,否则报错!
DTD 本体写起来很简单(配套 xml/data.dtd):
<!ELEMENT 书架 (书+)>
<!ELEMENT 书 (书名,作者,售价)>
<!ELEMENT 书名 (#PCDATA)>
<!ELEMENT 作者 (#PCDATA)>
<!ELEMENT 售价 (#PCDATA)>
在 XML 里导入的方式是加一行 <!DOCTYPE ... SYSTEM "data.dtd">:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE 书架 SYSTEM "data.dtd">
<书架>
<书>
<书名>从入门到跑路</书名>
<作者>dlei</作者>
<售价>9.9</售价>
</书>
<书>
<书名>从删库到跑路</书名>
<作者>啊猫</作者>
<售价>很便宜</售价>
</书>
</书架>
课件的结论是两句话:DTD 可以约束 XML 文件的编写,但不能约束具体的数据类型。上面 售价 写 9.9 合法,写 很便宜 也合法,DTD 管不了。
5.3 Schema 的使用
课件第 36 页的需求:“利用 schema 文档约束,约束一个 XML 文件的编写。“步骤:
- 编写 schema 约束文档,后缀必须是
.xsd,具体的形式到代码中观看; - 在需要编写的 XML 文件中导入该 schema 约束文档;
- 按照约束内容编写 XML 文件的标签。
配套 xml/data.xsd(带课件注释):
<?xml version="1.0" encoding="UTF-8" ?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.itcast.cn"
elementFormDefault="qualified" >
<!-- targetNamespace:申明约束文档的地址(命名空间) -->
<element name='书架'>
<!-- 写子元素 -->
<complexType>
<!-- maxOccurs='unbounded':书架下的子元素可以有任意多个! -->
<sequence maxOccurs='unbounded'>
<element name='书'>
<complexType>
<sequence>
<element name='书名' type='string'/>
<element name='作者' type='string'/>
<element name='售价' type='double'/>
</sequence>
</complexType>
</element>
</sequence>
</complexType>
</element>
</schema>
导入方式是在根标签上声明命名空间:
<?xml version="1.0" encoding="UTF-8" ?>
<书架 xmlns="http://www.itcast.cn"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.itcast.cn data.xsd">
<!-- xmlns="http://www.itcast.cn" 基本位置
xsi:schemaLocation="http://www.itcast.cn data.xsd" 具体的位置 -->
<书>
<书名>从入门到跑路</书名>
<作者>dlei</作者>
<售价>9.9</售价>
</书>
<书>
<书名>从删库到跑路</书名>
<作者>啊猫</作者>
<售价>19.98</售价>
</书>
</书架>
5.4 DTD 与 Schema 对比
| 对比项 | DTD | Schema |
|---|---|---|
| 文件后缀 | .dtd | .xsd |
| 约束 XML 的编写 | 可以 | 可以 |
| 约束具体的数据类型 | 不能 | 可以(如 type='double'、type='string') |
这一节课件标了[了解],重点是知道”约束是什么、有哪两类”,不必背 DTD 与 XSD 的写法。
6. 日志技术
6.1 什么是日志,为什么需要日志
课件第 38 页的类比是”好比生活中的日记,可以记录你生活中的点点滴滴”:
- 程序中的日志,通常就是一个文件,里面记录的是程序运行过程中的各种信息。
第 41 页给出的正式定义是:用来记录程序运行过程中的信息,并可以进行永久存储。它要回答的三个现实问题(课件第 38 页):
- 希望系统能记住某些数据是被谁操作的,比如被谁删除了?
- 想分析用户浏览系统的具体情况,以便挖掘用户的具体喜好?
- 当系统在开发中或者上线后出现了 bug、崩溃了,该通过什么去分析、定位 bug?
6.2 目前记录日志的方案:输出语句的弊端
课件第 39 页先给了一段”用输出语句记日志”的代码:
public static void test(String number) {
try {
int result = Integer.parseInt(number);
System.out.println("输入的数字为" + result);
} catch (NumberFormatException e) {
System.out.println("输入的数字有误,请输入一个整数");
}
}
然后列出 System.out 的弊端:
| 弊端 | 说明 |
|---|---|
| 位置受限 | 日志会展示在控制台 |
| 去向单一 | 不能更方便地将日志记录到其他的位置(文件、数据库) |
| 无法开关 | 想取消日志,需要修改源代码才可以完成 |
6.3 日志技术应该具备的特点和优势
课件第 40 页正面回答”日志技术应该具备哪些特点和优势”:
- 可以将系统执行的信息,方便地记录到指定的位置(控制台、文件中、数据库中);
- 可以随时以开关的形式控制日志的启停,无需侵入到源代码中去进行修改。
这正是后面要引入”日志框架 + 配置文件”的原因:位置和开关都交给配置,代码只负责”在什么地方该记一条什么级别的日志”。
6.4 日志技术的体系结构
课件第 43 页把日志生态分成两层:
| 分类 | 含义 | 常见代表 |
|---|---|---|
| 日志接口 | 设计日志框架的一套标准,日志框架需要实现这些接口 | Commons Logging(JCL)、Simple Logging Facade for Java(SLF4J) |
| 日志框架 | 牛人或者第三方公司已经做好的实现代码,后来者直接可以拿去使用 | Log4j、JUL(java.util.logging)、Logback 等 |
课件给的两条”注意”很关键:
- 注意 1:因为对 Commons Logging 接口不满意,有人就搞了 SLF4J;因为对 Log4j 的性能不满意,有人就搞了 Logback。
- 注意 2:Logback 是基于 slf4j 的日志规范实现的框架。
6.5 Logback 的模块
课件第 44 页给出官网 https://logback.qos.ch/index.html,并列出模块:
| 模块 | 定位 | 是否必须 |
|---|---|---|
slf4j-api | 日志接口 | 必须有 |
logback-core | 基础模块,是其他两个模块依赖的基础 | 必须有 |
logback-classic | 完整实现了 slf4j API 的模块 | 必须有 |
logback-access | 与 Tomcat 和 Jetty 等 Servlet 容器集成,以提供 HTTP 访问日志的功能 | 可选(以后再接触) |
也就是说,想使用 Logback 日志框架,至少需要在项目中整合 slf4j-api + logback-core + logback-classic 三个模块(配套 类库/logback/ 下就是 slf4j-api-1.7.26.jar、logback-core-1.2.3.jar、logback-classic-1.2.3.jar 加一份 logback.xml)。
6.6 Logback 快速入门
课件第 47 页的需求:使用 Logback 日志框架,记录系统的运行信息。实现步骤(第 47、48 页):
- 在项目下新建文件夹
lib,导入 Logback 的相关 jar 包到该文件夹下,并添加到项目库中去; - 必须将 Logback 的核心配置文件
logback.xml直接拷贝到src目录下(必须是src下); - 在代码中获取日志的对象:
public static final Logger LOGGER = LoggerFactory.getLogger("类名");
- 调用日志对象的方法记录日志信息。
配套示例 Test:
package com.itheima.d3_logback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Test {
// 1、创建一个 Logback 框架的 Logger 日志对象,来记日志
public static final Logger LOGGER = LoggerFactory.getLogger("Test.class");
public static void main(String[] args) {
try {
LOGGER.info("除法开始了。。。。。");
chu(10, 0);
LOGGER.info("除法执行成功了。。。。。");
} catch (Exception e) {
LOGGER.error("除法执行失败了:" + e.getMessage());
}
}
public static void chu(int a, int b) {
LOGGER.debug("参数a:" + a);
LOGGER.debug("参数b:" + b);
int c = a / b;
LOGGER.info("结果c是:" + c);
}
}
注意 LOGGER 声明为 public static final:Logger 对象应当在整个类里共享一个,不要在方法内部反复 getLogger()。
6.7 logback.xml:日志的输出位置、输出格式
课件第 49 页说明:通常可以设置 2 个输出日志的位置——一个控制台、一个系统文件中;logback.xml 就是”对 Logback 日志框架进行控制的”核心配置文件,里面用 <appender> 定义输出位置,用 <root> 把 appender 挂到根日志器上。
| appender 名字 | class | 输出位置 |
|---|---|---|
CONSOLE | ch.qos.logback.core.ConsoleAppender | 控制台 |
FILE | ch.qos.logback.core.rolling.RollingFileAppender | 文件(支持按大小/时间切分) |
配套工程的 src/logback.xml 全文:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--
CONSOLE :表示当前的日志信息是可以输出到控制台的。
-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--输出流对象 默认 System.out 改为 System.err-->
<target>System.out</target>
<encoder>
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度
%msg:日志消息,%n是换行符-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%-5level] %c [%thread] : %msg%n</pattern>
</encoder>
</appender>
<!-- File是输出的方向通向文件的 -->
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<!--日志输出路径-->
<file>D:/log/itheima-data.log</file>
<!--指定日志文件拆分和压缩规则-->
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!--通过指定压缩文件名称,来确定分割文件方式-->
<fileNamePattern>D:/log/itheima-data-%i-%d{yyyy-MM-dd}-.log.gz</fileNamePattern>
<!--文件拆分大小-->
<maxFileSize>1MB</maxFileSize>
</rollingPolicy>
</appender>
<!--
1、控制日志的输出情况:如,开启日志,取消日志
all 代表打印全部日志
off 关闭全部日志
debug info error
-->
<root level="all">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE" />
</root>
</configuration>
三个要点:
<root>里的level控制日志级别,课件把它归成两句话:“开启日志(ALL),取消日志(OFF)”;- 想让哪个 appender 生效,就要在
<root>里用<appender-ref ref="..."/>引用它; FILE用的是RollingFileAppender+SizeAndTimeBasedRollingPolicy:日志写到D:/log/itheima-data.log,按 1MB 拆分并压缩成.log.gz(课件配置里的路径写死了 Windows 盘符,实际用时应改成本机路径)。
6.8 日志级别
课件第 51 页的规则:“只有日志的级别是大于或等于核心配置文件配置的日志级别,才会被记录,否则不记录。”
第 52、53 页把级别和默认规则讲清楚(级别表按课件题目整理,课件第 51 页原表在抽取文本里只剩占位):
| 级别 | 说明 |
|---|---|
ALL | 打开全部日志 |
TRACE | 追踪,最低级别 |
DEBUG | 调试信息 |
INFO | 常规信息 |
WARN | 警告 |
ERROR | 错误 |
OFF | 关闭全部日志 |
要记住的四条结论:
- 设置日志输出级别的作用:用于控制系统中哪些日志级别是可以输出的。
ALL和OFF分别是打开全部日志和关闭全部日志。- 级别程度依次是:
TRACE<DEBUG<INFO<WARN<ERROR。 - 默认级别是
debug(忽略大小写),只输出当前级别及高于该级别的日志。
配置文件里就是把级别写在根节点上,例如只保留 info 及以上的日志:
<root level="info">
<appender-ref ref="CONSOLE"/>
<appender-ref ref="FILE" />
</root>
对照 6.6 的 Test 类理解:logback.xml 里是 level="all" 时,LOGGER.debug(...)、LOGGER.info(...)、LOGGER.error(...) 全都会打印;一旦把 level 改成 info,两条 LOGGER.debug("参数a:10")、LOGGER.debug("参数b:0") 就不再输出了,而代码一行都不用改。
7. 作业与练习
本日无配套作业文档。(已核对 day11-特殊文件、日志技术/ 目录,其中只有 PPT/、代码/、类库/ 与 Contact.xml,没有作业题目文件。)
可自行练习的三条主线:
- 用
Properties读一个users.properties(load()+getProperty()+stringPropertyNames()遍历),再改一个键的值并用store()写回原文件。 - 用 Dom4j 解析
Contact.xml或contacts.xml,把每条contact封装成对象存进List并遍历输出;顺手验证elementText()与elementTextTrim()对<name> 潘金莲 </name>的区别。 - 搭一个 Logback 工程(三个 jar 放
lib并Add as Library,logback.xml放src),分别观察level="all"、level="info"、level="off"时控制台与日志文件输出的差异。
8. 复习要点
- 属性文件的三个特点是”只能键值对、键不能重复、后缀一般
.properties”;普通.txt因为格式随意,程序无法可靠地区分字段,这就是要用特殊文件的原因。 Properties是Map的实现类(和HashMap/TreeMap同族),但课件明确说”一般不会当集合使用”——它的核心作用是代表属性文件,读写属性文件里的内容。- 两个核心签名:加载用
void load(Reader reader),写出用void store(Writer writer, String comments);load()的重载可以传字节流(FileInputStream),说明它直接接在 day09/day10 的 IO 流体系上。 store()会自动在文件头部写入一行时间戳注释,我们传的comments写在它上面;写中文时可能被转义成\uXXXX,读回来能自动还原。- 取值要用
getProperty(String)而不是Map.get(Object);stringPropertyNames()拿全部键,containsKey()判断键是否存在。 - XML 的五条特点:标签成对出现、标签名可自定义但必须正确嵌套、有且只有一个根标签、标签可以有属性、后缀一般
.xml;写法上还有”文档声明必须写在第一行”。 - 转义与 CDATA:
< > & ' "分别替代< > & ' ";<![CDATA[ … ]]>里的内容可以随便写、不用转义。 - Dom4j 的文档对象模型:
SAXReader是解析器(代表整个框架),read()得到Document,getRootElement()得到Element,标签上的id是Attribute,标签内的文字用elementText()/getText()取;elements("contact")按名过滤一级子元素,element("user")默认拿第一个。 - 写出 XML 时课件不建议用 Dom4j,推荐直接把数据拼成 XML 格式字符串,再用 IO 流(如
PrintStream)写出去。 - 约束 XML 的文档分 DTD(
.dtd,能约束编写、不能约束数据类型)和 Schema(.xsd,两者都能约束),这一节为了解内容。 - 为什么用日志框架:
System.out.println只能打在控制台、换位置要改代码、取消日志必须改源码;日志技术能记录到控制台/文件/数据库,并且用配置开关控制启停、不改源代码。 - 日志体系分两层:日志接口(Commons Logging、SLF4J)只是规范,日志框架(Log4j、JUL、Logback)是具体实现;Logback 基于 slf4j 规范实现。
- Logback 最少要三个模块:
slf4j-api(接口)+logback-core(基础)+logback-classic(完整实现 slf4j);logback-access是可选的 Servlet 容器集成模块。 - 快速入门四步:jar 放进
lib并Add as Library→logback.xml必须放在src下 →LoggerFactory.getLogger(...)拿到Logger(声明为public static final)→ 调用info/debug/error等方法记录日志。 - 日志级别顺序为
TRACE<DEBUG<INFO<WARN<ERROR,ALL打开全部、OFF关闭全部,默认级别是debug(忽略大小写);规则是只输出”大于或等于所配置级别”的日志。