蘑菇君深入源码学习Tomcat系列 - Tomcat与Servlet的那些事

Posted 蘑菇君520

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了蘑菇君深入源码学习Tomcat系列 - Tomcat与Servlet的那些事相关的知识,希望对你有一定的参考价值。

瞎扯淡

最近很焦虑,每天过着咸鱼般的生活,感觉前途渺茫。再这么下去,整个人就真成咸鱼了。焦虑来源于日复一日工作中,自己变得越来越麻木,不会动脑思考。憋说举一反三了,脑子多转一下都感觉要耗尽全身气力。

焦虑之余,平时也会看各种技术文章。无论是HashMap, ReentrantLock, 还是Redis, Kafka, dubbo, 我都看的有模有样。最喜欢Spring, 兼容并包, 等到睡上一觉,已被我全都忘掉。若是有人问到,请自己网上去找~ヾ(o・ω・)ノ

反而三年前写的关于android控件源码解析的文章,自己还印象深刻。时不时的收到有人给我点赞的邮件,还会嘿嘿一笑。

所以我打算狠狠心,把打王者的时间用在看源码和写博客上。我倒是要看看,一年以后,我的王者水平会不会掉到青铜。╮(╯﹏╰)╭

既然目的是看源码学习思想,也没有必要挑那些复杂的最新的技术(怕看不懂,受到打鸡…),那就挑一个工作中开发都会用到的,这样既能学习思想,又能解决实际问题。所以我挑了Tomcat,这个每天都要启动N遍,又较为底层的开源Web容器。

Tomcat是什么

想起第一次接触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容器,直接由应用本身就能用快速启动容器运行。

Servlet规范

刚刚说到Tomcat实现了Servlet规范,那我们在看Tomcat源码前,得先了解一下Servlet规范是什么。

抛开Servlet,如果是我来实现一个Web容器,我会怎么做?

  1. 首先肯定要有个HTTP解析模块帮我们将HTTP请求转换成Java类,比如Request, Response

  2. 再来个模块处理这些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...


呐,那这么写就肯定被画圈圈诅咒的。这种高耦合低内聚的方式是个违背面向对象设计的典型例子。

理想情况下,应该是

  1. 有个老大哥来接收并调度请求
  2. 很多小老弟,也就是不同的java类来处理某个模块或联系紧密的请求
  3. 可以配置不同请求和处理的java类之间的映射逻辑

再回到Servlet规范,Servlet就是我们上面说到的处理请求的java类,Servlet容器就是那个老大哥。老大哥Servlet容器会将请求根据映射规则转发到具体的Servlet,在Servlet里再处理具体业务。可以参考下图:

Servlet Api

我们来从代码层面上来看看Servlet的规范,可以直接看javax.servlet:javax.servlet-apijar包。

这里以3.1.0版本的servlet api举例,其主要由5个部分组成:

  1. javax.servlet包:里面是被servlet和web容器使用到的接口和类。与特定的协议无关。也就是说Servlet其实与协议是解耦的,除了HTTP协议,同样可以搭配其他协议一起使用。

  2. java.servlet.http包:虽然servlet与协议解耦,但最常用的还是与HTTP协议搭配使用。

  3. java.servlet.jsp包: JSP相关的接口和类,暂时不管它。

  4. java.servlet.annotation包:提供了注解形式的映射方式,就像Spring提供的@Controller@RequestMapping一样。

  5. java.servlet.websocket:WebSocket相关的接口和类,暂时不管它。

Servlet相关接口

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方法,具体的业务处理逻辑就应该放在这个方法里。其中两个形参ServletRequestServletResponse用于封装请求和响应信息。这里要注意的是这些接口都与协议无关。

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);
    
    // ...

同时,我们还看到两个跟生命周期有关的方法initdestroy,这是个贴心又残酷的设计。 Servlet的实现类将会是个莫得灵魂的类,它的一生被Servlet容器安排的明明白白的。它能做到的仅仅是在init方法里初始化一些资源,并在destroy方法里释放这些资源。这就跟Spring管理的Bean一样,也跟Android开发里的Activity一样,都是由外部控制这些类的生命周期,自己等着被调用即可。

再看init方法的形参ServletConfig这个接口,顾名思义,推测是Servlet容器初始化Servlet时传递过来的参数配置。作为Tomcat经验丰富的…使用者,立马就能想到ServletConfig里存的肯定是从web.xml里解析出来的配置:

<servlet>
    <servlet-name>WebApp</servlet-name>
    <servlet-class>wang.mogujun.DemoServlet</servlet-class>
    <init-param>
      <param-name>username</param-name>
      <param-value>mogujun</param-value>
    </init-param>
</servlet>

打开ServletConfig去看确实如此,

public interface ServletConfig 

    public String getServletName();
    // 推测是用于获取web.xml里配置的init-param结点里的数据
    public String getInitParameter(String name);

HttpServlet相关接口

再来看看跟我们编写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协议相关的各种方法,如doGetdoPost,一看就知道是处理GET和POST方式的请求。同时,这些方法不是抽象方法,Web开发者只需要override想要支持的请求方式对应的方法即可。

再来看看HTTP协议中的请求和响应对应的HttpServletRequestHttpServletResponse接口:

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们去处理。Servlet规范里有个ServletContext接口,就是这个老大哥,他代表着一个Web应用程序,负责管理Web应用程序里大大小小的事。

public interface ServletContext 

    public ServletRegistration.Dynamic addServlet(String servletName, String className);
    
    public <T extends Servlet> T createServlet(Class<T> clazz) throws ServletException;
    
    public FilterRegistration.Dynamic addFilter(String filterName, String className);
        
    public void addListener(String className);    
    
    // ...


除了Servlet相关的接口,还有两个重要的接口addFilteraddListener

看到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

以上是关于蘑菇君深入源码学习Tomcat系列 - Tomcat与Servlet的那些事的主要内容,如果未能解决你的问题,请参考以下文章

详解Tomcat系列-从源码分析Tomcat的启动

Tomcat源码解读系列——server.xml文件的配置

Day680.大佬如何学习源码 -深入拆解 Tomcat & Jetty

怎么读 Tomcat 源码?

Tomcat7源码分析学习系列之一-----tomcat的启动文件startup的注释

Spring JDBC的优雅设计 - 异常封装(下)