一、前言
第一次被人喊曹工,我相当诧异,那是有点久的事情了,楼主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>
检测到一个
sax模型有个优点是,我们在获取到想要的内容后,完全可以手动终止解析。在上面的xml片段中,假设我们只关心
这里引用下前面博文里总结的论点:
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
这里,先看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 就会变成下面这样:
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 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 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 getSuitableMethod(List 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,params);
79 }
80 }
81 return null;
82 }
package com.coder;
public class TwoTuple {
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完了后,也会写别的内容。 同时,最近在准备面试,也会分享些面试内容。