通过技术发展理解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
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package com.baiyina.demo.springweblearndemo.notomcat;
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

/**
* SimpleHttpServer 是一个简单的 HTTP 服务器实现。
* 它监听指定端口,并对每个传入请求打印一个简单的响应。
* 该服务器主要用于演示和测试目的,并不适合生产环境使用。
*
* @author baiyina
* @since 1.0
*/
public class SimpleHttpServer {
/**
* 主函数,启动 HTTP 服务器并开始监听请求。
*
* @param args 命令行参数,本例中未使用。
* @throws IOException 如果服务器Socket初始化失败或在处理请求时发生I/O错误。
*/
public static void main(String[] args) throws IOException {
// 监听端口 8080
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("Server started on port 8080...");

// 无限循环等待并处理客户端请求
while (true) {
try (Socket clientSocket = serverSocket.accept()) {
// 处理客户端请求
handleRequest(clientSocket);
}
}
}

/**
* 处理客户端请求并返回简单响应。
*
* @param clientSocket 客户端Socket,用于读取请求和发送响应。
* @throws IOException 如果在读取请求或发送响应时发生I/O错误。
*/
private static void handleRequest(Socket clientSocket) throws IOException {
// 读取请求输入流
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 写入响应输出流
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));

// 读取请求行
String requestLine = in.readLine();
System.out.println("Received request: " + requestLine);

// 构建简单响应
String response = """
HTTP/1.1 200 OK\r
Content-Type: text/html\r
Content-Length: 20\r
\r
<h1>Hello World!</h1>""";

// 发送响应
out.write(response);
out.flush();
}
}


总结 & 下一阶段

代码总结
  1. 主函数 main:
    • 创建一个 ServerSocket 对象,监听 8080 端口
    • 进入无限循环,等待客户端连接。每次有新的连接时,调用 handleRequest 方法处理请求
  2. 处理请求 handleRequest:
    • 使用 BufferedReader 从客户端套接字读取请求数据
    • 使用 BufferedWriter 向客户端套接字写入响应数据
    • 读取请求的第一行(通常是请求行),并打印到控制台
    • 构建一个简单的 HTTP 响应,包括状态行(HTTP/1.1 200 OK)、头部信息(Content-Type: text/htmlContent-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
2
curl http://localhost:8080/home
curl -X POST http://localhost:8080/about
环境

注意,此处环境仅为笔者常用环境,与本文涉及代码关联不紧密,使用其他版本亦可。

  • Java17
  • Maven 3.9.8
  • Spring Boot 3.3.5
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
37
38
39
40
41
42
/**
* Router类用于管理和路由HTTP请求到相应的控制器
* 它通过维护一个路径到控制器的映射来实现请求的路由
*
* @author baiyina
* @since 1.0
* @since 2024/11/11 17:50
*/
public class Router {
// 存储路径与控制器映射的路由表
private final Map<String, Controller> routes = new HashMap<>();

/**
* 向路由表中添加一个路径到控制器的映射
*
* @param path 路径,请求的URL路径
* @param controller 控制器,处理请求的具体实现
*/
public void addRoute(String path, Controller controller) {
routes.put(path, controller);
}

/**
* 根据请求路径处理请求并生成响应
* 如果找到对应的控制器,则调用控制器处理请求,否则返回404未找到
*
* @param request 请求对象,包含请求的所有信息
* @param response 响应对象,用于发送响应给客户端
* @throws IOException 如果在处理请求或生成响应时发生I/O错误
*/
public void handle(Request request, Response response) throws IOException {
// 根据请求路径获取对应的控制器
Controller controller = routes.get(request.getPath());
if (controller != null) {
// 如果找到控制器,则调用控制器处理请求
controller.handle(request, response);
} else {
// 如果未找到控制器,则发送404未找到响应
response.sendNotFound();
}
}
}

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
 <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>compile</scope>
</dependency>
</dependencies>

image-20241113111458939

代码
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
/**
* 主类,用于启动HTTP服务器并配置路由
* 本类负责初始化路由器,配置路由映射,并启动HTTP服务器
*
* @author baiyina
* @since 1.0
* @since 2024/11/11 17:50
*/
public class Main {
/**
* 主函数,程序的入口点
* 在此函数中,我们创建了路由器实例,配置了路由映射,并启动了HTTP服务器
*
* @param args 命令行参数,本程序不使用此参数
* @throws IOException 如果服务器启动过程中发生I/O错误
*/
public static void main(String[] args) throws IOException {
Router router = new Router();

// 添加路由映射
router.addRoute("/home", new HomeController());
router.addRoute("/about", new AboutController());

// 启动服务器
HttpServer server = new HttpServer(router);
server.start(8080);
}
}
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.baiyina.demo.springweblearndemo.notomcat;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;

/**
* 表示一个HTTP请求,主要负责从输入流中解析请求方法和路径。
*
* @author baiyina
* @since 1.0
* @since 2024/11/11 17:48
*/
public class Request {
/**
* 存储HTTP请求方法,如GET、POST等
*/
private String method;
/**
* 存储请求的路径,如/index.html
*/
private String path;

/**
* 通过解析输入流中的请求信息来构造Request对象。
*
* @param inputStream 包含HTTP请求信息的输入流。
* @throws IOException 如果在读取输入流时发生I/O错误。
*/
public Request(InputStream inputStream) throws IOException {
// 使用BufferedReader按行读取输入流,便于处理
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
// 读取请求的第一行,该行包含请求方法和路径
String line = reader.readLine();

// 检查第一行是否为空,以防止解析错误
if (line != null && !line.isEmpty()) {
// 按空格分割第一行,获取请求方法和路径
String[] parts = line.split(" ");
// 将解析出的请求方法和路径赋值给对应的字段
this.method = parts[0];
this.path = parts[1];
}
}

/**
* 获取HTTP请求方法。
*
* @return 请求方法,如GET、POST等。
*/
public String getMethod() {
return method;
}

/**
* 获取请求的路径。
*
* @return 请求的路径,如/index.html。
*/
public String getPath() {
return path;
}
}
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.baiyina.demo.springweblearndemo.notomcat;

import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;

/**
* Response类用于处理HTTP响应相关操作
* 它提供了将内容写入输出流的方法,主要用于发送HTTP响应给客户端
*
* @author baiyina
* @since 1.0
* @since 2024/11/11 17:48
*/
public class Response {
/**
* outputStream用于将响应内容输出到客户端
*/
//
private final OutputStream outputStream;

/**
* 构造方法,初始化Response对象
*
* @param outputStream 输出流,用于将响应内容输出到客户端
*/
public Response(OutputStream outputStream) {
this.outputStream = outputStream;
}

/**
* 发送HTTP响应的方法
* 它会构建HTTP响应的头部和内容,并将其写入输出流
*
* @param content 要发送的HTTP响应内容
* @throws IOException 如果在写入输出流时发生I/O错误
*/
public void send(String content) throws IOException {
// 使用PrintWriter包装outputStream,以便于发送响应
PrintWriter writer = new PrintWriter(outputStream, true);
// 构建HTTP响应头部,包括状态码、内容类型和内容长度
writer.println("HTTP/1.1 200 OK");
writer.println("Content-Type: text/plain");
writer.println("Content-Length: " + content.length());
// 空行表示HTTP响应头部结束
writer.println();
// 发送实际的响应内容
writer.println(content);
}

/**
* 发送404 Not Found HTTP响应的方法
* 它专门用于构建和发送404错误的HTTP响应
*
* @throws IOException 如果在写入输出流时发生I/O错误
*/
public void sendNotFound() throws IOException {
// 使用PrintWriter包装outputStream,以便于发送响应
PrintWriter writer = new PrintWriter(outputStream, true);
// 构建HTTP 404错误响应的头部
writer.println("HTTP/1.1 404 Not Found");
writer.println("Content-Type: text/plain");
// 空行表示HTTP响应头部结束
writer.println();
// 发送404错误的响应内容
writer.println("404 Not Found");
}
}

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.baiyina.demo.springweblearndemo.notomcat;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;

/**
* 表示一个HTTP服务器,该服务器监听指定端口并使用路由器处理传入的HTTP请求。
* 服务器无限期运行,接受客户端连接并使用路由器处理请求。
*
* @author baiyina
* @since 1.0
* @since 2024/11/11 17:50
*/
public class HttpServer {
/**
* 用于路由HTTP请求的核心组件。
*/
private final Router router;

/**
* 使用指定的路由器构造HttpServer实例。
*
* @param router 用于处理HTTP请求的路由器。
*/
public HttpServer(Router router) {
this.router = router;
}

/**
* 在指定端口启动HTTP服务器,监听并处理传入的HTTP请求。
*
* @param port 服务器监听客户端请求的端口号。
* @throws IOException 当创建或访问服务器套接字时发生I/O错误。
*/
public void start(int port) throws IOException {
// 创建服务器套接字并监听指定端口
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("Server is running on port " + port);

// 服务器主循环,持续监听客户端连接
while (true) {
// 接受客户端连接
try (Socket socket = serverSocket.accept()) {
// 解析客户端的请求
Request request = new Request(socket.getInputStream());
// 准备向客户端发送响应
Response response = new Response(socket.getOutputStream());

// 使用路由器处理请求并生成响应
router.handle(request, response);
} catch (IOException e) {
// 如果处理请求时发生异常,输出错误信息
System.err.println("Error handling request: " + e.getMessage());
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.baiyina.demo.springweblearndemo.notomcat;

import java.io.IOException;

/**
* Controller 接口定义了处理请求和响应的标准方式。
* 它设计用于处理请求对象并生成相应的响应对象,有助于应用程序架构中的关注点分离和代码组织。
*
* @author baiyina
* @since 1.0
* @since 2024/11/11 17:49
*/
public interface Controller {
/**
* 处理请求并生成响应。
* 该方法是 Controller 接口的核心,允许实现类处理传入的请求,并根据请求内容生成相应的响应,实现从请求到响应的转换。
*
* @param request 请求对象,封装了请求的所有信息。
* @param response 响应对象,用于封装返回的响应信息。
* @throws IOException 如果在请求处理过程中发生 I/O 错误。
*/
void handle(Request request, Response response) throws IOException;
}

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
package com.baiyina.demo.springweblearndemo.notomcat;

import java.io.IOException;

/**
* HomeController类负责处理主页的HTTP请求,实现了Controller接口。
* 它根据请求的方法类型(GET或POST),返回不同的响应消息。
*
* @author baiyina
* @since 1.0
* @since 2024/11/11 17:49
*/
public class HomeController implements Controller {
/**
* 处理HTTP请求的方法,根据请求方法类型返回不同的响应。
*
* @param request 请求对象,包含HTTP请求的相关信息。
* @param response 响应对象,用于向客户端发送响应。
* @throws IOException 如果发送响应时发生I/O错误。
*/
@Override
public void handle(Request request, Response response) throws IOException {
// 检查请求方法是否为GET
if ("GET".equalsIgnoreCase(request.getMethod())) {
response.send("Welcome to Home Page!");
} else if ("POST".equalsIgnoreCase(request.getMethod())) {
// 请求方法为POST时的处理
response.send("POST request to Home Page.");
} else {
// 请求方法既不是GET也不是POST时的处理
response.send("Method Not Supported");
}
}
}

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
package com.baiyina.demo.springweblearndemo.notomcat;

import java.io.IOException;

/**
* AboutController 类负责处理与关于页面相关的 HTTP 请求。
* 它实现了 Controller 接口,并为不同的 HTTP 方法提供了具体的处理逻辑。
*
* @author baiyina
* @since 1.0
* @since 2024/11/11 17:49
*/
public class AboutController implements Controller {
@Override
public void handle(Request request, Response response) throws IOException {
// 根据请求方法处理不同的 HTTP 请求
if ("GET".equalsIgnoreCase(request.getMethod())) {
response.send("Welcome to About Page!");
} else if ("POST".equalsIgnoreCase(request.getMethod())) {
response.send("POST request to About Page.");
} else {
response.send("Method Not Supported");
}
}
}

总结 & 下一阶段

总结

根据我们在上一章所提到的,一个 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 加密、认证和授权等安全特性。
  • 连接池管理:内置数据库连接池,提高数据库访问性能。
  • 日志记录:提供详细的日志记录功能,便于调试和监控。

实现步骤

  1. 设置项目结构
    • 创建一个新的 Maven 项目,并添加必要的依赖项。
    • 配置 pom.xml 文件,引入 Servlet API 依赖。
  2. 创建 Servlet
    • 编写一个简单的 Servlet 类,用于处理 HTTP 请求并生成响应。
    • web.xml 文件中配置 Servlet 映射,将 URL 路径与 Servlet 关联起来。
  3. 部署和运行
    • 将项目打包成 WAR 文件。
    • 使用 Tomcat 部署 WAR 文件,并启动服务器。
运行方式

下面是如何使用 Tomcat 部署和运行你的项目:

  1. 下载 Tomcat

    • 访问 Apache Tomcat 官网 下载最新版本的 Tomcat。
    • 根据你的环境选择合适的版本。建议下载 .zip 版本,使用起来更简单。
  2. 打包你的项目

    • 确保你的 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
      <?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.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>
  3. 将 WAR 包复制到 Tomcat 的 webapps 文件夹

    • 将生成的 xxx.war 文件复制到 Tomcat 的 webapps 目录下。Tomcat 会自动检测到新的 WAR 文件并解压部署。
  4. 启动 Tomcat

    • 进入 Tomcat 的 bin 目录,运行 startup.bat(Windows)或 startup.sh(Mac/Linux)来启动 Tomcat。
  5. 访问应用

    • 启动成功后,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

image-20241113112123286

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.baiyina.demo.web.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
* AboutServlet类扩展了HttpServlet,用于处理关于页面的HTTP请求。
* 它主要处理两种类型的请求:GET和POST。
* 该类被@WebServlet("/about")注解标记,意味着它被映射到"/about"路径。
*
* @author baiyina
* @since 1.0
* @since 2024/11/12 20:55
*/
@WebServlet("/about")
public class AboutServlet extends HttpServlet {
/**
* 处理GET请求。它通过设置响应内容类型为文本,并写入欢迎信息到响应中。
*
* @param request 代表HTTP请求的对象,包含请求数据。
* @param response 代表HTTP响应的对象,用于向客户端发送数据。
* @throws ServletException 如果Servlet遇到异常。
* @throws IOException 如果发生输入或输出异常。
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/plain");
PrintWriter out = response.getWriter();
out.println("Welcome to About Page!");
}

/**
* 处理POST请求。与GET方法类似,但专门处理POST请求。
*
* @param request 代表HTTP请求的对象,包含请求数据。
* @param response 代表HTTP响应的对象,用于向客户端发送数据。
* @throws ServletException 如果Servlet遇到异常。
* @throws IOException 如果发生输入或输出异常。
*/
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/plain");
PrintWriter out = response.getWriter();
out.println("POST request to About Page.");
}
}

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.baiyina.demo.web.servlet;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
* HomeServlet类用于处理主页的HTTP请求
* 它继承自HttpServlet,重写了doGet和doPost方法来处理GET和POST请求
*
* @author baiyina
* @since 1.0
* @since 2024/11/12 21:00
*/
@WebServlet("/home")
public class HomeServlet extends HttpServlet {
/**
* 处理GET请求
* 设置响应内容类型为纯文本,并在响应中写入欢迎信息
*
* @param request 请求对象,包含来自客户端的请求信息
* @param response 响应对象,用于向客户端发送响应
* @throws ServletException 如果Servlet遇到异常
* @throws IOException 如果发生输入或输出异常
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/plain");
PrintWriter out = response.getWriter();
out.println("Welcome to Home Page!");
}

/**
* 处理POST请求
* 设置响应内容类型为纯文本,并在响应中写入收到POST请求的信息
*
* @param request 请求对象,包含来自客户端的请求信息
* @param response 响应对象,用于向客户端发送响应
* @throws ServletException 如果Servlet遇到异常
* @throws IOException 如果发生输入或输出异常
*/
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("text/plain");
PrintWriter out = response.getWriter();
out.println("POST request to Home Page.");
}
}

总结 & 下一阶段

总结

如上所示,如果我们使用 Servlet 和 Servlet 容器(如 Tomcat)进行开发,我们只需要关注程序本身需要处理的逻辑,通过简单的 XML 配置或注解来标识接口的路由,而不需要关心请求的映射过程和创建 HttpServletRequestHttpServletResponse 对象的复杂过程。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 文件并运行:

  1. 构建项目

    • 使用 Maven 构建项目。在项目的根目录下运行以下命令:

      1
      1mvn clean package
  2. 运行 JAR 文件

    • 构建完成后,会在 target 目录下生成一个可执行的 JAR 文件(如 demo-0.0.1-SNAPSHOT.jar)。

    • 使用以下命令运行 JAR 文件:

      1
      1java -jar target/demo-0.0.1-SNAPSHOT.jar
  3. 访问应用

    • 启动成功后,可以通过浏览器或curl命令访问你的应用,例如:

      1
      curl http://localhost:8080/home

代码

image-20241115001016233

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
37
38
package com.baiyina.demo.springweblearndemo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* AboutController类负责处理与“关于”页面相关的HTTP请求。
* 该类定义了两个处理函数,分别用于处理GET和POST请求。
*
* @author baiyina
* @since 1.0
* @since 2024/11/14 15:18
*/
@RestController
@RequestMapping("/about")
public class AboutController {
/**
* 处理GET请求到/about,返回关于页面的欢迎信息。
*
* @return 返回关于页面的欢迎信息字符串。
*/
@GetMapping
public String get() {
return "Welcome to About Page";
}

/**
* 处理POST请求到/about,返回关于页面的POST请求处理信息。
*
* @return 返回关于页面的POST请求处理信息字符串。
*/
@PostMapping
public String post() {
return "POST request to About Page.";
}
}

实际上,你需要编写的代码只有这一个控制器类。当你使用 Spring Boot 创建好项目之后,只需引入以下依赖项即可:

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

这个依赖项会帮助你自动配置好 Web 服务器,并处理好 URL 映射。

总结 & 下一阶段

上面的代码非常简洁,主要定义了一个 AboutController 类,用于处理 /about 路径下的 GET 和 POST 请求。Spring Boot 通过其自动配置功能简化了大部分底层配置工作,使得开发者可以更专注于业务逻辑的实现。

如果你对 Spring Boot 如何处理网络请求的具体流程感兴趣,可以参考我的另一篇博客 Spring Web 网络请求处理流程 。在那篇文章中,我详细梳理了 Spring Web 处理网络请求的流程和涉及的组件。

总结

通过从 SocketSpring Boot 的发展历程,我们可以发现技术的发展是基于更基础的技术进行包装和升级的。例如,Servlet 可以理解为基于 Socket 技术开发的更高层次的开发模型,它提供了一种比直接使用 Socket 更简便、高效的开发模式。而 Spring WebServlet 之上提供了更高的抽象和更简洁优雅的开发模式,它甚至可以在一套规范内集成数据库连接、Web 应用开发等自动化配置。Spring Boot 内嵌了 Web 服务器,使得开发和部署变得更加便捷。更不用提它与 Spring 家族其他成员的无缝配合。

然而,越来越高层次的抽象也让我们对程序的底层原理变得陌生。我们逐渐变成了框架的使用者而不是创造者。实际上,这些框架本身就可以被视为一整套应用程序。

为什么要深入了解?

  • 理解底层原理:通过了解 SocketServlet 的工作原理,我们可以更好地理解 Spring WebSpring Boot 的设计思路。

  • 提高开发效率:掌握底层原理可以帮助我们更有效地使用框架提供的功能,避免不必要的复杂配置。我们可以更灵活地定制和扩展应用,满足特定需求。

  • 提升代码质量:我们可以更好地利用框架的最佳实践,避免常见的陷阱和错误。

  • 增强调试能力:我们可以更好地理解日志和异常信息,从而更快地解决问题。这有助于我们在遇到问题时能够快速定位并解决。

因此,了解框架的底层原理,一步步拆解底层的运行过程,才能让我们在编写程序时游刃有余。只有当我们知道每一行代码的意义时,才能更好地实现所需的功能,不会被框架限制住,而是真正吸收和利用框架去创造更高质量的程序。