曹工說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完了后,也會寫別的內容。 同時,最近在准備面試,也會分享些面試內容。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM