1. 文章
  2. Java 进阶线
  3. 文件、配置与并发

Properties、XML 与日志:配置如何读取,问题如何定位

更新于 28 分钟阅读 第 11 / 15 站 #java#java-advanced#day11

明确配置编码、层级解析与日志接口的职责。

我的学习 →

Java 进阶线 · 线路目录
本页导读

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 就可以靠嵌套标签把”一个用户”这个整体表达清楚。

![存同一组用户信息时,属性文件只能用 name1、password2 这类编号键区分](https://studio.shenziggy.top/media/3217fc92-088e-475c-8f6a-714f24e3b164) ![同一组用户信息改用 XML,一个 user 标签就是一个整体](https://studio.shenziggy.top/media/905f44a7-f476-416e-b93b-a77eb504beda)
![普通文件.txt 内容格式随意,无法区分字段](https://studio.shenziggy.top/media/18eb1e5a-25eb-4f05-817a-bf669f7b14d4) ![属性文件.properties 是规整的键值对](https://studio.shenziggy.top/media/d2575405-276c-4564-89a1-30b72cf36fc2) ![XML 文件用嵌套标签表达多个用户的结构](https://studio.shenziggy.top/media/90bdebd7-cc94-4cce-9912-acdf752bb8f7)

课件给特殊文件总结的用途只有两条(第 4 页):

  • 存储有关系的数据,作为系统的配置文件
  • 作为信息进行传输

第 5 页说明本专题的学习目标,也是后面三章的主线:了解它们的特点和作用 → 学习使用程序读取它们里面的数据 → 学习使用程序把数据存储到这些文件里。

2. Properties 属性文件

2.1 属性文件的三个特点

课件第 8 页把属性文件的规则压成三条:

  1. 都只能是键值对;
  2. 键不能重复;
  3. 文件后缀一般是 .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 页列了五条:

  1. XML 中的 <标签名> 称为一个标签或一个元素,一般是成对出现的。
  2. XML 中的标签名可以自己定义(可扩展),但必须要正确的嵌套。
  3. XML 中只能有一个根标签。
  4. XML 中的标签可以有属性。
  5. 如果一个文件中放置的是 XML 格式的数据,这个文件就是 XML 文件,后缀一般要写成 .xml。

3.3 XML 的创建

课件第 16 页:“就是创建一个 XML 类型的文件,要求文件的后缀必须使用 xml,如 hello_world.xml。“在 IDEA 里就是对着 src 右键 → New → File,然后在 New File 对话框里输入 hello_world.xml。

![IDEA 中右键 New → File 新建文件](https://studio.shenziggy.top/media/dbe4a406-3049-479e-b1a7-67aed1ff7c36) ![在 New File 对话框输入 hello_world.xml](https://studio.shenziggy.top/media/b3a2a86e-c1ae-4fd2-90ea-ab464530e4f5)

3.4 XML 的语法规则

课件第 17 页把规则集中在一页:

  • XML 文件的后缀名为 xml,文档声明必须是第一行:
<?xml version="1.0" encoding="UTF-8" ?>

其中 version:XML 默认的版本号码,该属性必须存在;encoding:本 XML 文件的编码。

  • XML 中可以定义注释信息:<!-- 注释内容 -->
  • XML 中书写 <、& 等字符可能会出现冲突导致报错,此时可用如下特殊字符替代:
实体代表的字符含义
&lt;<小于
&gt;>大于
&amp;&和号
&apos;'单引号
&quot;"引号
  • 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 &gt;= 18 &amp;&amp; age &lt;= 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 页给出的步骤:

  1. 下载 Dom4j 框架,官网下载;
  2. 在项目中创建一个文件夹:lib;
  3. 将 dom4j-2.1.3.jar 文件复制到 lib 文件夹;
  4. 在 jar 文件上点右键,选择 Add as Library → 点击 OK;
  5. 在类中导包使用。

配套工程的 类库/ 下放的是 dom4j-2.1.4.jar(版本号略有出入,用法一致)。

4.3 解析思想:文档对象模型

课件第 25 页用一棵”标签树”解释 Dom4j 的思想——XML 的每个部分都对应一个 Java 对象:

XML 组成对应的 Java 对象说明
整个文档Document整个文档
标签Element元素
标签上的 id="1"Attribute属性
标签里的文字子元素、文本由 Element 提供方法取出
解析入口SAXReaderDom4j 提供的解析器,可以认为是代表整个 Dom4j 框架

课件用的示例数据:

<?xml version="1.0" encoding="UTF-8" ?>
<users>
    <user id="1">
        <name>张无忌</name>
        <sex>男</sex>
        <地址>光明顶</地址>
        <password>minmin</password>
        <data> 3 &lt; 2 &amp;&amp; 5 > 4 </data>
        <data1>
            <![CDATA[
                3 < 2 && 5 > 4
            ]]>
        </data1>
    </user>
    <user id="2">
        <name>敏敏</name>
        <sex>女</sex>
        <地址>光明顶</地址>
        <password>wuji</password>
        <data> 3 &lt; 2 &amp;&amp; 5 > 4 </data>
        <data1>
            <![CDATA[
                3 < 2 && 5 > 4
            ]]>
        </data1>
    </user>
</users>

4.4 得到 Document 对象

课件第 26 页的要点(表格按课件题目与配套示例整理):

类型方法说明
SAXReaderSAXReader()创建 Dom4j 提供的解析器对象
SAXReaderDocument read(String url)把 XML 文件读成一个 Document 文档对象(还有 read(File)、read(InputStream) 重载)
DocumentElement getRootElement()拿到根标签对应的元素对象

4.5 Element 提供的方法

课件第 27、28 页连着两页讲 Element 的方法,并配了一段完整代码截图:

![用 SAXReader 读成 Document,再取根元素](https://studio.shenziggy.top/media/7f82c68e-ec04-491f-82d3-13f219afebf4)

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 集合,并遍历输出。”

![contacts.xml 联系人数据(contact 上带 id、vip 属性)](https://studio.shenziggy.top/media/d636cde5-7c22-4921-a123-5ad11e0ba714)

按课件案例的数据形态,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 文件的编写。“步骤:

  1. 编写 DTD 约束文档,后缀必须是 .dtd;
  2. 在需要编写的 XML 文件中导入该 DTD 约束文档;
  3. 然后 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 示例](https://studio.shenziggy.top/media/1a02e1fa-dab2-4925-a266-593f996375aa)

课件的结论是两句话:DTD 可以约束 XML 文件的编写,但不能约束具体的数据类型。上面 售价 写 9.9 合法,写 很便宜 也合法,DTD 管不了。

5.3 Schema 的使用

课件第 36 页的需求:“利用 schema 文档约束,约束一个 XML 文件的编写。“步骤:

  1. 编写 schema 约束文档,后缀必须是 .xsd,具体的形式到代码中观看;
  2. 在需要编写的 XML 文件中导入该 schema 约束文档;
  3. 按照约束内容编写 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 对比

对比项DTDSchema
文件后缀.dtd.xsd
约束 XML 的编写可以可以
约束具体的数据类型不能可以(如 type='double'、type='string')

这一节课件标了[了解],重点是知道”约束是什么、有哪两类”,不必背 DTD 与 XSD 的写法。

6. 日志技术

6.1 什么是日志,为什么需要日志

课件第 38 页的类比是”好比生活中的日记,可以记录你生活中的点点滴滴”:

  • 程序中的日志,通常就是一个文件,里面记录的是程序运行过程中的各种信息。

第 41 页给出的正式定义是:用来记录程序运行过程中的信息,并可以进行永久存储。它要回答的三个现实问题(课件第 38 页):

  1. 希望系统能记住某些数据是被谁操作的,比如被谁删除了?
  2. 想分析用户浏览系统的具体情况,以便挖掘用户的具体喜好?
  3. 当系统在开发中或者上线后出现了 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 页):

  1. 在项目下新建文件夹 lib,导入 Logback 的相关 jar 包到该文件夹下,并添加到项目库中去;
  2. 必须将 Logback 的核心配置文件 logback.xml 直接拷贝到 src 目录下(必须是 src 下);
  3. 在代码中获取日志的对象:
public static final Logger LOGGER = LoggerFactory.getLogger("类名");
  1. 调用日志对象的方法记录日志信息。

配套示例 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输出位置
CONSOLEch.qos.logback.core.ConsoleAppender控制台
FILEch.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关闭全部日志

要记住的四条结论:

  1. 设置日志输出级别的作用:用于控制系统中哪些日志级别是可以输出的。
  2. ALL 和 OFF 分别是打开全部日志和关闭全部日志。
  3. 级别程度依次是:TRACE < DEBUG < INFO < WARN < ERROR。
  4. 默认级别是 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,没有作业题目文件。)

可自行练习的三条主线:

  1. 用 Properties 读一个 users.properties(load() + getProperty() + stringPropertyNames() 遍历),再改一个键的值并用 store() 写回原文件。
  2. 用 Dom4j 解析 Contact.xml 或 contacts.xml,把每条 contact 封装成对象存进 List 并遍历输出;顺手验证 elementText() 与 elementTextTrim() 对 <name> 潘金莲 </name> 的区别。
  3. 搭一个 Logback 工程(三个 jar 放 lib 并 Add as Library,logback.xml 放 src),分别观察 level="all"、level="info"、level="off" 时控制台与日志文件输出的差异。

8. 复习要点

  1. 属性文件的三个特点是”只能键值对、键不能重复、后缀一般 .properties”;普通 .txt 因为格式随意,程序无法可靠地区分字段,这就是要用特殊文件的原因。
  2. Properties 是 Map 的实现类(和 HashMap/TreeMap 同族),但课件明确说”一般不会当集合使用”——它的核心作用是代表属性文件,读写属性文件里的内容。
  3. 两个核心签名:加载用 void load(Reader reader),写出用 void store(Writer writer, String comments);load() 的重载可以传字节流(FileInputStream),说明它直接接在 day09/day10 的 IO 流体系上。
  4. store() 会自动在文件头部写入一行时间戳注释,我们传的 comments 写在它上面;写中文时可能被转义成 \uXXXX,读回来能自动还原。
  5. 取值要用 getProperty(String) 而不是 Map.get(Object);stringPropertyNames() 拿全部键,containsKey() 判断键是否存在。
  6. XML 的五条特点:标签成对出现、标签名可自定义但必须正确嵌套、有且只有一个根标签、标签可以有属性、后缀一般 .xml;写法上还有”文档声明必须写在第一行”。
  7. 转义与 CDATA:&lt; &gt; &amp; &apos; &quot; 分别替代 < > & ' ";<![CDATA[ … ]]> 里的内容可以随便写、不用转义。
  8. Dom4j 的文档对象模型:SAXReader 是解析器(代表整个框架),read() 得到 Document,getRootElement() 得到 Element,标签上的 id 是 Attribute,标签内的文字用 elementText()/getText() 取;elements("contact") 按名过滤一级子元素,element("user") 默认拿第一个。
  9. 写出 XML 时课件不建议用 Dom4j,推荐直接把数据拼成 XML 格式字符串,再用 IO 流(如 PrintStream)写出去。
  10. 约束 XML 的文档分 DTD(.dtd,能约束编写、不能约束数据类型)和 Schema(.xsd,两者都能约束),这一节为了解内容。
  11. 为什么用日志框架:System.out.println 只能打在控制台、换位置要改代码、取消日志必须改源码;日志技术能记录到控制台/文件/数据库,并且用配置开关控制启停、不改源代码。
  12. 日志体系分两层:日志接口(Commons Logging、SLF4J)只是规范,日志框架(Log4j、JUL、Logback)是具体实现;Logback 基于 slf4j 规范实现。
  13. Logback 最少要三个模块:slf4j-api(接口)+ logback-core(基础)+ logback-classic(完整实现 slf4j);logback-access 是可选的 Servlet 容器集成模块。
  14. 快速入门四步:jar 放进 lib 并 Add as Library → logback.xml 必须放在 src 下 → LoggerFactory.getLogger(...) 拿到 Logger(声明为 public static final)→ 调用 info/debug/error 等方法记录日志。
  15. 日志级别顺序为 TRACE < DEBUG < INFO < WARN < ERROR,ALL 打开全部、OFF 关闭全部,默认级别是 debug(忽略大小写);规则是只输出”大于或等于所配置级别”的日志。