一、前言

第一次被人喊曹工,我相当诧异,那是有点久的事情了,楼主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. [转] Java的打包apk, jar、war、ear包

    apk, war, ear可用zip压缩,看起来这四个包都是用简单方式zip/jar即可生成. ---------------------------------------------------- ...

  2. Notice: Undefined index: user in D:\phpStudy\WWW\js\ls\lsmc\php\add.php on line 9

    原文:Notice: Undefined index: user in D:\phpStudy\WWW\js\ls\lsmc\php\add.php on line 9 (初用数据库(mysql)做用 ...

  3. kill the lock

    $ killall -s 9 krunner_lock [ZT][From:] http://www.commandlinefu.com/commands/view/2264/unlock-your- ...

  4. wpf控件设计时支持(1)

    原文:wpf控件设计时支持(1) 这部分内容几乎是大家忽略的内容,我想还是来介绍一下. 本篇源码下载 1.属性元数据 在vs IDE中,在asp.net,winfrom等开发环境下,右侧的Proper ...

  5. Frequentist 观点和 Bayesian 观点

    1. Frequentist view Frequentist approach views the model parameters as unknown constants(未知的常数,而不是一个 ...

  6. chaos —— 混沌

    混沌,一个被严重滥用的物理数学概念. 混沌(chaos)是一个动力学系统(Dynamic System)概念,指的是确定性动力学系统因对初值敏感而表现出的不可预测的.类似随机性的运动. 1. 洛伦兹吸 ...

  7. ASP.NET Core Windows 环境配置 - ASP.NET Core 基础教程 - 简单教程,简单编程

    原文:ASP.NET Core Windows 环境配置 - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core Windows 环境配置 ASP.NET Core ...

  8. 潜移默化学会WPF--值转换器

    原文:潜移默化学会WPF--值转换器 1. binding 后面的stringFormat的写法----连接字符串 <TextBlock Text="{Binding Path=Qty ...

  9. 精装友好联络算法实现借壳和RI

    精装友好联络会 注册算法分析: 1.  许可证由三部分组成. 2. 的登记号的第一部分是顺序编号0x6d模 3. 登记代码的第二部分: 先将订单号与0XB25F1异或,将异或后的结果转换成十进制字符串 ...

  10. WPF生命周期

    App.xaml.cs         重写OnStartup方法,完成初始化 wpf中Window的生命周期