曹工说Tomcat1:从XML解析说起

一、前言

第一次被人喊曹工,我相当诧异,那是有点久的事情了,楼主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>

 

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

sax模型有个优点是,我们在获取到想要的内容后,完全可以手动终止解析。在上面的xml片段中,假设我们只关心,那么在的 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个事件:

曹工说Tomcat1:从XML解析说起_第1张图片

 

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: caokunliang
20  * creat_date: 2019/6/29 0029
21  * creat_time: 11:06
22  **/
23 public class GirlFriendHandler  extends DefaultHandler {
24     private LinkedList 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、执行结果

曹工说Tomcat1:从XML解析说起_第2张图片

 

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

 

7、改进

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

    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 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 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 getSuitableMethod(List 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,params);
79             }
80         }
81         return null;
82     }
View Code
package com.coder;

public class TwoTuple {

    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解析说起)