通过技术发展理解Spring Web下的Java Web应用的处理Http请求流程
在学习 Java Web 开发时,我们可能会忽略如何使用原生 Java Servlet 和 JSP 构建 Java Web 应用并将其部署到如 Tomcat 等应用服务器中。得益于 Spring Boot 与 Spring MVC 的简化操作,创建一个 Java Web 应用变得异常简单。然而,如果不了解程序的底层运行机制,可能会影响我们对整个系统的全面理解,甚至限制进一步的深度开发。
因此,本文将梳理几种开发 Java Web 应用的方式及其之间的联系和发展方向,帮助读者更好地理解 Spring Web 在处理 HTTP 请求时的工作原理。
回到梦开始的地方
想要理解使用 Spring Boot 构建的 Java Web 应用是如何接收并处理请求的,我们首先要回顾传统的 Java Web 应用程序开发流程。这有助于我们建立对 Web 开发的基础认知,为理解框架的工作原理打下坚实基础
本文将分三个阶段来描述Java Web程序的处理过程:
- 无服务器环境
在这个最基础的阶段,我们将探索如何在没有应用服务器(如 Tomcat)的情况下编写 Java Web 应用。这通常涉及到直接使用 Servlet API 来创建简单的 HTTP 服务。虽然这种方式提供了对底层机制的深入了解,但它缺乏现代 Web 开发所需的许多高级功能 - 引入Tomcat
当我们将 Tomcat 这样的应用服务器引入开发环境中后,可以显著提高开发效率和应用性能。Tomcat 负责管理 Servlet 的生命周期,处理客户端请求,并将请求转发给相应的 Servlet 进行处理。此外,它还提供了一些额外的服务,比如 JSP 支持和连接池管理等,这些都是构建复杂 Web 应用所必需的 - Spring Boot简化开发
随着项目规模的增长,传统方式下的配置管理和依赖管理变得越来越复杂。Spring Boot 通过其“约定优于配置”的原则,极大地简化了基于 Spring 的应用开发。它自动配置了许多常用的设置,使得开发者可以更快地启动项目,同时提供了丰富的功能以支持微服务架构、安全性和数据访问等需求
无服务器环境
服务器的主要功能
一个服务器最基本的功能是什么?接收请求并回复请求。那么,在最初我们是如何实现这样的功能的呢?
先看代码
代码如下:
(代码注释由通义灵码倾情提供)
1 | package com.baiyina.demo.springweblearndemo.notomcat; |
总结 & 下一阶段
代码总结
- 主函数
main
:- 创建一个
ServerSocket
对象,监听 8080 端口 - 进入无限循环,等待客户端连接。每次有新的连接时,调用
handleRequest
方法处理请求
- 创建一个
- 处理请求
handleRequest
:- 使用
BufferedReader
从客户端套接字读取请求数据 - 使用
BufferedWriter
向客户端套接字写入响应数据 - 读取请求的第一行(通常是请求行),并打印到控制台
- 构建一个简单的 HTTP 响应,包括状态行(
HTTP/1.1 200 OK
)、头部信息(Content-Type: text/html
和Content-Length: 20
)以及 HTML 内容(<h1>Hello World!</h1>
) - 将构建好的响应写入输出流并刷新缓冲区
- 使用
通过上述代码,我们可以看到一个服务器的基本功能是:
- 监听端口:服务器在指定的端口上监听客户端的连接请求
- 接收和解析请求:当客户端连接时,服务器读取并解析客户端发送的请求
- 创建响应:根据请求的内容,服务器生成相应的 HTTP 响应
- 返回响应:将生成的响应发送回客户端
下一阶段
虽然这个简易的 HTTP 服务器可以工作,但在实际应用中,我们需要更多的功能来使服务器真正可用。例如:
- 多线程处理:当前的服务器只能处理一个请求,需要引入多线程来同时处理多个客户端请求
- 请求路由:根据不同的 URL 路径将请求分发到不同的处理逻辑
- 资源管理:更好地管理文件和其他资源
- 安全性:增加安全措施,如 SSL/TLS 加密、认证和授权等
- 日志记录:记录请求和响应的日志,以便调试和监控
这些功能可以通过引入更高级的应用服务器(如 Tomcat)和框架(如 Spring Boot)来实现。接下来,我们将探讨如何通过引入 Tomcat 来简化开发并提高应用性能。
服务器所应承担的更多功能
在实际应用中,服务器需要处理更复杂的 RESTful HTTP 请求,并根据 URL 路径将请求映射到对应的控制器方法。下面是一个简单的示例,展示了如何实现这一功能。
先看代码
我们引入了一个 Router
类来管理和路由 HTTP 请求到相应的控制器。这个类通过维护一个路径到控制器的映射表来实现请求的路由。下面是 Router
类的主要功能:
- 添加路由:向路由表中添加一个路径到控制器的映射。
- 处理请求:根据请求路径处理请求并生成响应。如果找到对应的控制器,则调用控制器处理请求;否则返回 404 未找到响应。
运行方式
启动类为Main,启动后服务器将监听8080端口
可以使用浏览器或curl进行测试
1 | curl http://localhost:8080/home |
环境
注意,此处环境仅为笔者常用环境,与本文涉及代码关联不紧密,使用其他版本亦可。
- Java17
- Maven 3.9.8
- Spring Boot 3.3.5
1 | /** |
完整代码如下:
1 | <dependencies> |
代码
1 | /** |
1 | package com.baiyina.demo.springweblearndemo.notomcat; |
1 | package com.baiyina.demo.springweblearndemo.notomcat; |
1 | package com.baiyina.demo.springweblearndemo.notomcat; |
1 | package com.baiyina.demo.springweblearndemo.notomcat; |
1 | package com.baiyina.demo.springweblearndemo.notomcat; |
1 | package com.baiyina.demo.springweblearndemo.notomcat; |
总结 & 下一阶段
总结
根据我们在上一章所提到的,一个 Web 服务器应具备的基本功能包括:
- 接收来自客户端的 HTTP 请求:监听指定端口并接受客户端连接。
- 解析请求:分析请求中的 URL、头信息、参数等。
- 返回 HTTP 响应:将数据(如 HTML 页面、JSON 数据、图片等)发送回客户端。
在上面的代码中,我们实现了 Web 服务器的一个关键功能:解析 URL 并将其映射到相应的处理器方法。具体来说,Router
类通过维护一个路径到控制器的映射表来实现这一功能。
对于 Web 服务器来说,解析 URL 并将请求映射给合适的处理器方法是一个必要且复杂的过程。为了简化这一过程,我们可以将这些通用的功能抽象出来,封装成可重用的组件。
恭喜你,你发明了 Servlet 和 Servlet 容器,实际上Servlet 就是一种用于处理客户端请求的小程序,而 Servlet 容器(如 Tomcat)则负责管理这些 Servlet 的生命周期,并提供必要的服务,如请求分发、资源管理、安全性等。
下一阶段
接下来,我们将探讨如何通过引入 Tomcat 这样的应用服务器来简化开发并提高应用性能。Tomcat 不仅提供了强大的 Servlet 容器功能,还支持 JSP、连接池管理等高级特性,使得构建复杂的 Web 应用变得更加简单和高效。
引入Tomcat
使用 Tomcat 构建简单服务器
在上一章中,我们讨论了一个 Web 服务器应具备的基本功能,并使用基础的 Socket
开发了一个可以处理基本 HTTP 请求的服务器。接下来,我们将展示如何使用 Servlet 和 Tomcat 来实现相同的功能。
为什么选择 Tomcat?
Tomcat 是一个流行的开源应用服务器,它实现了 Java Servlet 和 JavaServer Pages (JSP) 规范。使用 Tomcat 可以显著简化 Web 应用的开发和部署过程,因为它提供了以下优势:
- 请求分发:自动将 HTTP 请求分发到相应的 Servlet。
- 资源管理:提供对静态资源(如 HTML、CSS、JavaScript 文件)的管理。
- 安全性:支持 SSL/TLS 加密、认证和授权等安全特性。
- 连接池管理:内置数据库连接池,提高数据库访问性能。
- 日志记录:提供详细的日志记录功能,便于调试和监控。
实现步骤
- 设置项目结构:
- 创建一个新的 Maven 项目,并添加必要的依赖项。
- 配置
pom.xml
文件,引入 Servlet API 依赖。
- 创建 Servlet:
- 编写一个简单的 Servlet 类,用于处理 HTTP 请求并生成响应。
- 在
web.xml
文件中配置 Servlet 映射,将 URL 路径与 Servlet 关联起来。
- 部署和运行:
- 将项目打包成 WAR 文件。
- 使用 Tomcat 部署 WAR 文件,并启动服务器。
运行方式
下面是如何使用 Tomcat 部署和运行你的项目:
下载 Tomcat:
- 访问 Apache Tomcat 官网 下载最新版本的 Tomcat。
- 根据你的环境选择合适的版本。建议下载
.zip
版本,使用起来更简单。
打包你的项目:
确保你的 Maven 项目配置了正确的依赖项。以下是一个示例
pom.xml
文件:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<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.example</groupId>
<artifactId>maven-demo-web-tomcat</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<packaging>war</packaging>
<dependencies>
<!-- Servlet API -->
<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.2</version> <!-- 使用最新版本 -->
</plugin>
</plugins>
</build>
</project>
将 WAR 包复制到 Tomcat 的 webapps 文件夹:
- 将生成的
xxx.war
文件复制到 Tomcat 的webapps
目录下。Tomcat 会自动检测到新的 WAR 文件并解压部署。
- 将生成的
启动 Tomcat:
- 进入 Tomcat 的
bin
目录,运行startup.bat
(Windows)或startup.sh
(Mac/Linux)来启动 Tomcat。
- 进入 Tomcat 的
访问应用:
启动成功后,Tomcat 会将你的应用部署在
http://localhost:8080/xxx
(其中xxx
是你的项目名)。你可以通过浏览器或curl命令访问你的应用,例如:
1
curl http://localhost:8080/xxx/home
环境
与上文相同,根据你常用的版本进行调整即可,没有特殊要求:
- Java 17
- Maven 3.9.8
- Spring Boot 3.3.5
1 | package com.baiyina.demo.web.servlet; |
1 | package com.baiyina.demo.web.servlet; |
总结 & 下一阶段
总结
如上所示,如果我们使用 Servlet 和 Servlet 容器(如 Tomcat)进行开发,我们只需要关注程序本身需要处理的逻辑,通过简单的 XML 配置或注解来标识接口的路由,而不需要关心请求的映射过程和创建 HttpServletRequest
和 HttpServletResponse
对象的复杂过程。Servlet 容器会自动解析 HTTP 请求,并注入这些对象。
在这个程序中:
- Servlet 扮演了 Controller 的角色:负责处理具体的业务逻辑
- Servlet 容器扮演了 Router 和 HttpServer 的角色:它将自动分发请求、处理路径映射、监听
ServerSocket
以及处理 HTTP 请求的解析、传输和实现更多功能
实际上,Servlet 容器带来了更多的功能,包括但不限于:
- 线程管理:在并发处理请求时,Web 服务器需要使用多线程的方式提高性能。Servlet 容器会管理并发请求的线程,自动创建线程池并分配资源
- 会话管理:容器内置 Session 管理,支持需要状态保持的应用
- 生命周期管理:Servlet 的生命周期(初始化、请求处理、销毁)将由 Servlet 容器接管
- 扩展性和异常处理:提供更高的扩展性、强大的异常处理机制以及基础的安全性支持
正因为 Servlet 开发的优越性,它是 Java Web 开发的一个重要组成部分,使得开发者可以更专注于处理应用逻辑,而不必处理底层的 HTTP 细节。
尽管 Servlet 已经提供了许多优势,但由于现代应用服务器面临更复杂的需求,开发者也需要更简易、更优雅流畅的开发方式,以减少重复性的工作,将更多的时间投入到业务逻辑的处理中。因此,Spring Boot 应运而生,为大家带来了更优异的开发方式。
下一阶段
接下来,我们将探讨如何使用 Spring Boot 来进一步简化 Web 应用的开发。Spring Boot 通过其“约定优于配置”的原则,极大地减少了配置工作,提供了自动配置、依赖管理和丰富的功能支持,使得开发者可以更快地启动项目,并专注于核心业务逻辑的实现。
使用 Spring Boot 开发
Spring Boot 在多个方面都具有显著的优势,其“快速开发”的概念可以说是非常贴切的描述。它通过自动化配置和“约定优于配置”的方式极大地简化了开发工作。今天,我们不打算详细讨论它的所有优势,而是重点介绍它在处理 Web 应用方面的简便性:内嵌 Web 服务器并自动配置。这意味着当你编写完一个 Web 应用后,可以直接打包成可执行的 JAR 文件并运行,无需手动部署。
先看代码
环境
- Java 17
- Maven 3.9.8
- Spring Boot 3.3.5
启动方式
建议使用 IntelliJ IDEA 进行开发,因为它提供了便捷的开发环境和一键运行功能。如果你使用 IntelliJ IDEA,只需点击运行按钮即可启动应用。
如果你使用其他开发工具或命令行,也可以通过以下步骤将项目打包成可执行的 JAR 文件并运行:
构建项目:
使用 Maven 构建项目。在项目的根目录下运行以下命令:
1
1mvn clean package
运行 JAR 文件:
构建完成后,会在
target
目录下生成一个可执行的 JAR 文件(如demo-0.0.1-SNAPSHOT.jar
)。使用以下命令运行 JAR 文件:
1
1java -jar target/demo-0.0.1-SNAPSHOT.jar
访问应用:
启动成功后,可以通过浏览器或curl命令访问你的应用,例如:
1
curl http://localhost:8080/home
代码
1 | package com.baiyina.demo.springweblearndemo.controller; |
实际上,你需要编写的代码只有这一个控制器类。当你使用 Spring Boot 创建好项目之后,只需引入以下依赖项即可:
1 | <dependency> |
这个依赖项会帮助你自动配置好 Web 服务器,并处理好 URL 映射。
总结 & 下一阶段
上面的代码非常简洁,主要定义了一个 AboutController
类,用于处理 /about
路径下的 GET 和 POST 请求。Spring Boot 通过其自动配置功能简化了大部分底层配置工作,使得开发者可以更专注于业务逻辑的实现。
如果你对 Spring Boot 如何处理网络请求的具体流程感兴趣,可以参考我的另一篇博客 Spring Web 网络请求处理流程 。在那篇文章中,我详细梳理了 Spring Web 处理网络请求的流程和涉及的组件。
总结
通过从 Socket
到 Spring Boot
的发展历程,我们可以发现技术的发展是基于更基础的技术进行包装和升级的。例如,Servlet
可以理解为基于 Socket
技术开发的更高层次的开发模型,它提供了一种比直接使用 Socket
更简便、高效的开发模式。而 Spring Web
在 Servlet
之上提供了更高的抽象和更简洁优雅的开发模式,它甚至可以在一套规范内集成数据库连接、Web 应用开发等自动化配置。Spring Boot
内嵌了 Web 服务器,使得开发和部署变得更加便捷。更不用提它与 Spring 家族其他成员的无缝配合。
然而,越来越高层次的抽象也让我们对程序的底层原理变得陌生。我们逐渐变成了框架的使用者而不是创造者。实际上,这些框架本身就可以被视为一整套应用程序。
为什么要深入了解?
理解底层原理:通过了解
Socket
和Servlet
的工作原理,我们可以更好地理解Spring Web
和Spring Boot
的设计思路。提高开发效率:掌握底层原理可以帮助我们更有效地使用框架提供的功能,避免不必要的复杂配置。我们可以更灵活地定制和扩展应用,满足特定需求。
提升代码质量:我们可以更好地利用框架的最佳实践,避免常见的陷阱和错误。
增强调试能力:我们可以更好地理解日志和异常信息,从而更快地解决问题。这有助于我们在遇到问题时能够快速定位并解决。
因此,了解框架的底层原理,一步步拆解底层的运行过程,才能让我们在编写程序时游刃有余。只有当我们知道每一行代码的意义时,才能更好地实现所需的功能,不会被框架限制住,而是真正吸收和利用框架去创造更高质量的程序。