曹工說Tomcat1:從XML解析說起

三國夢迴發表於2019-07-01

一、前言

第一次被人喊曹工,我相當詫異,那是有點久的事情了,樓主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完了後,也會寫別的內容。 同時,最近在準備面試,也會分享些面試內容。

 

相關文章