一、前言

第一次被人喊曹工,我相当诧异,那是有点久的事情了,楼主13年校招进华为,14年在东莞出差,给东莞移动的通信设备进行版本更新。他们那边的一个小伙子来接我的时候,这么叫我的,刚听到的时候,心里一紧,楼主本来进去没多久,业务也不怎么熟练,感觉都是新闻联播里才听到什么“陈工”,“李工”之类的叫法,感觉也是经验丰富、技术强硬的工人才被人这么称呼。反正呢,咋一下,心里虚的很,好歹呢,后边遇到问题了就及时和总部沟通,最后问题还是解决了,没有太丢脸。毕业至今,6年过去,楼主也已经早不在华为了,但是想起来还是觉得这个名字有点好玩,因为后来待了几家公司,再也没人这么叫我了,哈哈。。。

言归正传,曹工准备和大家一起,深入学习一下 Tomcat。Tomcat 的重要性,对于从事 Java Web开发的工程师来说,想来不用多说了,从当初在学校时,那时还是Struts2、Spring、Hibernate的天下时,Tomcat 就已经是部署 Servlet应用的主流容器了。现在后端框架换成了Spring MVC、Spring、Mybatis(或JPA),但是Tomcat 依然是主流Servlet容器。当然,Tomcat有点重,有很多对我们来说,现在根本用不到或者很少用的功能,比如 JNDI、JSP、SessionManager、Realm、Cluster、Servlet Pool、AJP等。另外,Tomcat由connector和container部分组成,其中的container部分由大到小一共分了四层,engine——》host——》context——》wrapper(即servlet)。其中engine可以包含多个host,但这个其实没啥用,无非是一个别名而已,像现在的互联网企业,一个Tomcat可能放几个webapp,更多的,可能只放一个webapp。除此之外,connector部分的AJP connector、BIO connector代码,对我们来说,也没什么用,静态页面现在主流几乎都放 nginx,谁还弄个 apache(毕业后从没用过)?

当然,楼主绝对不是要否定这些技术,我只是想说,我们要学的东西已经够多了,一些不够主流的技术还是先不要耗费大力气去弄,你想啊,一个Tomcat你学半年,mq、JVM、mysql、netty、框架、JDK源码、Redis、分布式、微服务这些还学不学了。上面的有些技术还是很有用,比如楼主最近就喜欢用 JSP 来 debug 线上代码。

去掉这些非主要的功能,剩下的东西就只有:NIO的connector、Container中的Host——》Context——》Wrapper,这个架构其实和Netty差得就不多了,学完这个后,再看Netty,会简单很多,同时,我们也能有一个横向对比的视角,来看看它们的异同点。

再次言归正传,Tomcat 里有很多的配置文件,比如常用的server.xml、webapp的web.xml,还有些不常用的,比如conf目录下的context.xml、tomcat-users.xml、甚至包括Tomcat 源码 jar 包里的每个包下都有的mbeans-descriptors.xml(看到源码不要慌,我们先不管那些mbean)。这么多xml,都需要解析,工作量还是很大的, 同样,我们也希望不要消耗太多内存,毕竟Java还是比较吃内存。

曹工说Tomcat,准备弄成一个系列,这篇是第一篇,由于楼主也菜(毕竟大家这么多年了再也没叫过我曹工),对于一些资料,别人写得比我好的,我就引用过来,当然,我会注明出处。

二、xml解析方式

当前主流的xml解析方式,共有4种,1、DOM解析;2、SAX解析;3、JDOM解析;4、DOM4J解析。详细看这里吧https://www.cnblogs.com/longqingyang/p/5577937.html

其中,DOM模型,需要把整个文档读入内存,然后构建出一个树形结构,比较消耗内存,但是也比较好做修改。在Jquery中就会构建一个dom树,平时找个元素什么的,只需要根据id或者class去查找就行,找到了进行修改也方便,编码特别简单。 而SAX解析方式不一样,它会按顺序解析文档,并在适当的时候触发事件,比如针对下面的xml片段:

<Service name="Catalina">

    <Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
//其他元素省略。。
</Service>

检测到一个<Service>,就会触发START_ELEMENT事件,然后调用我们的handler进行处理。读到 中间内容,发现有子元素<Connector>,又会触发<Connector>的 START_ELEMENT事件,然后再触发 <Connector>的 END_ELEMENT事件,最后才触发<Service>的END_ELEMENT事件。所以,SAX就是基于事件流来进行编码,只要掌握清楚了事件触发的时机,写个handler是不难的。

sax模型有个优点是,我们在获取到想要的内容后,完全可以手动终止解析。在上面的xml片段中,假设我们只关心<Connector>,那么在<Connector>的 END_ELEMENT 事件对应的handler中,我们可以手动抛出异常,来终止整个解析,这样就不用像 dom 模型一样读入并解析整个文档。

这里引用下前面博文里总结的论点:

dom优点:

      1、形成了树结构,有助于更好的理解、掌握,且代码容易编写。

      2、解析过程中,树结构保存在内存中,方便修改。(Tomcat 不需要改配置文件,鸡肋)

    缺点:

      1、由于文件是一次性读取,所以对内存的耗费比较大(tomcat作为容器,必须追求性能,肯定不能太耗内存)。

      2、如果XML文件比较大,容易影响解析性能且可能会造成内存溢出。

sax优点:

      1、采用事件驱动模式,对内存耗费比较小。(这个好,正好适合 tomcat)

      2、适用于只读取不修改XML文件中的数据时。(笔者修改补充,这个也适合tomcat,不需要修改配置文件,只需要读取并处理)

    缺点:

      1、编码比较麻烦。(还好。)

      2、很难同时访问XML文件中的多处不同数据。(确实,要访问的话,只能自己搞个field存起来,比如hashmap)

结合上面笔者自己的理解,相信大家能理解,Tomcat 为啥要基于sax模型来读取配置文件了,当然了,Tomcat 是用的Digester,不过Digester是基于 SAX 的。我们下面先来看看怎么基于 SAX解析 XML。

三、利用sax解析xml

1、准备工作

假设有个程序员,叫小明,性别男,爱好女,他有一个相对完美的女朋友,1米7,罩杯C++,一米五的大长腿。那么在xml里,可能是这样的:

 <?xml version='1.0' encoding='utf-8'?>

 <Coder name="xiaoming" sex="man" love="girl">
<Girl name="Catalina" height="170" breast="C++" legLength="150">
</Girl>
6 </Coder>

对应于该xml,我们代码里定义了两个类,一个为Coder,一个为Girl。

 package com.coder;

 import lombok.Data;

 /**
* desc:
* @author: caokunliang
* creat_date: 2019/6/29 0029
* creat_time: 11:12
**/
@Data
public class Coder {
private String name; private String sex; private String love;
/**
* 女朋友
*/
private Girl girl;
}
package com.coder;

import lombok.Data;

/**
* desc:
* @author: caokunliang
* creat_date: 2019/6/29 0029
* creat_time: 11:13
**/
@Data
public class Girl {
private String name;
private String height;
private String breast;
private String legLength; }

我们的最终目的,是生成一个Coder 对象,再生成一个Girl 对象,同时,要把 Girl 对象设到 Coder 对象里面去。按照 sax 编程模型,sax 的解析器在解析过程中,会按如下顺序,触发以下4个事件:

2、coder的startElement事件处理

 package com.coder;

 import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.ext.DefaultHandler2;
import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger; /**
* desc:
* @author: caokunliang
* creat_date: 2019/6/29 0029
* creat_time: 11:06
**/
public class GirlFriendHandler extends DefaultHandler {
private LinkedList<Object> stack = new LinkedList<>(); private AtomicInteger eventOrderCounter = new AtomicInteger(0); @Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
System.out.println("startElement: " + qName + " It's the " + eventOrderCounter.getAndIncrement() + " one"); 32 if ("Coder".equals(qName)){ Coder coder = new Coder(); coder.setName(attributes.getValue("name"));
coder.setSex(attributes.getValue("sex"));
coder.setLove(attributes.getValue("love")); 40 stack.push(coder);
}
} public static void main(String[] args) {
GirlFriendHandler handler = new GirlFriendHandler(); SAXParserFactory spf = SAXParserFactory.newInstance();
try {
SAXParser parser = spf.newSAXParser();
InputStream inputStream = ClassLoader.getSystemClassLoader()
.getResourceAsStream("girlfriend.xml"); parser.parse(inputStream, handler);
} catch (ParserConfigurationException | SAXException | IOException e) {
e.printStackTrace();
}
}
}

这里,先看46行,我们先 new 了 一个 GirlFriendHandler ,然后通过工厂,获取了一个  SAXParser 实例,然后读取了classpath 下的 girlfriend.xml ,然后利用 parser 对该xml 进行解析。接下来,再看GirlFriendHandler 类,该类继承了 org.xml.sax.helpers.DefaultHandler,org.xml.sax.helpers.DefaultHandler里面的方法都是空实现,继承该方法主要就是方便我们重写。 我们首先重写了 com.coder.GirlFriendHandler#startElement 方法,这个方法里,我们首先进行计算,打印访问顺序。

然后,在32行,我们判断,如果当前的元素为 coder,则生成一个 coder 对象,并填充属性,然后放到 handler 的一个 实例变量里,该变量利用链表实现栈的功能。该方法执行结束后,stack 中就会存进了coder 对象。

3、girl的startElement事件处理

为了缩短篇幅,这里只贴出部分有改动的代码。

  @Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
System.out.println("startElement: " + qName + " It's the " + eventOrderCounter.getAndIncrement() + " one"); if ("Coder".equals(qName)){ Coder coder = new Coder(); coder.setName(attributes.getValue("name"));
coder.setSex(attributes.getValue("sex"));
coder.setLove(attributes.getValue("love")); stack.push(coder);
}else if ("Girl".equals(qName)){ Girl girl = new Girl();
girl.setName(attributes.getValue("name"));
girl.setBreast(attributes.getValue("breast"));
girl.setHeight(attributes.getValue("height"));
girl.setLegLength(attributes.getValue("legLength")); Coder coder = (Coder)stack.peek();
coder.setGirl(girl);
}
}

14行,判断是否为 Girl 元素;16-20行主要对 Girl 的属性进行赋值,22 行从栈中取出 Coder对象,23行设置 coder 的 girl 属性。现在应该明白了stack 的作用了吧,主要是方便我们访问前面已经处理过的对象。

4、girl 元素的 endElement事件

不做处理。当然,也可以做点啥,比如把小明的女朋友抢了。。。当然,我们不是那种人。

5、coder 元素的 endElement事件

  @Override
public void endElement(String uri, String localName, String qName) throws SAXException {
System.out.println("endElement: " + qName + " It's the " + eventOrderCounter.getAndIncrement() + " one"); if ("Coder".equals(qName)){
Object o = stack.pop();
System.out.println(o);
}
}

这里,我们重写了endElement,主要是遇到 coder 元素结尾时,将 coder元素从栈中弹出来,并打印。

6、执行结果

可以看到,小明已经有了一个相当不错的女朋友。鼓掌!

7、改进

现在,假设小明和女朋友有了突飞猛进的发展,女朋友怀孕了,这时候,xml 就会变成下面这样:

    <Girl name="Catalina" height="170" breast="C++" legLength="150" pregnant="true">

那我们代码可能就不太满足了,首先, girl 这个当然肯定要改,这个没办法,但是,我们的handler好像也要加一行:

girl.setIsPregnant(true);

这就麻烦了,虽然改动不多。但你改了还得测,还得重新打包,烦呐。。小明真的坑啊,没事把人家弄怀孕干嘛。。当时怎么不用反射呢,反射的话,不就没这么多麻烦了吗?

为了给小明的操作买单,我们改了一版:

 @Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
System.out.println("startElement: " + qName + " It's the " + eventOrderCounter.getAndIncrement() + " one"); if ("Coder".equals(qName)) { Coder coder = new Coder(); 9 setProperties(attributes,coder); stack.push(coder);
} else if ("Girl".equals(qName)) { Girl girl = new Girl();
15 setProperties(attributes, girl); Coder coder = (Coder) stack.peek();
coder.setGirl(girl);
}
}

其中第9/15行,利用反射完成属性的映射。具体代码如下,比较多,这里为了避免篇幅太长,折叠了。我们还新增了一个工具类 TwoTuple,方便方法进行多值返回。

 private void setProperties(Attributes attributes, Object object) {
Method[] methods = object.getClass().getMethods();
ArrayList<Method> list = new ArrayList<>();
list.addAll(Arrays.asList(methods));
list.removeIf(o -> o.getParameterCount() != 1); for (int i = 0; i < attributes.getLength(); i++) {
// 获取属性名
String attributesQName = attributes.getQName(i);
String setterMethod = "set" + attributesQName.substring(0, 1).toUpperCase() + attributesQName.substring(1); String value = attributes.getValue(i);
TwoTuple<Method, Object[]> tuple = getSuitableMethod(list, setterMethod, value);
// 没有找到合适的方法
if (tuple == null) {
continue;
} Method method = tuple.first;
Object[] params = tuple.second;
try {
method.invoke(object,params);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
} private TwoTuple<Method, Object[]> getSuitableMethod(List<Method> list, String setterMethod, String value) { for (Method method : list) { if (!Objects.equals(method.getName(), setterMethod)) {
continue;
} Object[] params = new Object[1]; /**
* 1;如果参数类型就是String,那么就是要找的
*/
Class<?>[] parameterTypes = method.getParameterTypes();
Class<?> parameterType = parameterTypes[0];
if (parameterType.equals(String.class)) {
params[0] = value;
return new TwoTuple<>(method,params);
} Boolean ok = true; // 看看int是否可以转换
String name = parameterType.getName();
if (name.equals("java.lang.Integer")
|| name.equals("int")){
try {
params[0] = Integer.valueOf(value);
}catch (NumberFormatException e){
ok = false;
e.printStackTrace();
}
// 看看 long 是否可以转换
}else if (name.equals("java.lang.Long")
|| name.equals("long")){
try {
params[0] = Long.valueOf(value);
}catch (NumberFormatException e){
ok = false;
e.printStackTrace();
}
// 如果int 和 long 不行,那就只有尝试boolean了
}else if (name.equals("java.lang.Boolean") ||
name.equals("boolean")){
params[0] = Boolean.valueOf(value);
} if (ok){
return new TwoTuple<Method,Object[]>(method,params);
}
}
return null;
}
package com.coder;

public class TwoTuple<A, B> {

    public final A first;

    public final B second;

    public TwoTuple(A a, B b){
first = a;
second = b;
} @Override
public String toString(){
return "(" + first + ", " + second + ")";
} }

8、后续

后续其实还会有很多变化,我们这里不一一演示了。比如小明的职业可能发生变化,可能会秃,小明的女朋友后续会变成一个当妈的。但我们这里的类型还是写死的,明显是要不得的,所以这个例子,其实还有相当的优化空间。但是,幸运的是,这些工作也不用我们去做,Tomcat 就利用了 digester 机制来动态而灵活地处理这些变化。

四、总结及源码

本篇作为一个开篇,讲了xml解析的sax模型。xml 解析,对于写sdk、写框架的开发者来说,还是很重要的,大家学了这个,就扫平了自己写框架的第一个障碍了。 当然,这个sax解析还很基础,Tomcat 要是照我们这么写,那估计也活不到现在。Tomcat 其实是用了 Digester 来解析 xml,相当方便和高效。下一讲我们就说说Digester。

源码:

https://github.com/cctvckl/tomcat-saxtest

我拉了个微信群,方便大家和我一起学习,后续tomcat完了后,也会写别的内容。 同时,最近在准备面试,也会分享些面试内容。

曹工说Tomcat1:从XML解析说起的更多相关文章

  1. 曹工说Tomcat3:深入理解 Tomcat Digester

    一.前言 我写博客主要靠自己实战,理论知识不是很强,要全面介绍Tomcat Digester,还是需要一定的理论功底.翻阅了一些介绍 Digester 的书籍.博客,发现不是很系统,最后发现还是官方文 ...

  2. 曹工说Spring Boot源码(6)-- Spring怎么从xml文件里解析bean的

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  3. 曹工说Spring Boot源码(7)-- Spring解析xml文件,到底从中得到了什么(上)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  4. 曹工说Spring Boot源码(8)-- Spring解析xml文件,到底从中得到了什么(util命名空间)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  5. 曹工说Spring Boot源码(9)-- Spring解析xml文件,到底从中得到了什么(context命名空间上)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  6. # 曹工说Spring Boot源码(10)-- Spring解析xml文件,到底从中得到了什么(context:annotation-config 解析)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  7. 曹工说Spring Boot源码(12)-- Spring解析xml文件,到底从中得到了什么(context:component-scan完整解析)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  8. 曹工说Spring Boot源码(15)-- Spring从xml文件里到底得到了什么(context:load-time-weaver 完整解析)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  9. 曹工说Spring Boot源码(16)-- Spring从xml文件里到底得到了什么(aop:config完整解析【上】)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

随机推荐

  1. 强大的 pdf 编辑器 —— Acrobat

    菜单栏中的 [编辑](Edit)⇒ [编辑文本和图像](Edit Text & Images) 可以随意地编辑当前 pdf 中的文本信息,和图像信息: pdf 格式的转换,更是不在话下. 转 ...

  2. 【003】【Java虚拟机——对象死亡的判断】

    对象死亡! 垃圾收集器在对堆进行回收前,首先要做的事情就是要确定这些对象之中哪些还"存活"着, 哪些已经"死去" (即不可能再被不论什么途径使用的对象). 1)  引用计 ...

  3. 简明Python3教程 8.控制流

    简介 迄今为止我们见到的所有程序总是含有一连串语句并且python忠实的顺序执行它们. 那么如何改变它们的执行顺序呢?例如你希望程序根据不同情况作出不同反应,按照当前时间分别 打印出’Good Mor ...

  4. C#获取windows 10的下载文件夹路径

    Windows没有为“下载”文件夹定义CSIDL,并且通过Environment.SpecialFolder枚举无法使用它. 但是,新的Vista 知名文件夹 API确实使用ID定义它FOLDERID ...

  5. 微信小程序开发之从相册获取图片 使用相机拍照 本地图片上传

    1.index.wxml <!--index.wxml--> <button style="margin:30rpx;" bindtap="choose ...

  6. WPF自定义控件 使用阿里巴巴图标

    原文:WPF自定义控件 使用阿里巴巴图标 上一篇介绍了 WPF自定义控件 按钮 的初步使用,在进一步介绍WPF自定义控件 按钮之前,先介绍一下如何在WPF项目中使用阿里巴巴图标,方便以后做示例. 1. ...

  7. WPF中ListBox滚动时的缓动效果

    原文:WPF中ListBox滚动时的缓动效果 上周工作中遇到的问题: 常规的ListBox在滚动时总是一格格的移动,感觉上很生硬. 所以想要实现类似Flash中的那种缓动的效果,使ListBox滚动时 ...

  8. docker ubuntu 不选时区

    在用ubuntu:18.04基本镜像进行构建的时候.出现啦选择时区的地方,然后会卡住. FROM ubuntu:18.04 #env 环境变量 ENV AUTHOR="xianyunyehe ...

  9. ASP 用隐藏域解决Http无状态问题

    <!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head>    < ...

  10. 【全面解禁!真正的Expression Blend实战开发技巧】第八章 FluidMoveBehavior完全解析之一漂浮移动

    原文:[全面解禁!真正的Expression Blend实战开发技巧]第八章 FluidMoveBehavior完全解析之一漂浮移动 好久没更新博客了,今天如果没急事,准备连发三篇,完全讲解Blend ...