一, 前言
第一次被人喊曹工, 我相当诧异, 那是有点久的事情了, 楼主 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>
- </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");
- 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);
- }
- }
- 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();
- setProperties(attributes,coder);
- stack.push(coder);
- } else if ("Girl".equals(qName)) {
- Girl girl = new Girl();
- 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;
- }
- 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 完了后, 也会写别的内容. 同时, 最近在准备面试, 也会分享些面试内容.
来源: https://www.cnblogs.com/grey-wolf/p/11105744.html