学内存马前就要来学一学java web三大件的相关原理:Servlet、Filter、Listener

参考文章:https://www.cnblogs.com/jadite/p/16951328.html

Servlet

Servlet是什么

Servlet是JavaEE规范(接口)之一;
Servlet是运行在服务器(Web容器Tomcat等)上的一个 java 小程序,它用来接收客户端发送过来的请求进行处理,并响应数据给客户端。
Servlet及相对的对象,都由Tomcat创建,我们只是使用。

Tomcat就是一个servlet容器

Servlet需要完成3个任务:

  1. 接收请求:将客户端发送过来的请求封装成ServletRequest对象(包含请求头、参数等各种信息)
  2. 处理请求:在service方法中接收参数,并且进行处理请求。
  3. 数据响应:请求处理完成后,通过转发(forward)或者重定向(redirect)到某个页面。

Servlet程序实现

  1. 实现Servlet接口,重新service方法
  2. 在web.xml或者用注解配置映射

Servlet生命周期

  1. 执行 Servlet 构造器方法
    第一步,在web.xml中的servlet中配置 load-on-startup 的值 ≥ 0 时,表示应用启动时就创建这个servlet。否则,第一次访问的时候调用。
  2. 执行 init 初始化方法
    第二步,第一次访问的时候调用。
  3. 执行 service 方法
    第三步,每次访问都会调用。
  4. 执行 destroy 销毁方法
    第四步,在 web 工程停止的时候调用。

ServletConfig

它是Servlet程序的配置信息类

它的三大作用:

  1. 获取web.xml 中 Servlet 程序的别名 servlet-name 的值
  2. 获取web.xml 中 Servlet 程序的获取初始化参数 init-param
  3. 获取 ServletContext 对象

ServletConfig

  1. 每个web项目只有一个ServletContext对象,在web工程部署启动的时候创建,在工程停止的时候关闭。
  2. ServletContext 对象是一个域对象(可以像Map一样存储数据的对象。域指的是作用域,这里是整个web工程)。

ServletContext 类的四个作用:

  1. 获取 web.xml 中配置的上下文参数 context-param
  2. getContextPath()获取当前的工程路径,格式: /工程路径
  3. getRealPath()获取工程部署后在服务器硬盘上的绝对路径
  4. 像 Map 一样存取数据

HttpServletRequest和HttpServletResponse

HttpServletResponse继承了ServletRequest,HttpServletResponse继承了ServletResponse,他们两个都是接口,所以我们在doGet或者doPost的时候传入的肯定是他们的实现类,而这个实现类是由tomcat创建的,封装了请求和响应的信息,到下面讲tomcat的时候再串起来细说。

Filter

Filter 是JavaEE规范(接口)之一;
Filter 过滤器它的作用是:拦截请求,过滤响应。

常见应用场景:
1、权限检查
2、日记操作
3、事务管理
……等等

所以Filter的顺序是在处理请求之前进行

Filter使用

1、实现 Filter 接口,实现过滤方法 doFilter()
2、到 web.xml或者注解中去配置 Filter 的拦截路径

Filter生命周期

  1. 构造器方法
  2. init 初始化方法
    第 1,2 步,在 web 工程启动的时候执行(Filter 已经创建)
  3. doFilter 过滤方法
    第 3 步,每次拦截到请求,就会执行
  4. destroy 销毁
    第 4 步,停止 web 工程的时候,就会执行(停止 web 工程,也会销毁 Filter 过滤器)

FilterConfig

Tomcat 每次创建 Filter 的时候,也会同时创建一个 FilterConfig 类,这里包含了 Filter 配置文件的配置信息。

FilterConfig 类的作用是获取 filter 过滤器的配置内容:

  1. 获取 Filter 的名称 filter-name 的内容
  2. 获取在 Filter 中配置的 init-param 初始化参数
  3. 获取 ServletContext 对象

FilterChain

就是过滤器链,过滤器可能存在不止一个,它们执行的优先顺序由它们在web.xml中从上到下配置的filter-mapping顺序决定,与filter的配置顺序无关

特点

  1. 所有filter和目标资源默认都执行在一个线程中。
  2. 多个filter共同执行的时候,它们使用的是同一个Request对象。

拦截路径匹配规则

  • 精确匹配 /target.jsp
  • 目录匹配 /admin/*
  • 后缀名匹配 *.html

Filter只关心路径是否匹配,不关心资源是否存在,毕竟最终不是由它来处理

Listener

用于对其他对象身上发生的事件或状态改变进行监听和相应处理的对象,当被监视的对象发生情况时,立即采取相应的行动。本质是观察者模式

Servlet监听器:Servlet规范中定义的一种特殊类,它用于监听Web应用程序中的ServletContext,HttpSession 和HttpServletRequest等域对象的创建与销毁事件,以及监听这些域对象中的属性发生修改的事件。

三类监听器

image-20240908021003987

  • 域对象监听器
  • 域对象的属性域监听器
  • Session域中数据的监听器

八大监听器

  1. ServletContextListener
    监听ServletContext对象的创建与销毁

    在SpringMVC中,有个ContextLoaderListener,这个监听器就实现了ServletContextListener接口,表示对ServletContext对象本身的生命周期进行监控

  2. HttpSessionListener

    监听HttpSession对象的创建与销毁

  3. ServletRequestListener

    监听ServletRequest对象的创建与销毁

  4. ServletContextAttributeListener

    监听ServletContext中属性的创建、修改和销毁

  5. HttpSessionAttributeListener

    监听HttpSession中属性的创建、修改和销毁

  6. ServletRequestAttributeListener

    监听ServletRequest中属性的创建、修改和销毁

  7. HttpSessionBindingListener

    监听某个对象在Session域中的创建与移除

  8. HttpSessionActivationListener

    监听某个对象在Session中的序列化与反序列化。

监听器使用

  1. 实现八大监听器中的一种,重写对应方法

  2. 同样去web.xml或者用注解配置

    web.xml配置

    <listener>
    <listener-class>com.demo.listener.HelloListener</listener-class>
    </listener>

Tomcat

推荐一个视频讲得非常好(我个人觉得),把很多东西串起来了而且深入到源码层面,理解得更加清晰

视频链接:【图灵学院】终于有人把tomcat讲清楚了!Tomcat底层原理深度解析_哔哩哔哩_bilibili

Tomcat简单架构图

image-20240912201120883

找到的另一张架构图

image-20240914231538327

Tomcat源码启动

这里就搞了我好久了,看了很多文章才搞定,最后参考的是下面两篇文章

记一次tomcat源码启动控制台中文乱码问题调试过程_org.apache.catalina.startup.versionloggerlistener.-CSDN博客

idea调试tomcat源码 - huim - 博客园 (cnblogs.com)

源码下载

首先找到源码包下载,这里用的tomcat-8.5.50的版本

历史版本列表:https://archive.apache.org/dist/tomcat/tomcat-8/
源码文件夹:https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.50/src/

然后在源码根目录下添加如下pom.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.tomcat</groupId>
<artifactId>Tomcat8.0</artifactId>
<name>Tomcat8.0</name>
<version>8.0</version>

<build>
<finalName>Tomcat8.0</finalName>
<sourceDirectory>java</sourceDirectory>
<testSourceDirectory>test</testSourceDirectory>
<resources>
<resource>
<directory>java</directory>
</resource>
</resources>
<testResources>
<testResource>
<directory>test</directory>
</testResource>
</testResources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3</version>
<configuration>
<encoding>UTF-8</encoding>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.4</version>
</dependency>
<dependency>
<groupId>ant</groupId>
<artifactId>ant</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxrpc</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt.core.compiler</groupId>
<artifactId>ecj</artifactId>
<version>4.5.1</version>
</dependency>
</dependencies>
</project>

image-20240909205433170

然后配置一下合适的jdk版本,这个就不多说了

下一步配置configuration,添加一个application

image-20240909205609843

添加入口类org.apache.catalina.startup.Bootstrap

在此之前需要reload一下maven项目,不然会不识别相关的java源码

image-20240909205701250

然后我们就可以启动了

错误一

这时候会遇到第一个错误,无法解析jsp,也就是访问localhost:8080不是tomcat的首页而是返回了500,这时候需要去添加一个JSP解析器,需要我们去修改源码

找到org.apache.catalina.startup.ContextConfig类,在ConfigureStart方法下添加如下代码

context.addServletContainerInitializer(new JasperInitializer(), null);

image-20240909210146552

错误二

这时候页面访问是正常的,但是控制台的日志是乱码的,如下

image-20240909204702883

这是因为在java中, 读取文件的默认格式是iso8859-1, 而我们中文存储的时候一般是UTF-8. 所以导致读出来的是乱码。

文章中有两种方式修改乱码,我这里采用修改源码的方式去修改,就是找到读取文件的地方,转化一下编码方式,这里直接copy一下解决方案,可以自己通过调试去找到对应位置(我比较懒就不调了)

  • org.apache.tomcat.util.res.StringManager类中的getString(final String key, final Object… args)方法;添加如下代码

    try{
    value =new String(value.getBytes("ISO-8859-1"),"UTF-8");
    }catch (Exception e){
    e.printStackTrace();
    }

    image-20240909210550131

  • org.apache.jasper.compiler.Localizer类的getMessage(String errCode)方法;添加如下代码

    try{
    errMsg =new String(errMsg.getBytes("ISO-8859-1"),"UTF-8");
    }catch(Exception e){
    e.printStackTrace();
    }

    image-20240909210715069

到这里就解决完我遇到的所有问题,可以快乐调试了🫡

原理分析

我们首先要知道Tomcat是一个servlet容器。

HttpServletRequest和HttpServletResponse

image-20240911203158516

image-20240911203322536

我们知道HttpServletRequest和HttpServletResponse是一个接口,我们正常写一个servlet如下

package org.clown.servlettest;

import java.io.*;

import jakarta.servlet.http.*;
import jakarta.servlet.annotation.*;


@WebServlet(name = "helloServlet", value = "/hello-servlet")
public class HelloServlet extends HttpServlet {
private String message;

public void init() {
message = "Hello World!";
System.out.println(message);
}

public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setContentType("text/html");


// Hello
PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>" + message + "</h1>");
out.println("</body></html>");
}

public void destroy() {
System.out.println("destory "+message);
}
}

那我们调用这两个接口的方法就需要一个实现类,那这个实现类是谁创建呢,就由我们的tomcat来创建

去看tomcat的源码就可以知道RequestFacade就是其中一个实现类

image-20240911203618945

不过这个类只是一个类似门面的类,里面真正的实现是Request类,里面的方法更复杂,该类也是实现了Servlet规范的类

image-20240912105042783

这里就是给下面的分析当个引子,引发一下思考。

jar包和war包

我们在tomcat部署项目的时候,可以将web项目打包成war包然后部署到tomcat的webapps目录下

image-20240912105338467

启动tomcat的时候他就会自动解压,里面的内容如下:

image-20240912105432764

我们也可以在server.xml里面设置是否进行自动解压

image-20240912105551171

那么jar包呢?

其实jar的内容和war包解压出来是没有什么区别的,jar和war包主要是tomcat启动时用来区分这是一个依赖还是一个应用

tomcat应用的几种部署方式

部署的几种方式可以在HostConfig#deployApps中看到

image-20240912110054380

  1. 描述符部署
  2. war包部署
  3. 文件夹部署,就是将war解压的文件夹直接放到webapps下面,和war包部署没什么区别

源码中可以看到tomcat部署应用的时候是进行多线程部署的

描述符部署

描述符部署用的是<Context>标签,比如我要布置上面的应用可以在server.xml这样配置

image-20240912111717790

docBase就是应用的目录,到时候tomcat就会从该目录查找所需要的资源比如我们的class文件

Context

Context的本质上就是一个容器,源码中就有一个叫做Context的接口

image-20240912114943927

他继承自一个Container接口,我们可以去看看Container有哪些继承接口,里面就包含着tomcat的四大容器

Tomcat容器

image-20240912115147007

tomcat的四大容器:

  • Context:就是一个web应用程序,也就是我们前面配置的程序,配置在Host节点下面

  • Host:表示一个虚拟主机,一个虚拟主机下面可以有很多的应用

    image-20240912115408390

    name就是主机名,appBase就是应用目录,也就是我们为什么要放在webapps下面

  • Engine:字面意思引擎,Host是Engine的子节点

    image-20240912115730532

    在Engine里面,我们是可以定义多个虚拟主机,所以也就是我们可以将不同的应用放在不同的主机下,通过不同的主机名访问具体应用,不至于将所有应用放在localhost下面。

  • Wrapper:它实际上就封装着一个Servlet,负责管理整个Servlet的生命周期,包括装载、初始化、资源回收等。

image-20240912121655347

我们正常会继承一个HttpServlet,所有访问这个Servlet的请求是共用一个Servlet实例也就是单例模式,但如果实现SingleThreadModel接口的话就是每个请求单独拥有一个实例

整个容器的层级结构就如下:
Engine==》Host==》Context==》Wrapper==》Servlet

再讲讲为什么要多一个Wrapper,因为我们有时候会有多个Servlet实例,全放在Context下面会不好管理,所以就用Wrapper将Servlet实例按照类型管理起来,所以存储结构类似如下:

Engine:
List<Host>
Host:
List<Context>
Context
List<Wrapper> list;
Wrapper---Servlet类
List<Servlet> servlets;

Pipeline

这里也可以看一下这篇文章:https://www.cnblogs.com/coldridgeValley/p/5816414.html

pipeline翻译过来就是管道,每一个容易都有一个管道组件,pipeline里面又有valve阀门

所以上面的结构又可以优化成这样

Engine:
Pipeline:
List<valve>
List<Host>
Host:
Pipeline:
List<valve>
List<Context>
Context
Pipeline:
List<valve>
List<Wrapper> list;
Wrapper---Servlet类
Pipeline:
List<valve>
List<Servlet> servlets;

我们的Request对象想要最终servlet里面的doGet等方法,会经过前面一系列的容器、管道、阀门。

每个管道最重要的是最后一个阀门,因为他要负责将request往下一个容器进行传递,所以最后一个阀门是tomcat提前写好的。

Valve节点

该节点配置在Host节点下面,可以配置经过该阀门时需要做什么,我们只需要去实现或继承valve相关的接口或类即可自己配置

image-20240912135432620

比如下面的记录日志

image-20240912120035093

StandardWrapper

这里我们去看一下Wrapper的pipeline的最后一个valve,因为他是直接和servlet接触的;

Wrapper的实现类是StandardWrapper

image-20240912135850663

这个valve就是我们说的最后一个valve

image-20240912140039778

具体的逻辑在StandardWrapperValve的invoke方法里面,可以看到这里接受了Request和Response

我们来看一下他里面一些关键的步骤

image-20240912195619220

这里是分配servlet实例的地方,方法里的具体逻辑就不分析,知道他的作用就好

image-20240912195722497

然后这里生成了一个filterchain,将我们的request、wrapper、servelt都封装了进去,这里的filterchain就是我们前面提到Filter

image-20240912200056244

然后最终的操作就是在我们的doFilter里面进行,我们自己要建立Filter也是像写servlet一样,写一个类继承Filter相关的接口,然后在web.xml中配置,和要过滤的servlet对应起来

image-20240912200740837

后面的调用流程就自己调一下源码看看就好了

Tomcat Connector

我们在前面的Tomcat架构图可以看到有Connector组件,tomcat有两个核心功能:

1.处理 Socket 连接,负责网络字节流与 Request 和 Response 对象的转化。

2.加载和管理 Servlet,以及具体处理 Request 请求。

我们的Container组件负责内部Servlet的管理和处理过来的Request请求,那外部传过来的数据是怎么生成Request对象的呢,这靠的就是Connector组件利用Socket接受操作系统传过来的数据,然后生成Request和Response对象。

在Tomcat中有一个Connector类可以去看看他的setProtocol方法

image-20240922183030860

他这里会根据选择不同的处理类,这个protocol就是我们server.xml那里配置的

image-20240922183544328

然后HTTP1.1对应的两个处理类的区别如下:

org.apache.coyote.http11.Http11AprProtocol ---BIO
org.apache.coyote.http11.Http11NioProtocol ---NIO

那么这些类就是负责socket的连接管理和数据读取,我们可以进去看一下,这里看一下Http11NioProtocol的NIO读取数据

image-20240922202104507

这里面new了一个NioEndpoint,这就是tomcat的一个连接器,用于处理网络连接,看一下他里面的方法

image-20240922202358352

这里的Acceptor类是一个用于多线程执行任务的,因为AbstractEndpoint.Acceptor实现了Runnable接口,然后下面可以看到它accept了socket连接

那接受了socket之后就需要去处理这个socket连接,我们接着看

image-20240922203110543

这里就会调用一个setSocketOptions来处理socket,如果返回false则关闭socket,我看视频里的版本他的是porcessSocket方法(可能是bio的方法),我看了我的bio方法它使用的是processSocketWithOptions方法

image-20240922204517909

nio还没学习看不太明白,但是最终就是在这里处理了,BIO就是使用线程池的方式来读取socket数据

image-20240922204936475

这是bio的读取方式,然后继续往下跟就是一些数据解析然后构造Request对象,就不分析,有个大概概念就行。