Spring源码学习 | 0 没有Spring的Web程序

726 阅读7分钟

前言

最近在学习Spring源码,断断续续的看了一阵子,感觉学的有些零散。好记性不如烂笔头,想要通过写系列文章的形式,来记录自己学习的心得体会。计划是从Spring核心的core、bean、context开始,逐渐到SpringBoot相关内容。希望自己能够坚持。

当下Java开发绕不过的一个东西就是Spring,绝大多数的Java应用都是基于Spring的开发。经过多年的发展,Spring生态内包含的内容也越来越多。网上各种教程一上来大都也是SSM开始,或是流行的SpringBoot。越来越高度的封装带来开发便利。

但是高度封装对学习原理并不是很友好,很多深层的东西都隐藏在层层代码中,让人不容易探寻,常常让人只知其然不知其所以然。我希望自己能够从最简单的内容开始,逐步学习,逐步抽丝剥茧,能真正理解、掌握Spring源码,

背景知识

Tomcat

Tomcat是常见的轻量应用服务器,它的结构可以简单化为Web服务器、Web容器、静态资源几部分。它主要负责三件事:

  • 接收请求
  • 处理请求
  • 响应请求

其中,Web服务器主要负责接收请求、响应请求;Web容器中的Servlet则是负责处理请求,完成具体的业务逻辑。当然Tomcat内容不可能这么简单,但是我们可以先简单的这样理解,主要是明白Servlet在其中的作用。

web.xml

web.xml通常位于webapp/WEB-INF/,用以描述组成应用程序的servlet和其他组件、以及相关初始化参数等信息。常见的配置项有:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <!--监听器,会在一定条件下得到通知,触发逻辑-->
    <listener>
        <listener-class>cn.dingyufan.learnspring.servletxmldemo.HelloListener</listener-class>
    </listener>

    <!--上下文参数-->
    <context-param>
        <param-name>myName</param-name>
        <param-value>dingyufan</param-value>
    </context-param>

    <!--过滤器,url-pattern都匹配的情况下,按自上而下的顺序依次过滤-->
    <filter>
        <filter-name>randomFilter</filter-name>
        <filter-class>cn.dingyufan.learnspring.servletxmldemo.RandomFilter</filter-class>
    </filter>
    <!--过滤器的匹配规则-->
    <filter-mapping>
        <filter-name>randomFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!--servlet-->
    <servlet>
        <servlet-name>helloServlet</servlet-name>
        <servlet-class>cn.dingyufan.learnspring.servletxmldemo.HelloServlet</servlet-class>
    </servlet>
    <!--servlet对应的匹配规则,可以让应用服务器知道选择哪个Servlet-->
    <servlet-mapping>
        <servlet-name>helloServlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

</web-app>

在应用服务器读取web.xml后,对其中内容的处理也是有顺序的:

  1. 首先为应用(application)创建自己的ServletContext,并将context-param存入ServletContext
  2. 创建listener实例,并在相应时机发出通知。常见的有ServletContextListener的实现类,会在ServletContext创建后得到通知,调用contextInitialized(event)方法
  3. 创建filter实例
  4. Servlet实例并不会在刚开始就初始化,而是在第一次接受到请求时初始化并相应请求。需要注意的是,即使请求被过滤器拦截了,这个Servlet还是会初始化实例。

Servlet

说了这么多,终于到了今天的主角Servlet。很多人可能对Servlet只有一个模糊的概念,大概知道是怎么回事,但是很难描述。但是其实Servlet很简单,它本质上是在服务器软件中跑的一个程序

我们来看javax.servlet包中Servlet接口源码,源码注释中描述的是:

**A servlet is a small Java program that runs within a Web server. **Servlets receive and respond to requests from Web clients, usually across HTTP, the HyperText Transfer Protocol.

Servlet接口定义了五个所有Servlet必须实现的方法

public interface Servlet {
    // 初始化Servlet调用
    public void init(ServletConfig config) throws ServletException;
    
    // 获取Servlet相关的配置,如初始化参数等
    public ServletConfig getServletConfig();
    
    // Servlet具体的业务逻辑方法
    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
    
    // 返回Servlet信息,如作者、版本等
    public String getServletInfo();
    
    // Servlet销毁时调用
    public void destroy();
}

将上面的内容连起来看,我们想要通过Servlet来写一个web服务需要的步骤有:

  • 编写Servlet实现类
  • 在web.xml中对Servlet进行配置
  • 服务部署在tomcat中

完成上述步骤,启动Tomcat,发出请求。那么我们编写的Servlet就会存在于Tomcat的Web容器当中,提供处理请求的功能。那下面我们动手实践一下。

Servlet实战

开发环境

  • AdoptOpenJDK 11(hotspot)
  • Tomcat 9
  • IntelliJ IDEA

注意这个Tomcat版本,如果你使用的是Tomcat 10.0.x或更加新的版本,会发现程序运行时缺少javax.servlet相关内容。因为Tomcat在Tomcat 10.0.x开始使用jakarta.servlet替换了javax.servlet。 同样的,Tomcat版本也会限制你的Servlet版本和Java版本。

image-20210823154041939

搭建项目框架

如果你想要直接获得完整的项目,你可以直接从Gitee获取:servlet-xml-demo

在IDEA中创建一个Maven项目,servlet-xml-demo,pom文件中添加依赖。完整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>cn.dingyufan.learnspring</groupId>
    <artifactId>servlet-xml-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>servlet-xml-demo</name>
    <packaging>war</packaging>

    <properties>
        <maven.compiler.target>11</maven.compiler.target>
        <maven.compiler.source>11</maven.compiler.source>
    </properties>

    <dependencies>

        <!--Tomcat 10使用-->
        <!--
        <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>5.0.0</version>
            <scope>provided</scope>
        </dependency>
        -->

        <!--为兼容SpringMVC,还是使用javax.servlet。截至5.2.12.RELEASE版本,SpringMVC仍不支持jakarta.servlet-->
        <!--Tomcat 9使用-->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>4.0.1</version>
            <scope>provided</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.3.0</version>
            </plugin>
        </plugins>
    </build>
</project>

可以看到,其实只引入了一个依赖包 javax.servlet-api。

然后创建webapp/WEB-INF/web.xml,补全项目结构。完整文件目录如下

image-20210823155134733

web.xml初始内容如下,如果没有可以自己拷贝进去

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
         
</web-app>

到这一步,我们需要修改一下module settings,在Facets中配置好Deployment Descriptors和Web Resource Directories。前面也说过web.xml是描述部署的参数、组件的,这里Deployment Descriptors就是指定到我们的web.xml文件。

image-20210823155323399

编写Servlet

package cn.dingyufan.learnspring.servletxmldemo;


import jakarta.servlet.*;
import java.io.IOException;

public class HelloServlet implements Servlet {

    public void init(ServletConfig config) throws ServletException {
        System.out.println("HelloServlet.init()");
    }

    public ServletConfig getServletConfig() {
        return null;
    }

    public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        // 这里可以写具体的业务逻辑,数据读取等。demo里是向页面输出一段文字,
        res.getWriter().append("HelloServlet!").close();
    }

    public String getServletInfo() {
        return null;
    }

    public void destroy() {

    }
}

配置web.xml

配置<servlet>与<servlet-mapping>。

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <servlet>
        <servlet-name>helloServlet</servlet-name>
        <servlet-class>cn.dingyufan.learnspring.servletxmldemo.HelloServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>helloServlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>

</web-app>

配置Tomcat

在IDEA的Run/Debug Configurationstans弹窗配置Tomcat

image-20210823155525685

并在Deployment Tab页添加Artifact。注意Tab页下方的Application context配置。

image-20210823155608608

运行

运行程序,Postman发出http请求,访问localhost:8080。可以成功看到response输出的消息。

image-20210823155742006

其实到这里就已经成功用Servlet来实现web服务了。想要更多、更复杂的业务,就可以写更多的Servlet。同样的,javax包中也有一些封装过的功能更加丰富的如HttpServlet等,可以更加方便的进行开发。

上面介绍web.xml的时候,提到了一些常见的组件,我们这里也都来实现一下。

编写Listener

我们来编写一个Listener,在ServletContext创建完成之后,打印web.xml中配置的数据。

先看接口ServletContextListener的源码,这个接口有两个默认空方法。实现类可以通过重写来实现想要的逻辑。

public interface ServletContextListener extends EventListener {
    // Servlet上下文初始化后调用
    default public void contextInitialized(ServletContextEvent sce) {}
    // Servlet上下文销毁后调用
    default public void contextDestroyed(ServletContextEvent sce) {}
}

接下来是自己写一个实现类,读取myName参数的值。

package cn.dingyufan.learnspring.servletxmldemo;

import jakarta.servlet.ServletContextEvent;
import jakarta.servlet.ServletContextListener;

public class HelloListener implements ServletContextListener {
    public void contextInitialized(ServletContextEvent sce) {
        String paramValue = sce.getServletContext().getInitParameter("myName");
        System.out.println("HelloListener.contextInitialized(), paramValue=>" + paramValue);
    }
}

编写Filter

javax.servlet包中的Filter接口有init、destroy方法是默认空方法,也是提供实现类在过滤器初始化、销毁时可以进行一些处理。doFilter方法是过滤器的核心方法,就是在这个方法里,决定是过滤还是放行。如果放行,就把请求交给FilterChain中的下一个过滤器。

public interface Filter {

    default public void init(FilterConfig filterConfig) throws ServletException {}
   
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException;

    default public void destroy() {}
}

我们来实现一个随机数奇偶过滤,如果随机数是奇数则过滤,偶数则放行。因为我们在过滤器销毁时不做其他处理,所以我们只实现init、doFilter方法。

package cn.dingyufan.learnspring.servletxmldemo;

import jakarta.servlet.*;

import java.io.IOException;
import java.util.Random;

public class RandomFilter implements Filter {

    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("RandomFilter.init()");
    }

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        int random = new Random().nextInt(9);
        System.out.println("RandomFilter.doFilter(),random=>" + random);
        // 判断随机数是否为奇数
        if ((random & 1) == 1) {
            response.getWriter().append("NO ENTRY!").close();
            return;
        }
        // 放行,进入下一个过滤器
        chain.doFilter(request, response);
    }
}

把监听器、过滤器配置到web.xml中,完整的web.xml如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">

    <listener>
        <listener-class>cn.dingyufan.learnspring.servletxmldemo.HelloListener</listener-class>
    </listener>

    <context-param>
        <param-name>myName</param-name>
        <param-value>dingyufan</param-value>
    </context-param>

    <filter>
        <filter-name>randomFilter</filter-name>
        <filter-class>cn.dingyufan.learnspring.servletxmldemo.RandomFilter</filter-class>
    </filter>

    <filter-mapping>
        <filter-name>randomFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>


    <servlet>
        <servlet-name>helloServlet</servlet-name>
        <servlet-class>cn.dingyufan.learnspring.servletxmldemo.HelloServlet</servlet-class>
    </servlet>

    <servlet-mapping>
        <servlet-name>helloServlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

再次运行

启动Tomcat,我们可以看到:HelloListener中的contextInitialized方法被调用,并且打印出了web.xml中的context-param参数的值;RandomFilter的init被调用;HelloServlet的init方法则未被调用。

image-20210823160202175

我们通过Postman对Servlet发出请求

image-20210823161042227

可以看到HelloServlet这时候才发生初始化,并响应了请求。同时看到请求也经过RandomFilter,未被拦截。

image-20210823161154693

到这里,我们实现了一个简单的web程序,包含Servlet业务处理、Filter过滤器以及Listener事件监听。通过这个三个组件的组合使用,可以实现更丰富更强大的web程序。

纯注解开发

从Servlet 3.0开始,就支持使用注解的形式进行Servlet开发,不再需要繁复的web.xml。运行和xml效果相同,有兴趣的可以看servlet-annotation-demo ,不在此赘述。

结语

为什么Spring源码学习的系列文章要从Servlet开始呢?因为接下来会讲引入Spring后,我们怎么来写一个Web服务,会看Spring是怎么和web容器结合的。Spring的启动入口在哪里,为什么Tomcat启动了Spring程序就可以访问了。然后进一步从源码层面开始学习Spring。