ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

11手写服务器HTTP Server

11手写服务器HTTP Server

反射

下面这段代码演示了反射的两个核心步骤:先获取Class对象,再通过它动态创建实例。

package com.server; /** 1.获取Class对象 2.动态创建对象 */ public class ReflectTest { public static void main(String[] args) throws Exception { // 三种获取Class对象的方式 // 方式1:对象.getClass() Student stu = new Student(); Class c1 = stu.getClass(); // 方式2:类.class c1 = Student.class; // 方式3:Class.forName("全类名") Class c2 = Class.forName("com.server.Student"); // 通过public无参构造器创建实例 Student student = (Student) c2.getConstructor().newInstance(); System.out.println(student); } } class Student { public String name; private int age; public Student() { } }

XML 解析

XML 解析的核心目标是把一段 XML 文本转换成程序可以访问的对象结构。Java 中常见的解析方式有四种:DOM、SAX、JDOM和DOM4J。其中 SAX 是唯一一种完全基于事件流、不需要把整份文档加载进内存的解析方式,适合处理大文件。

SAX 的核心思想

SAX(Simple API for XML)是一种推模型(Push)的流式解析器。解析器从文档开头逐字符扫描,遇到开始标签、文本、结束标签等结构时,主动回调注册好的处理器方法。整个过程只保留当前节点信息,内存占用与文档大小基本无关。

  • 顺序读取:只能从头到尾读一遍,不能回退,也不能随机访问某个节点。
  • 事件驱动:解析器负责“推”,开发者只需在回调里写业务逻辑。
  • 低内存:不构建 DOM 树,适合几百 MB 甚至更大的 XML 文件。
  • 只读:SAX 只能解析,不能修改或回写 XML。

SAX 与 DOM 的对比

对比项SAXDOM
解析模型事件驱动、流式树形结构、一次性加载
内存占用低,与文档大小无关高,约为文档的 5~10 倍
访问方式只能顺序读取可随机访问任意节点
是否可修改只读可增删改并回写
适用场景大文件、只需提取部分数据小文件、需要频繁操作节点

SAX 的核心接口

使用 SAX 需要实现org.xml.sax.ContentHandler接口,实际开发中通常继承它的适配器类DefaultHandler,只重写关心的方法:

  • 读到文档开头 →startDocument()
  • 读到开始标签<xxx>→startElement()
  • 读到标签中间的文本 →characters()
  • 读到结束标签</xxx>→endElement()
  • 读到文档末尾 →endDocument()

XML解析,把<person>标签数据封装成 Person 对象存入 List

XML:

<?xml version="1.0" encoding="UTF-8"?> <persons> <person> <name>至尊宝</name> <age>9000</age> </person> </persons>

① Person 实体类

就是普通 JavaBean,存 name 和 age,提供 get/set

class Person { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }

② PersonHandler 继承 DefaultHandler

成员变量

class PersonHandler extends DefaultHandler { private List<Person> persons; // 存储所有Person对象 private Person person; // 当前正在解析的Person对象 private StringBuilder sb; // 用于拼接标签内的文本内容,解决SAX解析中的文本分片问题 }

方法 1:startDocument () 文档开始时触发

整个 XML 刚开始解析的时候执行一次。 适合做初始化:创建集合、初始化字符串缓冲区

@Override public void startDocument() throws SAXException { persons = new ArrayList<>(); // 初始化容器 sb = new StringBuilder(); // 初始化字符串缓冲区 }

方法 2:startElement () 读到【开始标签】<xxx>触发

参数qName:标签名,比如persons、person、name、age逻辑:

  1. 新标签进来,清空文本缓冲区(上一个标签的文本不要残留)
  2. 如果标签是person→ new Person 对象,准备接收 name、age 数据
@Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { System.out.println(qName + "-->解析开始"); sb.setLength(0); // 清空缓冲区,确保每次新标签内容独立 // 判断当前标签是否为 <person>,若是则创建新的Person对象 if ("person".equals(qName)) { person = new Person(); } }

方法 3:characters () 读取标签中间的文本内容

⚠️ 重点考点:SAX 这个方法可能分多次调用,不能在这里直接取值赋值! 只做一件事:把读到的字符追加到 StringBuilder

@Override public void characters(char[] ch, int start, int length) throws SAXException { // 将字符数组中指定范围的内容追加到缓冲区 sb.append(ch, start, length); }

方法 4:endElement () 读到【结束标签】</xxx>触发

qName是当前闭合标签名字,逻辑:

  1. 先把缓冲区里拼接好的文本拿出来,去掉前后空白
  2. 判断闭合标签:
  • 如果是</name>→ 给 person 设置 name
  • 如果是</age>→ 给 person 设置 age(转数字)
  • 如果是</person>→ 代表这个人信息读完了,add 进集合
@Override public void endElement(String uri, String localName, String qName) throws SAXException { System.out.println(qName + "-->解析结束"); String content = sb.toString().trim(); // 去除前后空白字符 // 根据标签名设置对应属性值 if ("name".equals(qName)) { person.setName(content); } else if ("age".equals(qName)) { if (content.length() > 0) { person.setAge(Integer.parseInt(content)); // 转换为整数 } } else if ("person".equals(qName)) { // 完成一个完整 <person> 元素的解析,将其加入列表 persons.add(person); } }

方法 5:endDocument () 整个文档解析完成

@Override public void endDocument() throws SAXException { System.out.println("------------解析文档结束----------"); }

向外提供方法 getPersons ()

public List<Person> getPersons() { return persons; }

③ XmlTest02 主程序,负责创建解析器、启动解析

SAX 固定 4 步:

  1. 创建 SAXParserFactory 工厂
  2. 通过工厂拿到 SAXParser 解析器
  3. 自己写的 Handler 处理器
  4. parser.parse (流,handler) 开始解析 XML
public class XmlTest02 { public static void main(String[] args) { // 1. 获取SAX工厂实例,用于创建SAX解析器 SAXParserFactory factory = SAXParserFactory.newInstance(); try { // 2. 使用工厂创建SAX解析器对象 SAXParser parser = factory.newSAXParser(); // 3. 实例化自定义事件处理器,用于处理XML元素的开始、结束及内容 PersonHandler handler = new PersonHandler(); // 4. 从类路径加载XML文件流,路径为:com/server/p.xml InputStream is = Thread.currentThread() .getContextClassLoader() .getResourceAsStream("com/server/p.xml"); // 5. 若资源流为空,说明文件未找到,输出错误提示并终止程序 if (is == null) { System.err.println("找不到XML文件!请检查路径是否正确"); return; } // 6. 启动解析过程,将输入流和处理器传入,触发事件回调 parser.parse(is, handler); // 7. 解析完成后,获取处理器中保存的全部Person对象列表 List<Person> persons = handler.getPersons(); // 8. 遍历结果并打印每个人的姓名与年龄信息 for (Person p : persons) { System.out.println(p.getName() + " " + p.getAge()); } } catch (ParserConfigurationException | SAXException | IOException e) { // 9. 捕获解析过程中可能出现的异常,并打印堆栈信息以供调试 e.printStackTrace(); } } }

web.xml 解析:把多个标签数据封装成对象存入 List

下面是一份典型的web.xml配置片段,它通过servlet与servlet-mapping两组标签,把 Servlet 类与访问路径一一对应起来。

<?xml version="1.0" encoding="UTF-8"?> <web-app> <servlet> <servlet-name>login</servlet-name> <servlet-class>com.shsxt.LoginServlet</servlet-class> </servlet> <servlet> <servlet-name>reg</servlet-name> <servlet-class>com.shsxt.RegisterServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>login</servlet-name> <url-pattern>/login</url-pattern> <url-pattern>/g</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>reg</servlet-name> <url-pattern>/reg</url-pattern> </servlet-mapping> </web-app>

可以看到,servlet标签负责声明「名字到类」的映射,servlet-mapping标签负责声明「名字到访问路径」的映射,两者通过servlet-name关联起来。同一个 Servlet 可以配置多个url-pattern,例如上面的login就同时映射了/login和/g两个路径。

package servlet; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; /** * 使用SAX方式解析web.xml配置文件,提取Servlet定义与URL映射关系 */ public class XmlTest02 { public static void main(String[] args) { // 1. 获取SAX解析工厂实例 SAXParserFactory factory = SAXParserFactory.newInstance(); try { // 2. 创建SAX解析器对象 SAXParser parser = factory.newSAXParser(); // 3. 实例化自定义事件处理器,用于处理XML解析过程中的各种事件 WebHandler handler = new WebHandler(); // 4. 从类路径中加载web.xml资源流(路径为:servlet/web.xml) InputStream is = Thread.currentThread() .getContextClassLoader() .getResourceAsStream("servlet/web.xml"); // 5. 判断资源是否加载成功 if (is == null) { System.err.println("找不到XML文件!请检查路径是否正确"); return; } // 6. 启动解析过程,将XML流交由处理器处理 parser.parse(is, handler); // 7. 解析完成后,获取处理结果并输出 List<Entity> entitys = handler.getEntitys(); List<Mapping> mappings = handler.getMappings(); System.out.println("共解析到 " + entitys.size() + " 个Servlet定义"); System.out.println("共解析到 " + mappings.size() + " 个URL映射"); System.out.println("Servlet列表:" + entitys); System.out.println("URL映射列表:" + mappings); } catch (ParserConfigurationException | SAXException | IOException e) { e.printStackTrace(); } } } /** * 自定义SAX处理器,负责响应XML解析过程中的开始文档、开始元素、字符数据、结束元素等事件 */ class WebHandler extends DefaultHandler { private List<Entity> entitys; // 存储所有Servlet定义信息 private List<Mapping> mappings; // 存储所有URL映射关系 private Entity entity; // 当前正在构建的Servlet对象 private Mapping mapping; // 当前正在构建的URL映射对象 private String tag; // 当前正在处理的标签名 private boolean isMapping; // 标记当前是否处于servlet-mapping上下文中 private StringBuilder sb; // 用于拼接标签内的文本内容,解决分片问题 /** * 文档开始时初始化容器 */ @Override public void startDocument() throws SAXException { entitys = new ArrayList<>(); mappings = new ArrayList<>(); } /** * 遇到开始标签时触发,根据标签名称进行不同逻辑处理 */ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { System.out.println(qName + "-->解析开始"); tag = qName; sb.setLength(0); // 清空缓冲区,准备接收新内容 // 若是<servlet>标签,创建新的Entity对象 if ("servlet".equals(tag)) { entity = new Entity(); isMapping = false; } // 若是<servlet-mapping>标签,创建新的Mapping对象 else if ("servlet-mapping".equals(tag)) { mapping = new Mapping(); isMapping = true; } } /** * 收集字符数据(即标签之间的文本内容),通过缓冲区逐步拼接 */ @Override public void characters(char[] ch, int start, int length) throws SAXException { sb.append(ch, start, length); } /** * 遇到结束标签时触发,根据标签名设置对应属性值,并决定是否保存对象 */ @Override public void endElement(String uri, String localName, String qName) throws SAXException { System.out.println(qName + "-->解析结束"); String content = sb.toString().trim(); // 去除前后空白字符 // 处理<servlet-name>标签内容 if ("servlet-name".equals(qName)) { if (isMapping && mapping != null) { mapping.setName(content); } else if (entity != null) { entity.setName(content); } } // 处理<servlet-class>标签内容 else if ("servlet-class".equals(qName)) { if (entity != null && !content.isEmpty()) { entity.setClz(content); } } // 处理<url-pattern>标签内容 else if ("url-pattern".equals(qName)) { if (mapping != null && !content.isEmpty()) { mapping.addPattern(content); } } // 结束<servlet>标签时,将完整实体加入集合 if ("servlet".equals(qName)) { entitys.add(entity); } // 结束<servlet-mapping>标签时,将完整映射加入集合 else if ("servlet-mapping".equals(qName)) { mappings.add(mapping); } tag = null; // 清除当前标签状态 } /** * 文档解析完成时调用,可做收尾工作 */ @Override public void endDocument() throws SAXException { System.out.println("------------解析文档结束----------"); } /** * 提供外部访问解析结果的方法 */ public List<Mapping> getMappings() { return mappings; } public List<Entity> getEntitys() { return entitys; } } /** * 封装Servlet定义信息的类 */ class Entity { private String name; private String clz; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getClz() { return clz; } public void setClz(String clz) { this.clz = clz; } @Override public String toString() { return "Entity{name='" + name + "', clz='" + clz + "'}"; } } /** * 封装URL映射关系的类 */ class Mapping { private String name; private List<String> patterns = new ArrayList<>(); public String getName() { return name; } public void setName(String name) { this.name = name; } public void addPattern(String pattern) { patterns.add(pattern); } public List<String> getPatterns() { return patterns; } @Override public String toString() { return "Mapping{name='" + name + "', patterns=" + patterns + "}"; } }

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进