• 曹工说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>

     

    检测到一个<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: caokunliang
    20  * 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

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

  • 相关阅读:
    SCRUM项目 4.0
    【操作系统】实验三 进程调度模拟程序
    Spring 计划
    backlog
    0505-NABCD模型、视频
    0429团队准备
    1028 C语言文法
    编译原理第二次作业 编译器任务总结
    1014编译原理第二次作业(修改版)
    算法原理与分析第二次作业
  • 原文地址:https://www.cnblogs.com/grey-wolf/p/11105744.html
Copyright © 2020-2023  润新知