最近很焦虑,每天过着咸鱼般的生活,感觉前途渺茫。再这么下去,整个人就真成咸鱼了。焦虑来源于日复一日工作中,自己变得越来越麻木,不会动脑思考。憋说举一反三了,脑子多转一下都感觉要耗尽全身气力。
焦虑之余,平时也会看各种技术文章。无论是HashMap, ReentrantLock, 还是Redis, Kafka, dubbo, 我都看的有模有样。最喜欢Spring, 兼容并包, 等到睡上一觉,已被我全都忘掉。若是有人问到,请自己网上去找~ヾ(o・ω・)ノ
反而三年前写的关于Android控件源码解析的文章,自己还印象深刻。时不时的收到有人给我点赞的邮件,还会嘿嘿一笑。
所以我打算狠狠心,把打王者的时间用在看源码和写博客上。我倒是要看看,一年以后,我的王者水平会不会掉到青铜。╮(╯﹏╰)╭
既然目的是看源码学习思想,也没有必要挑那些复杂的最新的技术(怕看不懂,受到打鸡…),那就挑一个工作中开发都会用到的,这样既能学习思想,又能解决实际问题。所以我挑了Tomcat,这个每天都要启动N遍,又较为底层的开源Web容器。
想起第一次接触Tomcat,还是我五岁那年,那时他还很年轻,还有一个cp叫杰瑞…
咳咳,错了错了。第一次接触Tomcat大概是大一吧,有一门课程就是学Web程序设计。那个时候按照书本操作,写了个jsp页面,放到Tomcat里运行起来。 打开浏览器,输入localhost:8080
这串神秘代码,看到了"Hello World",那个时候感觉自己离淘宝,离马云只有一步之遥了。
那Tomcat究竟是什么呢?
long long ago, Web应用主要用于浏览器新闻等静态页面。那个时候只需要服务端有一个能解析Http协议,返回HTML给浏览器的应用程序即可,称作HTTP服务器。但是现在呢,一个页面,没有点热门排行,没有点个性化推荐小广告,都不好意思见人。这些动态结果,就需要服务端经过一定的逻辑处理,再生成用户需要的页面信息,让HTTP服务器返回给浏览器。
所以除了HTTP解析工具外,还需要一套扩展机制去调用其他业务逻辑来生成最终的返回结果。Sun公司推出了Servlet技术,用于规范Java语言的这种服务端扩展机制。我们可以把Servlet简单理解为运行在服务端的Java程序,但是Servlet不能独立运行,必须把它部署到Servlet容器中,由容器来调用。Tomcat就是实现了Servlet规范的Servlet容器,同时也具备HTTP服务器的功能,我们称这种应用程序为Web容器。
现在微服务大行其道,都喜欢将一个大的应用拆分成一个个功能独立单一的小应用,进行快速部署。在这个过程中,后端应用数量必然要大大增加,每个应用都需要运行在一个独立的Web容器里。所以,为了减少资源消耗,我们希望Web容器也尽可能消耗少的CPU和内存资源。而Tomcat就是一个轻量级且稳定的Web容器。同时,Tomcat本身也是Spring Boot默认的内嵌Web容器,直接由应用本身就能用快速启动容器运行。
刚刚说到Tomcat实现了Servlet规范,那我们在看Tomcat源码前,得先了解一下Servlet规范是什么。
抛开Servlet,如果是我来实现一个Web容器,我会怎么做?
首先肯定要有个HTTP解析模块帮我们将HTTP请求转换成Java类,比如Request, Response
再来个模块处理这些Request,根据不同Request内容返回不同的Response。最简单的方式就是:
public Response handleRequest(String url) {
if ("/mogujun/handsome/get".equals(request.getUrl())) {
return noFace();
}
else if ("/mogujun/money/get".equals(request.getUrl())) {
return empty();
}
省略一万个if...
}
呐,那这么写就肯定被画圈圈诅咒的。这种高耦合低内聚的方式是个违背面向对象设计的典型例子。
理想情况下,应该是
再回到Servlet规范,Servlet就是我们上面说到的处理请求的java类,Servlet容器就是那个老大哥。老大哥Servlet容器会将请求根据映射规则转发到具体的Servlet,在Servlet里再处理具体业务。可以参考下图:
我们来从代码层面上来看看Servlet的规范,可以直接看javax.servlet:javax.servlet-api
jar包。
这里以3.1.0版本的servlet api举例,其主要由5个部分组成:
javax.servlet包:里面是被servlet和web容器使用到的接口和类。与特定的协议无关。也就是说Servlet其实与协议是解耦的,除了HTTP协议,同样可以搭配其他协议一起使用。
java.servlet.http包:虽然servlet与协议解耦,但最常用的还是与HTTP协议搭配使用。
java.servlet.jsp包: JSP相关的接口和类,暂时不管它。
java.servlet.annotation包:提供了注解形式的映射方式,就像Spring提供的@Controller
和@RequestMapping
一样。
java.servlet.websocket:WebSocket相关的接口和类,暂时不管它。
Servlet里定义了5个方法:
public interface Servlet {
// 周期方法:servlet容器必须在Servlet接收任意请求前,先调用这个方法初始化Servlet
public void init(ServletConfig config) throws ServletException;
// 封装Servlet初始化参数,这些参数可以从web.xml里解析得到
public ServletConfig getServletConfig();
// servlet容器会调用service方法来让此servlet来响应请求
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
// 获取Servlet的一些额外信息,作者啊,版本啊等等
public String getServletInfo();
// 周期方法:回收时会被调用
public void destroy();
}
最重要的是service
方法,具体的业务处理逻辑就应该放在这个方法里。其中两个形参ServletRequest
和ServletResponse
用于封装请求和响应信息。这里要注意的是这些接口都与协议无关。
public interface ServletRequest {
public String getCharacterEncoding();
public int getServerPort();
// 获取输入流,从请求中读取数据
public ServletInputStream getInputStream() throws IOException;
// ...
}
public interface ServletResponse {
// 获取输出流,从响应中输出数据到客户端
public ServletOutputStream getOutputStream() throws IOException;
public void setCharacterEncoding(String charset);
// ...
}
同时,我们还看到两个跟生命周期有关的方法init
和destroy
,这是个贴心又残酷的设计。 Servlet的实现类将会是个莫得灵魂的类,它的一生被Servlet容器安排的明明白白的。它能做到的仅仅是在init
方法里初始化一些资源,并在destroy
方法里释放这些资源。这就跟Spring管理的Bean一样,也跟Android开发里的Activity一样,都是由外部控制这些类的生命周期,自己等着被调用即可。
再看init
方法的形参ServletConfig
这个接口,顾名思义,推测是Servlet容器初始化Servlet时传递过来的参数配置。作为Tomcat经验丰富的…使用者,立马就能想到ServletConfig
里存的肯定是从web.xml里解析出来的配置:
WebApp
wang.mogujun.DemoServlet
username
mogujun
打开ServletConfig
去看确实如此,
public interface ServletConfig {
public String getServletName();
// 推测是用于获取web.xml里配置的init-param结点里的数据
public String getInitParameter(String name);
}
再来看看跟我们编写Web程序息息相关的HttpServlet相关接口。
看HttpServlet
, HttpServlet
是一个抽象类,继承自抽象类GenericServlet
。为啥子这里不用接口,要用两个抽象类了呢?
主要是Servlet顶层接口太形而上了,并不能复用一些通用的逻辑。正所谓,天上规范在天上飞,地上抽象类来追。而GenericServlet
就是为所有的Servlet实现类提供了一些通用的代码。
在落地到某个具体的协议实现,比如HTTP,那肯定得转化成一套更接地气的接口暴露出去:
public abstract class HttpServlet extends GenericServlet {
// 1. 实现Servlet的service方法
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
if (req instanceof HttpServletRequest && res instanceof HttpServletResponse) {
// 2. 转化成HTTP协议相关的封装类
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
this.service(request, response);
} else {
throw new ServletException("non-HTTP request or response");
}
}
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 3. 根据请求的方式,调用不同的处理逻辑
String method = req.getMethod();
if (method.equals("GET")) {
this.doGet(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
}
// ...
}
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
// 子类要是不覆盖实现,就默认返回错误
if (protocol.endsWith("1.1")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
...
}
// ...
}
HttpServlet
抽象类实现Servlet
的接口,将其转化成跟HTTP协议相关的各种方法,如doGet
和doPost
,一看就知道是处理GET和POST方式的请求。同时,这些方法不是抽象方法,Web开发者只需要override想要支持的请求方式对应的方法即可。
再来看看HTTP协议中的请求和响应对应的HttpServletRequest
和HttpServletResponse
接口:
public interface HttpServletRequest extends ServletRequest {
public String getRequestURI();
public Cookie[] getCookies();
public String getHeader(String name);
public String getMethod();
// ...
}
public interface HttpServletResponse extends ServletResponse {
public void addCookie(Cookie cookie);
public void setStatus(int sc, String sm);
public void sendRedirect(String location) throws IOException;
// ...
}
上面的接口方法是不是一看就很亲切,都是HTTP协议相关的内容。(什么,不亲切?不亲切还不快去看HTTP协议, ̄へ ̄)
上面提到Servlet容器相当于老大哥,对外接收请求,对内转发给小老弟Servlet们去处理。Servlet规范里有个ServletContext
接口,就是这个老大哥,他代表着一个Web应用程序,负责管理Web应用程序里大大小小的事。
public interface ServletContext {
public ServletRegistration.Dynamic addServlet(String servletName, String className);
public T createServlet(Class clazz) throws ServletException;
public FilterRegistration.Dynamic addFilter(String filterName, String className);
public void addListener(String className);
// ...
}
除了Servlet相关的接口,还有两个重要的接口addFilter
和addListener
。
看到Filter这个词,我立马就想到责任链模式,这也是各种复杂框架里常用的方式。框架给我们定义好了一整套流程,我们只要实现某个接口就能完成某一套标准流程。然而,凡事总有例外,每个人都有自己作妖的方式。为了保证一定的灵活性,就可以通过过滤器去干预标准流程,比如处理请求前进行登录验证,统计记录请求到log日志等等。
public interface Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;
}
public interface FilterChain {
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException;
}
Servlet容器创建各种Filter,并将它们链接成一个FilterChain,就能在Servlet处理前和处理后分别加上特定的逻辑,干预标准流程。
再看addListener
,是不是很熟悉的感觉,注册监听器,一看就是观察者模式。这就跟我们监听按钮事件,回调我们listener处理点击事件一个道理。Servlet容器在运行中状态会不停改变,会触发各种事件,比如Web应用的启动和关闭,有请求到达等等。我们可以注册这些事件的监听器来做出处理。
Filter和Listener机制都是为了系统的扩展需要。Filter是为了干预流程,Listener是为了响应状态的变更,一般不会影响流程走向。
好,终于磨叽完了。总结一下,今天这篇文章主要是简单介绍一下Tomcat是什么:作为HTTP服务器 与Servlet容器的结合,提供运行环境给我们的Web应用程序,让我们只需要专注于业务逻辑的处理。
我们又看了Servlet规范的核心内容和设计思想,作为Tomcat在实现上的依据。
在通过源码方式研究一个系统或框架时,蘑菇君觉得先不急着直接去看源码实现。可以先自己去构思一下如何设计模块,(就算划分不清楚,看起来很low也没关系,谁一开始不是个(宝ᴗ宝)),
再通过系统里接口的注释和方法,来快速了解这个系统或框架模块的设计。在这个过程中,先忽略细枝末节,结合对比自己的设计思考,看看大神们是如何洞察全局,高屋建瓴。在鸟瞰全图之后,再信仰之跃,淹死在代码的海洋里_|\○_。
下一篇就该真正的深入到Tomcat源码里了。债见~
我是蘑菇君,从今天开始,肥宅,看源码,写博客,做一个充实(苦逼)爱思考(咸鱼)的人。
极客时间《深入拆解Tomcat & Jetty》
https://www.javatpoint.com/servlet-api