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