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