曹工说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里,可能是这样的:
<?xml version='1.0' encoding='utf-8'?> <Coder name="xiaoming" sex="man" love="girl">
<Girl name="Catalina" height="170" breast="C++" legLength="150">
</Girl>
6 </Coder>
对应于该xml,我们代码里定义了两个类,一个为Coder,一个为Girl。
package com.coder; import lombok.Data; /**
* desc:
* @author: caokunliang
* creat_date: 2019/6/29 0029
* creat_time: 11:12
**/
@Data
public class Coder {
private String name; private String sex; private String love;
/**
* 女朋友
*/
private Girl girl;
}
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事件处理
package com.coder; import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.ext.DefaultHandler2;
import org.xml.sax.helpers.DefaultHandler; import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.LinkedList;
import java.util.concurrent.atomic.AtomicInteger; /**
* desc:
* @author: caokunliang
* creat_date: 2019/6/29 0029
* creat_time: 11:06
**/
public class GirlFriendHandler extends DefaultHandler {
private LinkedList<Object> stack = new LinkedList<>(); private AtomicInteger eventOrderCounter = new AtomicInteger(0); @Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
System.out.println("startElement: " + qName + " It's the " + eventOrderCounter.getAndIncrement() + " one"); 32 if ("Coder".equals(qName)){ Coder coder = new Coder(); coder.setName(attributes.getValue("name"));
coder.setSex(attributes.getValue("sex"));
coder.setLove(attributes.getValue("love")); 40 stack.push(coder);
}
} public static void main(String[] args) {
GirlFriendHandler handler = new GirlFriendHandler(); SAXParserFactory spf = SAXParserFactory.newInstance();
try {
SAXParser parser = spf.newSAXParser();
InputStream inputStream = ClassLoader.getSystemClassLoader()
.getResourceAsStream("girlfriend.xml"); parser.parse(inputStream, handler);
} catch (ParserConfigurationException | SAXException | IOException e) {
e.printStackTrace();
}
}
}
这里,先看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事件处理
为了缩短篇幅,这里只贴出部分有改动的代码。
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
System.out.println("startElement: " + qName + " It's the " + eventOrderCounter.getAndIncrement() + " one"); if ("Coder".equals(qName)){ Coder coder = new Coder(); coder.setName(attributes.getValue("name"));
coder.setSex(attributes.getValue("sex"));
coder.setLove(attributes.getValue("love")); stack.push(coder);
}else if ("Girl".equals(qName)){ Girl girl = new Girl();
girl.setName(attributes.getValue("name"));
girl.setBreast(attributes.getValue("breast"));
girl.setHeight(attributes.getValue("height"));
girl.setLegLength(attributes.getValue("legLength")); Coder coder = (Coder)stack.peek();
coder.setGirl(girl);
}
}
14行,判断是否为 Girl 元素;16-20行主要对 Girl 的属性进行赋值,22 行从栈中取出 Coder对象,23行设置 coder 的 girl 属性。现在应该明白了stack 的作用了吧,主要是方便我们访问前面已经处理过的对象。
4、girl 元素的 endElement事件
不做处理。当然,也可以做点啥,比如把小明的女朋友抢了。。。当然,我们不是那种人。
5、coder 元素的 endElement事件
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
System.out.println("endElement: " + qName + " It's the " + eventOrderCounter.getAndIncrement() + " one"); if ("Coder".equals(qName)){
Object o = stack.pop();
System.out.println(o);
}
}
这里,我们重写了endElement,主要是遇到 coder 元素结尾时,将 coder元素从栈中弹出来,并打印。
6、执行结果
可以看到,小明已经有了一个相当不错的女朋友。鼓掌!
7、改进
现在,假设小明和女朋友有了突飞猛进的发展,女朋友怀孕了,这时候,xml 就会变成下面这样:
<Girl name="Catalina" height="170" breast="C++" legLength="150" pregnant="true">
那我们代码可能就不太满足了,首先, girl 这个当然肯定要改,这个没办法,但是,我们的handler好像也要加一行:
girl.setIsPregnant(true);
这就麻烦了,虽然改动不多。但你改了还得测,还得重新打包,烦呐。。小明真的坑啊,没事把人家弄怀孕干嘛。。当时怎么不用反射呢,反射的话,不就没这么多麻烦了吗?
为了给小明的操作买单,我们改了一版:
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
System.out.println("startElement: " + qName + " It's the " + eventOrderCounter.getAndIncrement() + " one"); if ("Coder".equals(qName)) { Coder coder = new Coder(); 9 setProperties(attributes,coder); stack.push(coder);
} else if ("Girl".equals(qName)) { Girl girl = new Girl();
15 setProperties(attributes, girl); Coder coder = (Coder) stack.peek();
coder.setGirl(girl);
}
}
其中第9/15行,利用反射完成属性的映射。具体代码如下,比较多,这里为了避免篇幅太长,折叠了。我们还新增了一个工具类 TwoTuple,方便方法进行多值返回。
private void setProperties(Attributes attributes, Object object) {
Method[] methods = object.getClass().getMethods();
ArrayList<Method> list = new ArrayList<>();
list.addAll(Arrays.asList(methods));
list.removeIf(o -> o.getParameterCount() != 1); for (int i = 0; i < attributes.getLength(); i++) {
// 获取属性名
String attributesQName = attributes.getQName(i);
String setterMethod = "set" + attributesQName.substring(0, 1).toUpperCase() + attributesQName.substring(1); String value = attributes.getValue(i);
TwoTuple<Method, Object[]> tuple = getSuitableMethod(list, setterMethod, value);
// 没有找到合适的方法
if (tuple == null) {
continue;
} Method method = tuple.first;
Object[] params = tuple.second;
try {
method.invoke(object,params);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
} private TwoTuple<Method, Object[]> getSuitableMethod(List<Method> list, String setterMethod, String value) { for (Method method : list) { if (!Objects.equals(method.getName(), setterMethod)) {
continue;
} Object[] params = new Object[1]; /**
* 1;如果参数类型就是String,那么就是要找的
*/
Class<?>[] parameterTypes = method.getParameterTypes();
Class<?> parameterType = parameterTypes[0];
if (parameterType.equals(String.class)) {
params[0] = value;
return new TwoTuple<>(method,params);
} Boolean ok = true; // 看看int是否可以转换
String name = parameterType.getName();
if (name.equals("java.lang.Integer")
|| name.equals("int")){
try {
params[0] = Integer.valueOf(value);
}catch (NumberFormatException e){
ok = false;
e.printStackTrace();
}
// 看看 long 是否可以转换
}else if (name.equals("java.lang.Long")
|| name.equals("long")){
try {
params[0] = Long.valueOf(value);
}catch (NumberFormatException e){
ok = false;
e.printStackTrace();
}
// 如果int 和 long 不行,那就只有尝试boolean了
}else if (name.equals("java.lang.Boolean") ||
name.equals("boolean")){
params[0] = Boolean.valueOf(value);
} if (ok){
return new TwoTuple<Method,Object[]>(method,params);
}
}
return null;
}
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完了后,也会写别的内容。 同时,最近在准备面试,也会分享些面试内容。
曹工说Tomcat1:从XML解析说起的更多相关文章
- 曹工说Tomcat3:深入理解 Tomcat Digester
一.前言 我写博客主要靠自己实战,理论知识不是很强,要全面介绍Tomcat Digester,还是需要一定的理论功底.翻阅了一些介绍 Digester 的书籍.博客,发现不是很系统,最后发现还是官方文 ...
- 曹工说Spring Boot源码(6)-- Spring怎么从xml文件里解析bean的
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 曹工说Spring Boot源码(7)-- Spring解析xml文件,到底从中得到了什么(上)
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 曹工说Spring Boot源码(8)-- Spring解析xml文件,到底从中得到了什么(util命名空间)
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 曹工说Spring Boot源码(9)-- Spring解析xml文件,到底从中得到了什么(context命名空间上)
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- # 曹工说Spring Boot源码(10)-- Spring解析xml文件,到底从中得到了什么(context:annotation-config 解析)
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 曹工说Spring Boot源码(12)-- Spring解析xml文件,到底从中得到了什么(context:component-scan完整解析)
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 曹工说Spring Boot源码(15)-- Spring从xml文件里到底得到了什么(context:load-time-weaver 完整解析)
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
- 曹工说Spring Boot源码(16)-- Spring从xml文件里到底得到了什么(aop:config完整解析【上】)
写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...
随机推荐
- 怎样一步一步删除(linux & UNIX)环境下 oracle 11g 集群节点
Deleting a Cluster Node on Linux and UNIX Systems 1.确定要删除的节点,是否active,pinned $ olsnodes -s -t 假设 ...
- DLL动态库的创建,隐式加载和显式加载
动态库的创建 打开VS,创建如下控制台工程,工程命名为DllTest: 在弹出的对话框中选择"DLL"后单击"完成"按钮: 在工程中新建DllTest.h和Dl ...
- WPF多点触摸放大缩小旋转
原文:WPF多点触摸放大缩小旋转 版权声明:本文为博主原创文章,需要转载尽管转载. https://blog.csdn.net/z5976749/article/details/40118437 如果 ...
- Python 推断素数
a = raw_input() #输入数字 a = int(a) #铸造成int b=True #的标记 for i in range(2,a): #从2开始循环本身 if a%i==0: #除了自己 ...
- H3C交换机配置ACL禁止vlan间互访
1.先把基础工作做好,就是配置VLAN,配置Trunk,确定10个VLAN和相应的端口都正确.假设10个VLAN的地址分别是192.168.10.X,192.168.20.X......192.168 ...
- WPF DataGrid自定义列DataGridTextColumn.ElementStyle和DataGridTemplateColumn.CellTemplate
<Window x:Class="DataGridExam.MainWindow" xmlns="http://schemas.microsoft.c ...
- C# Winform制作虚拟键盘,支持中文
原文:C# Winform制作虚拟键盘,支持中文 最近在做一个虚拟键盘功能,代替鼠标键盘操作,效果如下: 实现思路: 1 构建中文-拼音 数据库, ...
- 【WPF】右下角弹出自定义通知样式(Notification)——简单教程
原文:[WPF]右下角弹出自定义通知样式(Notification)--简单教程 1.先看效果 2.实现 1.主界面是MainWindow 上面就只摆放一个Button即可.在Button的点击事件中 ...
- Thinkphp模板开放给第三方编辑权限时,如何禁止模板使用php代码
首先我要吐槽一个问题:为什么在博客园发布的文章总是被其他网站采集过去,而他们采集过去后,排名比博客园还好,比如这篇文章,我把标题复制到百度搜索,结果第一页的搜索结果全部都是采集我的,而我在博客园发布的 ...
- 给Delphi程序添加版本信息(EXE和Dll)
我们在用Delphi编译完程序,准备发布产品时,总希望随产品发布个性信息以标示产品的来源以及开发者等信息,就像windows的程序一样,使我们一看属性就知道他是微软的产品,这些在Delphi中是如何实 ...