一、前言


第一次被人喊曹工,我相当诧异,那是有点久的事情了,楼主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
<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里,可能是这样的:
1 <?xml version='1.0' encoding='utf-8'?> 2 3 <Coder name="xiaoming" sex="man"
love="girl"> 4 <Girl name="Catalina" height="170" breast="C++" legLength="150">
5 </Girl> 6 </Coder>
 

对应于该xml,我们代码里定义了两个类,一个为Coder,一个为Girl。
1 package com.coder; 2 3 import lombok.Data; 4 5 /** 6 * desc: 7 *
@author: caokunliang 8 * creat_date: 2019/6/29 0029 9 * creat_time: 11:12 10
**/ 11 @Data 12 public class Coder { 13 private String name; 14 15 private
String sex;16 17 private String love; 18 /** 19 * 女朋友 20 */ 21 private Girl
girl;22 }
 
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事件处理
1 package com.coder; 2 3 import org.xml.sax.Attributes; 4 import
org.xml.sax.SAXException; 5 import org.xml.sax.ext.DefaultHandler2; 6 import
org.xml.sax.helpers.DefaultHandler; 7 8 import
javax.xml.parsers.ParserConfigurationException; 9 import
javax.xml.parsers.SAXParser;10 import javax.xml.parsers.SAXParserFactory; 11
import java.io.File; 12 import java.io.IOException; 13 import
java.io.InputStream;14 import java.util.LinkedList; 15 import
java.util.concurrent.atomic.AtomicInteger;16 17 /** 18 * desc: 19 * @author:
caokunliang20 * creat_date: 2019/6/29 0029 21 * creat_time: 11:06 22 **/ 23
public class GirlFriendHandler extends DefaultHandler { 24 private
LinkedList<Object> stack =new LinkedList<>(); 25 26 private AtomicInteger
eventOrderCounter =new AtomicInteger(0); 27 28 @Override 29 public void
startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException { 30 System.out.println("startElement: " + qName + " It's
the " + eventOrderCounter.getAndIncrement() + " one"); 31 32 if
("Coder".equals(qName)){33 34 Coder coder = new Coder(); 35 36
coder.setName(attributes.getValue("name")); 37
coder.setSex(attributes.getValue("sex")); 38
coder.setLove(attributes.getValue("love")); 39 40 stack.push(coder); 41 } 42 }
43 44 45 46 public static void main(String[] args) { 47 GirlFriendHandler
handler =new GirlFriendHandler(); 48 49 SAXParserFactory spf =
SAXParserFactory.newInstance();50 try { 51 SAXParser parser =
spf.newSAXParser();52 InputStream inputStream =
ClassLoader.getSystemClassLoader()53 .getResourceAsStream("girlfriend.xml"); 54
55 parser.parse(inputStream, handler); 56 } catch
(ParserConfigurationException | SAXException | IOException e) { 57
e.printStackTrace();58 } 59 } 60 }
 

这里,先看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事件处理

为了缩短篇幅,这里只贴出部分有改动的代码。
1 @Override 2 public void startElement(String uri, String localName, String
qName, Attributes attributes)throws SAXException { 3
System.out.println("startElement: " + qName + " It's the " +
eventOrderCounter.getAndIncrement() + " one"); 4 5 if ("Coder".equals(qName)){
6 7 Coder coder = new Coder(); 8 9 coder.setName(attributes.getValue("name"
));10 coder.setSex(attributes.getValue("sex")); 11
coder.setLove(attributes.getValue("love")); 12 13 stack.push(coder); 14 }else
if ("Girl".equals(qName)){15 16 Girl girl = new Girl(); 17
girl.setName(attributes.getValue("name")); 18
girl.setBreast(attributes.getValue("breast")); 19
girl.setHeight(attributes.getValue("height")); 20
girl.setLegLength(attributes.getValue("legLength")); 21 22 Coder coder =
(Coder)stack.peek();23 coder.setGirl(girl); 24 } 25 }
 

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

 

4、girl 元素的 endElement事件

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

 

5、coder 元素的 endElement事件
1 @Override 2 public void endElement(String uri, String localName, String
qName)throws SAXException { 3 System.out.println("endElement: " + qName + "
It's the " + eventOrderCounter.getAndIncrement() + " one"); 4 5 if ("Coder"
.equals(qName)){6 Object o = stack.pop(); 7 System.out.println(o); 8 } 9 }
 

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

 

6、执行结果



 

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

 

7、改进

现在,假设小明和女朋友有了突飞猛进的发展,女朋友怀孕了,这时候,xml 就会变成下面这样:
<Girl name="Catalina" height="170" breast="C++" legLength="150"
pregnant="true">
 

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

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

为了给小明的操作买单,我们改了一版:
1 @Override 2 public void startElement(String uri, String localName, String
qName, Attributes attributes)throws SAXException { 3
System.out.println("startElement: " + qName + " It's the " +
eventOrderCounter.getAndIncrement() + " one"); 4 5 if ("Coder".equals(qName))
{ 6 7 Coder coder = new Coder(); 8 9 setProperties(attributes,coder); 10 11
stack.push(coder);12 } else if ("Girl".equals(qName)) { 13 14 Girl girl = new
Girl();15 setProperties(attributes, girl); 16 17 Coder coder = (Coder)
stack.peek();18 coder.setGirl(girl); 19 } 20 }
其中第9/15行,利用反射完成属性的映射。具体代码如下,比较多,这里为了避免篇幅太长,折叠了。我们还新增了一个工具类 TwoTuple,方便方法进行多值返回。
1 private void setProperties(Attributes attributes, Object object) { 2
Method[] methods = object.getClass().getMethods(); 3 ArrayList<Method> list =
new ArrayList<>(); 4 list.addAll(Arrays.asList(methods)); 5 list.removeIf(o
-> o.getParameterCount() != 1); 6 7 8 for (int i = 0; i <
attributes.getLength(); i++) { 9 // 获取属性名 10 String attributesQName =
attributes.getQName(i);11 String setterMethod = "set" +
attributesQName.substring(0, 1).toUpperCase() + attributesQName.substring(1); 12
13 String value = attributes.getValue(i); 14 TwoTuple<Method, Object[]> tuple =
getSuitableMethod(list, setterMethod, value);15 // 没有找到合适的方法 16 if (tuple ==
null) { 17 continue; 18 } 19 20 Method method = tuple.first; 21 Object[]
params = tuple.second; 22 try { 23 method.invoke(object,params); 24 } catch
(IllegalAccessException | InvocationTargetException e) { 25
e.printStackTrace();26 } 27 } 28 } 29 30 private TwoTuple<Method, Object[]>
getSuitableMethod(List<Method> list, String setterMethod, String value) { 31 32
for (Method method : list) { 33 34 if (!Objects.equals(method.getName(),
setterMethod)) {35 continue; 36 } 37 38 Object[] params = new Object[1]; 39 40
/** 41 * 1;如果参数类型就是String,那么就是要找的 42 */ 43 Class<?>[] parameterTypes =
method.getParameterTypes();44 Class<?> parameterType = parameterTypes[0]; 45 if
(parameterType.equals(String.class)) { 46 params[0] = value; 47 return new
TwoTuple<>(method,params); 48 } 49 50 Boolean ok = true; 51 52 // 看看int是否可以转换
53 String name = parameterType.getName(); 54 if (name.equals("java.lang.Integer"
)55 || name.equals("int")){ 56 try { 57 params[0] = Integer.valueOf(value); 58 }
catch (NumberFormatException e){ 59 ok = false; 60 e.printStackTrace(); 61 }
62 // 看看 long 是否可以转换 63 }else if (name.equals("java.lang.Long") 64 ||
name.equals("long")){ 65 try { 66 params[0] = Long.valueOf(value); 67 }catch
(NumberFormatException e){68 ok = false; 69 e.printStackTrace(); 70 } 71 //
如果int 和 long 不行,那就只有尝试boolean了 72 }else if (name.equals("java.lang.Boolean") ||
73 name.equals("boolean")){ 74 params[0] = Boolean.valueOf(value); 75 } 76 77
if (ok){ 78 return new TwoTuple<Method,Object[]>(method,params); 79 } 80 } 81
return null; 82 } View Code 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
<https://github.com/cctvckl/tomcat-saxtest>

 

 

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

 

友情链接
ioDraw流程图
API参考文档
OK工具箱
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:637538335
关注微信