Servlet详解

O泡李华 11

Servlet 详解

本章位置:第二阶段 Java 核心框架
前置知识:JavaSE、HTTP 基础、Tomcat、Maven、MyBatis
本章内容:Servlet
下一篇:Servlet / JSP
学习目标:理解 Java Web 的基本运行流程,掌握 Tomcat、Servlet、请求、响应、参数获取、中文乱码、转发、重定向、生命周期、注解配置以及常见报错排查。


一、什么是 Servlet

Servlet 是 Java Web 中非常核心的技术。

可以简单理解:

Servlet 是运行在 Web 服务器中的 Java 程序,用来接收浏览器请求,并生成响应。

例如:

浏览器访问:

http://localhost:8080/demo/hello

Tomcat 收到请求后:

找到对应 Servlet
↓
调用 Servlet
↓
Servlet 执行业务逻辑
↓
生成响应
↓
Tomcat 返回给浏览器

二、为什么需要 Servlet

以前写 Java:

public static void main(
        String[] args
) {

}

程序入口固定是:

main()

但是 Web 程序不一样。

浏览器会随时发送:

HTTP 请求

例如:

登录
注册
查询学生
新增订单
删除数据

我们需要一种机制:

把 HTTP 请求
交给 Java 方法处理

Servlet 就是这个桥梁。


三、浏览器、Tomcat、Servlet 的关系

整体流程:

浏览器
↓
HTTP 请求
↓
Tomcat
↓
Servlet
↓
Service / DAO / MyBatis
↓
数据库
↓
Servlet
↓
HTTP 响应
↓
浏览器

四、Tomcat 是什么

Tomcat 是:

Java Web 服务器
Servlet 容器

它主要负责:

监听端口

接收 HTTP 请求

根据 URL 找 Servlet

创建 Servlet 对象

调用 Servlet 生命周期方法

把响应返回浏览器

五、为什么 Servlet 不能自己运行

普通 Java:

public static void main(...)

可以直接运行。

Servlet 通常没有:

main()

它需要:

Tomcat

帮它:

创建对象
调用方法
管理生命周期

所以 Servlet 属于:

容器管理对象

六、什么是 Servlet 容器

Servlet 容器就是:

管理 Servlet 的运行环境

Tomcat 就是最常见的 Servlet 容器之一。

它负责:

Servlet 加载

Servlet 实例化

初始化

请求分发

销毁

七、Servlet API

Servlet 相关接口和类来自:

Servlet API

在 Tomcat 8.5 时代常见包:

javax.servlet

例如:

javax.servlet.http.HttpServlet

注意:

Jakarta EE 9+ 已经把包名迁移到 jakarta.servlet。

如果你当前使用 Tomcat 8.5:

一般使用 javax.servlet

如果使用 Tomcat 10+:

通常使用 jakarta.servlet

不要混用。


八、Tomcat 8 和 Tomcat 10 的重要区别

Tomcat 8.5:

import javax.servlet.*;

Tomcat 10+:

import jakarta.servlet.*;

如果:

Tomcat 8.5

却使用:

jakarta.servlet

项目可能无法正常运行。

反过来也一样。


九、创建 Maven Web 项目

基础目录:

servlet-demo
│
├─ pom.xml
│
└─ src
   └─ main
      ├─ java
      │  └─ com.example.web
      │     └─ HelloServlet.java
      │
      └─ webapp
         ├─ index.html
         └─ WEB-INF
            └─ web.xml

十、pom.xml 打包方式

Web 项目常写:

<packaging>war</packaging>

例如:

<project ...>

    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>

    <artifactId>servlet-demo</artifactId>

    <version>1.0-SNAPSHOT</version>

    <packaging>war</packaging>

</project>

十一、Servlet API 依赖

Tomcat 8.5 对应:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
</dependency>

十二、为什么 scope 用 provided

<scope>provided</scope>

表示:

编译时需要

运行时由 Tomcat 提供

因为 Tomcat 本身已经包含 Servlet API。

如果重复打进去:

可能导致类冲突

十三、第一个 Servlet

package com.example.web;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;

import java.io.IOException;

@WebServlet(
        "/hello"
)
public class HelloServlet
        extends HttpServlet {

    @Override
    protected void doGet(
            HttpServletRequest request,
            HttpServletResponse response
    )
            throws ServletException,
            IOException {

        response
                .getWriter()
                .write(
                        "Hello Servlet"
                );
    }
}

十四、@WebServlet

@WebServlet(
        "/hello"
)

表示:

这个 Servlet 对应 URL:
/hello

浏览器访问:

http://localhost:8080/项目路径/hello

Tomcat 就会找到:

HelloServlet

十五、extends HttpServlet

public class HelloServlet
        extends HttpServlet

说明:

这是一个处理 HTTP 请求的 Servlet

HttpServlet 已经帮我们封装了大量 HTTP 处理逻辑。


十六、doGet()

protected void doGet(
        HttpServletRequest request,
        HttpServletResponse response
)

用于处理:

GET 请求

十七、doPost()

protected void doPost(
        HttpServletRequest request,
        HttpServletResponse response
)

用于处理:

POST 请求

十八、浏览器地址栏通常是什么请求

直接在浏览器地址栏输入:

http://localhost:8080/demo/hello

通常发送:

GET

所以会进入:

doGet()

十九、HTML form 提交 POST

例如:

<form
    action="/demo/login"
    method="post"
>

    <input
        name="username"
    >

    <input
        name="password"
        type="password"
    >

    <button
        type="submit"
    >
        登录
    </button>

</form>

会进入:

doPost()

二十、HttpServletRequest

HttpServletRequest request

表示:

客户端请求对象

里面包含:

请求参数

请求头

请求方法

请求路径

Cookie

Session

请求体

二十一、HttpServletResponse

HttpServletResponse response

表示:

服务器响应对象

可以设置:

响应状态码

响应头

响应内容

重定向

Cookie

二十二、request 和 response 怎么来的

不是我们:

new HttpServletRequest()

而是:

Tomcat 收到 HTTP 请求
↓
解析请求
↓
创建 request / response
↓
传给 Servlet

所以:

request / response 都是容器提供

二十三、获取请求方法

String method =
        request.getMethod();

可能:

GET

POST

PUT

DELETE

二十四、获取请求 URI

String uri =
        request.getRequestURI();

例如:

/demo/hello

二十五、获取请求 URL

StringBuffer url =
        request.getRequestURL();

例如:

http://localhost:8080/demo/hello

二十六、URI 和 URL 区别简单理解

URI:

资源标识

URL:

完整定位地址

例如:

URI:
/demo/hello

URL:
http://localhost:8080/demo/hello

二十七、获取 Context Path

String contextPath =
        request
                .getContextPath();

如果项目路径:

/demo

返回:

/demo

二十八、为什么 contextPath 很重要

不推荐页面写死:

<a href="/demo/student/list">

更灵活:

动态获取项目路径

后面 JSP 会经常用:

${pageContext.request.contextPath}

二十九、获取请求参数

HTML:

<input
    name="username"
>

Servlet:

String username =
        request
                .getParameter(
                        "username"
                );

三十、参数名取决于 name

HTML:

<input
    name="username"
>

Java:

request.getParameter(
        "username"
);

如果写:

getParameter(
        "userName"
);

可能返回:

null

因为参数名不一致。


三十一、getParameter() 返回 String

无论:

年龄

成绩

id

通常:

getParameter()

返回:

String

例如:

String ageText =
        request
                .getParameter(
                        "age"
                );

int age =
        Integer.parseInt(
                ageText
        );

三十二、参数不存在

request.getParameter(
        "age"
);

如果客户端没传:

可能返回 null

所以不要直接:

Integer.parseInt(
        null
);

三十三、参数校验

推荐:

String ageText =
        request
                .getParameter(
                        "age"
                );

if (
    ageText == null
    || ageText.isBlank()
) {

    response.sendError(
            400,
            "age 参数不能为空"
    );

    return;
}

三十四、获取多个同名参数

例如:

<input
    type="checkbox"
    name="hobby"
    value="java"
>

<input
    type="checkbox"
    name="hobby"
    value="music"
>

Servlet:

String[] hobbies =
        request
                .getParameterValues(
                        "hobby"
                );

三十五、getParameterMap()

Map<String, String[]>
        parameterMap =
        request
                .getParameterMap();

可以获得:

全部请求参数

后面做:

请求参数自动封装 JavaBean

会用到类似思路。


三十六、GET 参数

例如:

/student?id=10&name=张三

Servlet:

String id =
        request.getParameter(
                "id"
        );

String name =
        request.getParameter(
                "name"
        );

三十七、POST 表单参数

表单:

<form
    method="post"
    action="/demo/login"
>

如果 Content-Type 是:

application/x-www-form-urlencoded

Servlet 也可以:

request.getParameter(...)

获取。


三十八、请求头

String userAgent =
        request
                .getHeader(
                        "User-Agent"
                );

三十九、常见请求头

例如:

Host

User-Agent

Accept

Content-Type

Cookie

Authorization

四十、获取所有 Header

Enumeration<String> names =
        request
                .getHeaderNames();

然后遍历:

while (
    names.hasMoreElements()
) {

    String name =
            names.nextElement();

    String value =
            request.getHeader(
                    name
            );
}

四十一、响应文本

response
        .getWriter()
        .write(
                "Hello"
        );

四十二、设置响应内容类型

推荐:

response.setContentType(
        "text/html;charset=UTF-8"
);

这样浏览器知道:

这是 HTML

编码是 UTF-8

四十三、中文乱码最常见处理

Servlet 输出中文:

response.setContentType(
        "text/html;charset=UTF-8"
);

要在:

getWriter()

之前设置。


四十四、错误顺序

不推荐:

PrintWriter writer =
        response.getWriter();

response.setContentType(
        "text/html;charset=UTF-8"
);

因为:

Writer 可能已经按照默认编码创建

四十五、POST 请求乱码

旧式 Servlet 环境中,POST 表单参数可能需要:

request.setCharacterEncoding(
        "UTF-8"
);

并且必须在:

第一次 getParameter()

之前设置。


四十六、正确顺序

request.setCharacterEncoding(
        "UTF-8"
);

String username =
        request.getParameter(
                "username"
        );

不要:

String username =
        request.getParameter(
                "username"
        );

request.setCharacterEncoding(
        "UTF-8"
);

这样通常已经晚了。


四十七、GET 中文乱码

现代 Tomcat 通常会按 URI 编码配置处理 GET 参数。

Tomcat Connector 常见:

URIEncoding="UTF-8"

注意:

URIEncoding

拼写要正确。

不是:

URIEconding

四十八、Tomcat server.xml

可能看到:

<Connector
    port="8080"
    protocol="HTTP/1.1"
    connectionTimeout="20000"
    redirectPort="8443"
    URIEncoding="UTF-8"
/>

四十九、response.setCharacterEncoding()

也可以:

response.setCharacterEncoding(
        "UTF-8"
);

但更常直接:

response.setContentType(
        "text/html;charset=UTF-8"
);

因为它同时设置:

内容类型 + 编码

五十、输出 HTML

response.setContentType(
        "text/html;charset=UTF-8"
);

response
        .getWriter()
        .write(
                """
                <html>
                <body>
                    <h1>Hello Servlet</h1>
                </body>
                </html>
                """
        );

五十一、为什么后面不推荐 Servlet 拼大量 HTML

如果页面复杂:

几十行
几百行 HTML

全部:

writer.write(...)

非常难维护。

所以后来会学习:

JSP
模板
前后端分离
Vue

五十二、设置状态码

例如:

response.setStatus(
        200
);

或者:

response.setStatus(
        HttpServletResponse.SC_OK
);

五十三、常见状态码

200
成功

302
重定向

400
请求参数错误

401
未认证

403
无权限

404
资源不存在

500
服务器内部错误

五十四、sendError()

例如:

response.sendError(
        HttpServletResponse
                .SC_BAD_REQUEST,
        "参数错误"
);

五十五、Servlet 映射方式

主要两种:

注解配置

web.xml 配置

现代学习通常先:

@WebServlet

五十六、@WebServlet 单路径

@WebServlet(
        "/hello"
)

五十七、多个路径

@WebServlet(
        urlPatterns = {
                "/hello",
                "/hi"
        }
)

五十八、value 简写

@WebServlet(
        "/hello"
)

本质上是:

@WebServlet(
        value = "/hello"
)

五十九、web.xml 配置 Servlet

旧式配置:

<servlet>

    <servlet-name>
        helloServlet
    </servlet-name>

    <servlet-class>
        com.example.web.HelloServlet
    </servlet-class>

</servlet>

<servlet-mapping>

    <servlet-name>
        helloServlet
    </servlet-name>

    <url-pattern>
        /hello
    </url-pattern>

</servlet-mapping>

六十、为什么注解更方便

注解:

@WebServlet(
        "/hello"
)

不需要再单独写 XML。

优点:

配置和类靠近

更直观

减少 XML

六十一、为什么还要了解 web.xml

因为很多:

老项目

企业项目

框架配置

仍然会看到 web.xml。

而且:

Filter

Listener

欢迎页

错误页

也经常和 web.xml 有关系。


六十二、Servlet 生命周期

Servlet 生命周期是必须掌握的重点。

核心:

实例化
↓
init()
↓
service()
↓
destroy()

六十三、Servlet 创建

Tomcat 根据配置找到 Servlet 类后:

通过反射创建对象

这和前面学的:

反射

直接相关。


六十四、init()

Servlet 创建后:

public void init()

通常:

只执行一次

作用:

初始化资源

六十五、service()

每次有请求:

Tomcat 调用 service()

HttpServlet 的 service 会根据:

HTTP Method

进一步分发到:

doGet
doPost
doPut
doDelete

六十六、doGet 和 doPost 谁调用

通常不是 Tomcat 直接调用:

doGet()

而是:

Tomcat
↓
service()
↓
HttpServlet 判断请求方式
↓
doGet / doPost

六十七、destroy()

Tomcat:

关闭
重新部署
Servlet 被销毁

时会调用:

destroy()

通常:

只执行一次

六十八、生命周期示例

@WebServlet(
        "/life"
)
public class LifeServlet
        extends HttpServlet {

    @Override
    public void init() {

        System.out.println(
                "init"
        );
    }

    @Override
    protected void doGet(
            HttpServletRequest request,
            HttpServletResponse response
    )
            throws IOException {

        System.out.println(
                "doGet"
        );
    }

    @Override
    public void destroy() {

        System.out.println(
                "destroy"
        );
    }
}

访问多次:

init
doGet
doGet
doGet
...
destroy

六十九、Servlet 默认是不是每次 new

不是。

通常:

一个 Servlet 映射
由容器创建一个实例

多个请求可能:

共享同一个 Servlet 对象

七十、Servlet 线程安全问题

这是非常重要的。

假设:

public class CounterServlet
        extends HttpServlet {

    private int count = 0;

    protected void doGet(...) {

        count++;
    }
}

多个请求可能:

多个线程
同时访问同一个 Servlet 实例

所以:

成员变量可能出现线程安全问题

七十一、Servlet 中尽量不要用可变成员变量保存请求数据

错误:

private String username;

然后:

username =
        request.getParameter(
                "username"
        );

多个用户同时访问时:

可能互相覆盖

七十二、请求数据应该放局部变量

推荐:

protected void doPost(...) {

    String username =
            request.getParameter(
                    "username"
            );

}

局部变量:

每个线程自己的调用栈

相对安全。


七十三、为什么 Servlet 是线程安全重点

因为一个 Servlet 实例:

可能服务多个并发请求

这和我们前面学习的:

多线程
共享变量

直接相关。


七十四、转发 Forward

服务器内部转发:

request
        .getRequestDispatcher(
                "/result.jsp"
        )
        .forward(
                request,
                response
        );

七十五、转发特点

服务器内部跳转

浏览器地址栏不变

通常只有一次请求

request 数据可以继续使用

七十六、request.setAttribute()

转发前可以:

request.setAttribute(
        "message",
        "登录成功"
);

然后目标页面可以读取。


七十七、request.getAttribute()

Object message =
        request.getAttribute(
                "message"
        );

注意:

parameter
和
attribute
不是一回事

七十八、parameter 和 attribute 区别

parameter:

通常来自客户端请求

例如:

?name=张三
表单 input

获取:

getParameter()

attribute:

服务器内部主动保存

设置:

setAttribute()

读取:

getAttribute()

七十九、重定向 Redirect

response.sendRedirect(
        request.getContextPath()
                + "/login.html"
);

八十、重定向特点

服务器返回 302

浏览器重新发起新请求

浏览器地址栏变化

原 request 数据通常丢失

八十一、Forward 和 Redirect 对比

对比 Forward Redirect
请求次数 1 次 2 次
地址栏 不变 改变
request 属性 可以保留 通常不能保留
是否可跳外站 一般内部资源 可以
调用方式 RequestDispatcher sendRedirect

八十二、什么时候用转发

例如:

Servlet 查询数据
↓
把数据放 request
↓
转发 JSP 展示

八十三、什么时候用重定向

例如:

新增成功
↓
重定向到列表

这样避免:

刷新页面重复提交表单

八十四、PRG 思想

PRG:

Post
Redirect
Get

例如:

POST 新增学生
↓
处理成功
↓
302 Redirect
↓
GET 学生列表

优点:

刷新页面不会重复新增

八十五、一个登录 Servlet

@WebServlet(
        "/login"
)
public class LoginServlet
        extends HttpServlet {

    @Override
    protected void doPost(
            HttpServletRequest request,
            HttpServletResponse response
    )
            throws IOException,
            ServletException {

        request.setCharacterEncoding(
                "UTF-8"
        );

        response.setContentType(
                "text/html;charset=UTF-8"
        );

        String username =
                request.getParameter(
                        "username"
                );

        String password =
                request.getParameter(
                        "password"
                );

        if (
            "admin".equals(
                    username
            )
            &&
            "123456".equals(
                    password
            )
        ) {

            request.setAttribute(
                    "message",
                    "登录成功"
            );

            request
                    .getRequestDispatcher(
                            "/success.jsp"
                    )
                    .forward(
                            request,
                            response
                    );

            return;
        }

        response.sendRedirect(
                request.getContextPath()
                        + "/login.html"
        );
    }
}

八十六、为什么 “admin”.equals(username)

推荐:

"admin".equals(
        username
)

而不是:

username.equals(
        "admin"
)

因为 username 可能是:

null

前者不会 NPE。


八十七、Servlet 和 Service 分层

不推荐:

Servlet 里直接写大量 SQL

更合理:

Servlet
↓
Service
↓
Mapper / DAO
↓
Database

八十八、Servlet 主要负责什么

应该主要负责:

接收请求

获取参数

参数基础校验

调用 Service

根据结果决定响应

八十九、Service 负责什么

主要:

业务规则

事务

业务组合

例如:

注册校验

余额是否足够

订单状态转换

权限判断

九十、Mapper 负责什么

主要:

数据库访问

例如:

select
insert
update
delete

九十一、完整分层思路

浏览器
↓
Servlet
↓
Service
↓
Mapper
↓
MyBatis
↓
MySQL

九十二、学生查询 Servlet

@WebServlet(
        "/student/detail"
)
public class StudentDetailServlet
        extends HttpServlet {

    private final StudentService
            studentService =
            new StudentService();

    @Override
    protected void doGet(
            HttpServletRequest request,
            HttpServletResponse response
    )
            throws ServletException,
            IOException {

        String idText =
                request.getParameter(
                        "id"
                );

        if (
            idText == null
            || idText.isBlank()
        ) {

            response.sendError(
                    400,
                    "id 不能为空"
            );

            return;
        }

        Long id;

        try {

            id =
                    Long.valueOf(
                            idText
                    );

        } catch (
                NumberFormatException e
        ) {

            response.sendError(
                    400,
                    "id 格式错误"
            );

            return;
        }

        Student student =
                studentService
                        .findById(
                                id
                        );

        if (
            student == null
        ) {

            response.sendError(
                    404,
                    "学生不存在"
            );

            return;
        }

        request.setAttribute(
                "student",
                student
        );

        request
                .getRequestDispatcher(
                        "/student-detail.jsp"
                )
                .forward(
                        request,
                        response
                );
    }
}

九十三、参数校验顺序

推荐:

1. 是否存在

2. 是否为空

3. 格式是否合法

4. 业务是否合法

例如 id:

null
↓
blank
↓
Long 转换
↓
数据库是否存在

九十四、不要把所有异常都抛给浏览器

例如:

NumberFormatException

不应该直接让用户看到:

完整 Java 堆栈

应该转换成:

友好的 400 响应

九十五、GET 和 POST 的基础区别

GET:

主要用于获取资源

参数常见在 URL

可以被浏览器缓存/收藏

语义上应该是安全、幂等的读取

POST:

主要用于提交数据

参数常见在请求体

常用于新增、登录、复杂提交

九十六、不要只根据“参数在哪”理解 GET/POST

更重要的是:

HTTP 方法的业务语义

后面 RESTful 会系统学习:

GET
POST
PUT
DELETE

九十七、doGet 调 doPost 可以吗

学习阶段有人会写:

@Override
protected void doGet(...) {

    doPost(
            request,
            response
    );
}

技术上可以,

但如果 GET 和 POST 业务语义不同:

不建议简单混用

更好:

抽取公共私有方法

九十八、抽取公共处理方法

例如:

private void process(
        HttpServletRequest request,
        HttpServletResponse response
)
        throws IOException {

}

然后:

doGet(...)
doPost(...)

都调用:

process(...)

九十九、ServletConfig

每一个 Servlet 可以获得:

ServletConfig

例如:

ServletConfig config =
        getServletConfig();

可以读取:

Servlet 初始化参数

一百、@WebInitParam

@WebServlet(
        value = "/demo",
        initParams = {
                @WebInitParam(
                        name = "encoding",
                        value = "UTF-8"
                )
        }
)

Servlet:

String encoding =
        getServletConfig()
                .getInitParameter(
                        "encoding"
                );

一百零一、ServletContext

ServletContext context =
        getServletContext();

表示:

整个 Web 应用上下文

一个 Web 应用通常:

共享一个 ServletContext

一百零二、ServletContext 能做什么

例如:

读取应用级参数

获取真实路径

保存应用级共享数据

读取资源

一百零三、ServletContext 设置属性

context.setAttribute(
        "onlineCount",
        10
);

读取:

context.getAttribute(
        "onlineCount"
);

一百零四、不要随便把用户数据放 ServletContext

因为:

整个应用共享

例如:

当前登录用户

不应该放这里。

后面会学习:

Session

一百零五、三个常见作用域预览

request
一次请求

session
一次用户会话

application / ServletContext
整个应用

后面的:

Servlet 作用域
JSP
EL

会重点讲。


一百零六、ServletContext 读取资源

例如:

InputStream input =
        getServletContext()
                .getResourceAsStream(
                        "/WEB-INF/config.properties"
                );

一百零七、WEB-INF

WEB-INF 是 Web 项目特殊目录。

特点:

浏览器不能直接访问其中资源

但服务器内部可以访问。


一百零八、为什么 JSP 常放 WEB-INF

如果 JSP 放:

WEB-INF/views

浏览器不能直接:

通过 URL 绕过 Servlet 访问

必须:

经过 Servlet 转发

这样更容易控制流程。


一百零九、请求转发到 WEB-INF

request
        .getRequestDispatcher(
                "/WEB-INF/views/student.jsp"
        )
        .forward(
                request,
                response
        );

这是传统 MVC 很常见的做法。


一百一十、Servlet 异常处理

方法签名:

throws ServletException,
       IOException

常见:

IOException
输入输出异常

ServletException
Servlet 处理异常

一百一十一、包装业务异常

例如:

try {

    studentService.add(
            student
    );

} catch (
        Exception e
) {

    throw new ServletException(
            "新增学生失败",
            e
    );
}

这样保留:

原始 cause

一百一十二、不要 catch 后什么都不做

错误:

catch (
    Exception e
) {

}

这会导致:

页面失败
控制台没信息
难以排错

一百一十三、Tomcat 启动日志怎么看

重点关注:

Tomcat 版本

CATALINA_HOME

CATALINA_BASE

端口

部署项目路径

错误堆栈

一百一十四、CATALINA_HOME

表示:

Tomcat 安装目录

例如:

D:\apache-tomcat-8.5.65

一百一十五、CATALINA_BASE

表示:

当前 Tomcat 实例运行目录

如果使用 IDEA:

CATALINA_BASE
可能是 IDEA 自动创建的临时目录

所以:

HOME 和 BASE 不一样

是正常现象。


一百一十六、8080 端口被占用

错误类似:

Address already in use

localhost:8080 is already in use

说明:

已有程序监听 8080

一百一十七、Windows 查看 8080 端口

PowerShell:

netstat -ano | findstr :8080

找到 PID。

然后:

tasklist | findstr PID

确认进程。


一百一十八、结束占用进程

确认可以关闭后:

taskkill /PID 进程号 /F

不要:

看到 PID 就随便 kill

先确认是什么程序。


一百一十九、也可以修改 Tomcat 端口

server.xml:

<Connector
        port="8081"
        protocol="HTTP/1.1"
/>

然后访问:

http://localhost:8081

一百二十、404 是什么意思

404:

服务器找不到目标资源

Servlet 场景常见原因:

Tomcat 没启动

项目没部署

Context Path 写错

@WebServlet 路径写错

URL 写错

Servlet 类没加载

一百二十一、500 是什么意思

500:

服务器内部异常

常见:

NullPointerException

数据库异常

Servlet 初始化失败

Mapper 报错

代码逻辑异常

出现 500:

优先看 Tomcat 控制台堆栈

一百二十二、405 是什么意思

405:

Method Not Allowed

例如:

浏览器发 POST

Servlet 只实现 doGet

或者请求方法不被支持。


一百二十三、Servlet 不输出内容

如果访问 Servlet 后:

浏览器空白
控制台也没输出

检查:

URL 是否真的映射成功

doGet/doPost 是否进入

是否写 response

是否被重定向

是否抛异常

是否访问了错误 Context Path

可以先:

System.out.println(
        "进入 Servlet"
);

确认执行流程。


一百二十四、浏览器 Network 面板

按:

F12
→ Network

查看:

Request URL

Request Method

Status Code

Query String

Form Data

Response

这是 Web 调试必须掌握的工具。


一百二十五、请求路径拼错

例如项目 Context Path:

/demo

Servlet:

@WebServlet(
        "/hello"
)

正确:

http://localhost:8080/demo/hello

不是:

http://localhost:8080/hello

除非项目部署在根路径。


一百二十六、@WebServlet 路径必须以 / 开头

推荐:

@WebServlet(
        "/hello"
)

一百二十七、Servlet URL 映射模式

常见:

精确匹配
/hello

目录匹配
/api/*

扩展名匹配
*.do

默认匹配
/

一百二十八、精确匹配

@WebServlet(
        "/student/list"
)

只匹配:

/student/list

一百二十九、目录匹配

@WebServlet(
        "/student/*"
)

可以匹配:

/student/list

/student/add

/student/delete

一百三十、扩展名匹配

例如 web.xml:

*.do

可以匹配:

login.do

user.do

老项目常见。


一百三十一、一个 Servlet 处理多个 action

老项目有时:

@WebServlet(
        "/student/*"
)

然后:

String path =
        request.getPathInfo();

根据 path 判断:

/list
/add
/delete

后面 Spring MVC 会用更清晰的:

一个方法一个映射

一百三十二、Servlet 和 MVC

MVC:

Model
View
Controller

在传统 Java Web:

Servlet
通常充当 Controller

JavaBean / Service
属于 Model

JSP
属于 View

一百三十三、传统 MVC 流程

浏览器
↓
Servlet Controller
↓
Service
↓
Mapper
↓
Database
↓
Servlet
↓
request.setAttribute
↓
JSP
↓
HTML
↓
浏览器

一百三十四、为什么 Servlet 很重要

虽然现代项目大量使用:

Spring MVC
SpringBoot

但底层 Web 请求处理仍然建立在:

Servlet 规范

之上。

例如 Spring MVC 核心:

DispatcherServlet

本质上就是一个 Servlet。


一百三十五、DispatcherServlet 预览

Spring MVC:

所有请求
↓
DispatcherServlet
↓
Controller

所以后面看到:

前端控制器

就会更容易理解。


一百三十六、Servlet 和 SpringBoot 的关系

SpringBoot Web 项目启动后:

内嵌 Tomcat

然后:

Spring MVC

仍然基于 Servlet 体系工作。

所以学习 Servlet 是为了理解:

SpringBoot Web 底层请求模型

一百三十七、Servlet 线程模型

常见模型:

一个 Servlet 实例
+
多个请求线程

所以:

不要用成员变量保存请求状态

一百三十八、Servlet 中 static 变量更要谨慎

例如:

private static int count;

所有请求、所有实例都可能共享。

如果修改:

必须考虑线程安全

一百三十九、数据库连接不要放 Servlet 成员变量长期共享

错误思路:

private Connection connection;

然后所有请求共享同一个 Connection。

问题:

线程安全

事务混乱

连接失效

资源泄漏

应该使用:

连接池
每次业务获取连接
框架管理 SqlSession

一百四十、Servlet 中不要 new 很重的对象每个请求都做

例如:

每次请求都重新创建 SqlSessionFactory

不合理。

应该:

应用启动时创建一次

一百四十一、init() 适合做什么

例如:

读取初始化参数

初始化轻量共享配置

获取应用级对象

但大型项目通常交给:

Spring 容器

管理。


一百四十二、destroy() 适合做什么

例如:

释放长期资源

关闭后台线程

清理应用级资源

但数据库连接通常:

不要长期放 Servlet 里

一百四十三、响应 JSON 预览

Servlet 也可以返回:

{
    "code": 200,
    "message": "success"
}

例如:

response.setContentType(
        "application/json;charset=UTF-8"
);

response
        .getWriter()
        .write(
                """
                {
                    "code": 200,
                    "message": "success"
                }
                """
        );

一百四十四、真实项目 JSON 不建议手拼

后面通常使用:

Jackson

Gson

Spring MVC 自动序列化

把 Java 对象自动转成 JSON。


一百四十五、Servlet 处理前后端分离请求

流程:

Vue
↓
fetch / axios
↓
HTTP 请求
↓
Servlet / Spring MVC
↓
JSON
↓
Vue

所以 Servlet 并不是只能:

转发 JSP

它也可以:

直接返回 JSON

一百四十六、综合案例:学生列表 Servlet

@WebServlet(
        "/student/list"
)
public class StudentListServlet
        extends HttpServlet {

    private final StudentService
            studentService =
            new StudentService();

    @Override
    protected void doGet(
            HttpServletRequest request,
            HttpServletResponse response
    )
            throws ServletException,
            IOException {

        List<Student> students =
                studentService
                        .findAll();

        request.setAttribute(
                "students",
                students
        );

        request
                .getRequestDispatcher(
                        "/WEB-INF/views/student-list.jsp"
                )
                .forward(
                        request,
                        response
                );
    }
}

一百四十七、综合案例:新增学生 Servlet

@WebServlet(
        "/student/add"
)
public class StudentAddServlet
        extends HttpServlet {

    private final StudentService
            studentService =
            new StudentService();

    @Override
    protected void doPost(
            HttpServletRequest request,
            HttpServletResponse response
    )
            throws IOException {

        request.setCharacterEncoding(
                "UTF-8"
        );

        String studentNo =
                request.getParameter(
                        "studentNo"
                );

        String name =
                request.getParameter(
                        "name"
                );

        String ageText =
                request.getParameter(
                        "age"
                );

        if (
            studentNo == null
            || studentNo.isBlank()
            || name == null
            || name.isBlank()
        ) {

            response.sendError(
                    400,
                    "学号和姓名不能为空"
            );

            return;
        }

        Integer age = null;

        if (
            ageText != null
            && !ageText.isBlank()
        ) {

            try {

                age =
                        Integer.valueOf(
                                ageText
                        );

            } catch (
                    NumberFormatException e
            ) {

                response.sendError(
                        400,
                        "年龄格式错误"
                );

                return;
            }
        }

        Student student =
                new Student();

        student.setStudentNo(
                studentNo
        );

        student.setName(
                name
        );

        student.setAge(
                age
        );

        studentService.add(
                student
        );

        response.sendRedirect(
                request.getContextPath()
                        + "/student/list"
        );
    }
}

一百四十八、为什么新增后重定向

因为:

POST
↓
新增
↓
Redirect
↓
GET 列表

这样刷新:

不会重复执行新增

这就是 PRG 模式。


一百四十九、常见错误:响应已经提交后再转发

如果先:

response
        .getWriter()
        .write(
                "hello"
        );

然后又:

forward(...)

可能出现:

Cannot forward after response has been committed

一百五十、什么叫 response committed

当响应头和部分内容已经:

发送给客户端

就不能再:

随意修改状态
重定向
转发

一百五十一、常见错误:sendRedirect 后继续执行

例如:

response.sendRedirect(
        "/login"
);

// 后面继续大量逻辑

通常应该:

response.sendRedirect(...);

return;

避免后续逻辑继续执行。


一百五十二、常见错误:forward 后继续写响应

同样推荐:

forward(...);

return;

保持控制流清晰。


一百五十三、常见错误:参数名不一致

HTML:

<input
    name="studentNo"
>

Servlet:

request.getParameter(
        "student_no"
);

结果:

null

一定检查:

name 属性
和
getParameter 参数

一百五十四、常见错误:表单没有 name

HTML:

<input
    id="username"
>

只有:

id

没有:

name

表单提交时:

通常不会形成 username 参数

必须:

<input
    id="username"
    name="username"
>

一百五十五、id 和 name 区别

HTML:

id
前端 DOM 标识

name
表单提交参数名

不要混淆。


一百五十六、常见错误:action 路径错误

如果:

<form
    action="/login"
>

但项目 Context Path:

/demo

可能发到:

localhost:8080/login

而不是:

localhost:8080/demo/login

后面 JSP 可以动态拼:

contextPath

一百五十七、常见错误:Servlet API 包错

Tomcat 8.5:

javax.servlet

Tomcat 10+:

jakarta.servlet

如果版本不对应:

类加载失败
编译失败
404
部署失败

都可能出现。


一百五十八、常见错误:Servlet API 依赖没有 provided

如果把 Servlet API 重复打入 WAR:

可能和容器中的类冲突

标准项目常用:

<scope>provided</scope>

一百五十九、常见错误:项目启动但根路径 404

Tomcat 启动成功:

不代表你的项目路径一定正确

检查:

Deployment

Application context

Artifact 是否部署

一百六十、IDEA 中 Tomcat 配置思路

通常:

Run
→ Edit Configurations
→ Tomcat Server
→ Local

然后:

Deployment
→ 添加 Artifact

一百六十一、Application context

例如:

/demo

那么访问:

http://localhost:8080/demo/

Servlet:

@WebServlet(
        "/hello"
)

最终:

http://localhost:8080/demo/hello

一百六十二、端口和 Context Path 不要混淆

8080
是服务器端口

/demo
是项目 Context Path

/hello
是 Servlet 映射

完整地址:

http://localhost:8080/demo/hello

一百六十三、Servlet 调试建议

推荐顺序:

1. Tomcat 是否启动

2. 端口是否正确

3. 项目是否部署

4. Context Path 是否正确

5. Servlet 映射是否正确

6. 请求方法 GET/POST 是否正确

7. 控制台是否进入 Servlet

8. 参数是否正确

9. Service 是否正常

10. Mapper / SQL 是否正常

一百六十四、练习题 1:HelloServlet

要求:

访问 /hello

浏览器显示:
Hello Servlet

一百六十五、练习题 2:参数获取

访问:

/greet?name=张三

Servlet 输出:

你好,张三

一百六十六、练习题 3:计算器 Servlet

参数:

a
b

访问:

/calc?a=10&b=20

输出:

30

处理:

参数为空
数字格式错误

一百六十七、练习题 4:登录表单

HTML:

username
password

POST 到:

/login

Servlet 校验后:

成功转发
失败重定向

一百六十八、练习题 5:生命周期

重写:

init
doGet
destroy

观察 Tomcat 控制台执行顺序。


一百六十九、练习题 6:线程安全

创建成员变量:

private int count;

多次快速访问,

思考:

为什么共享变量存在并发问题

然后改成:

局部变量

一百七十、练习题 7:转发和重定向

分别实现:

/forward-demo

/redirect-demo

观察:

地址栏
请求次数
request attribute

区别。


一百七十一、练习题 8:学生详情

访问:

/student/detail?id=1

Servlet:

获取 id
参数校验
调用 Service
request.setAttribute
forward JSP

一百七十二、练习题 9:新增学生

表单 POST:

/student/add

成功后:

redirect 到 /student/list

一百七十三、练习题 10:返回 JSON

Servlet 返回:

{
    "code": 200,
    "message": "success"
}

并设置:

application/json;charset=UTF-8

一百七十四、必须掌握的类

HttpServlet

HttpServletRequest

HttpServletResponse

ServletConfig

ServletContext

RequestDispatcher

一百七十五、必须掌握的方法

request:

getParameter()

getParameterValues()

getParameterMap()

getMethod()

getRequestURI()

getContextPath()

getHeader()

setAttribute()

getAttribute()

getRequestDispatcher()

response:

setContentType()

setCharacterEncoding()

getWriter()

setStatus()

sendError()

sendRedirect()

Servlet:

init()

service()

doGet()

doPost()

destroy()

一百七十六、必须掌握的注解

@WebServlet

并理解:

urlPatterns
value
initParams

一百七十七、必须回答的问题

学完后应该能够回答:

1. Servlet 是什么?

2. Tomcat 是什么?

3. Servlet 为什么需要容器运行?

4. HttpServletRequest 是什么?

5. HttpServletResponse 是什么?

6. doGet 和 doPost 有什么区别?

7. request.getParameter 返回什么类型?

8. GET 和 POST 中文乱码分别怎么处理?

9. URIEncoding 写在哪里?

10. Forward 和 Redirect 有什么区别?

11. Servlet 生命周期是什么?

12. Servlet 为什么会有线程安全问题?

13. request parameter 和 attribute 有什么区别?

14. ServletContext 是什么?

15. WEB-INF 有什么特点?

16. @WebServlet 有什么作用?

17. 404 / 405 / 500 怎么排查?

18. Servlet 和 MVC 是什么关系?

19. Servlet 和 Spring MVC 有什么关系?

20. 为什么 Spring 的 DispatcherServlet 很重要?

一百七十八、Servlet 知识结构

Servlet
│
├─ 容器
│   └─ Tomcat
│
├─ 生命周期
│   ├─ init
│   ├─ service
│   ├─ doGet/doPost
│   └─ destroy
│
├─ Request
│   ├─ parameter
│   ├─ header
│   ├─ method
│   ├─ URI
│   └─ attribute
│
├─ Response
│   ├─ status
│   ├─ contentType
│   ├─ writer
│   ├─ sendError
│   └─ redirect
│
├─ 跳转
│   ├─ forward
│   └─ redirect
│
├─ 配置
│   ├─ @WebServlet
│   └─ web.xml
│
├─ 作用域
│   ├─ request
│   └─ ServletContext
│
└─ 分层
    ├─ Servlet
    ├─ Service
    ├─ Mapper
    └─ Database

一百七十九、从 Servlet 到 JSP

现在我们已经可以:

接收浏览器请求
获取参数
调用业务层
查询数据库
设置 request 数据

但如果 Servlet 自己拼 HTML:

writer.write(
        "<table>..."
);

非常难维护。

所以下一步会学习:

JSP

让:

Servlet
负责控制和业务调度

JSP
负责页面展示

一百八十、本章总结

Servlet 最核心的一句话:

Servlet 是运行在 Servlet 容器中的 Java Web 组件,用来接收 HTTP 请求并生成 HTTP 响应。

完整流程:

浏览器
↓
HTTP
↓
Tomcat
↓
Servlet
↓
Service
↓
Mapper / MyBatis
↓
MySQL
↓
Servlet
↓
Forward / JSON / Redirect
↓
浏览器

一定要掌握:

Tomcat
Servlet
HttpServletRequest
HttpServletResponse
@WebServlet
doGet
doPost
getParameter
中文编码
forward
redirect
生命周期
线程安全

尤其记住:

Servlet 对象通常不是每个请求创建一次

多个线程可能共享同一个 Servlet 实例

不要使用成员变量保存某个请求的数据

调试时按顺序排查:

Tomcat
↓
端口
↓
Deployment
↓
Context Path
↓
Servlet Mapping
↓
GET / POST
↓
参数
↓
Service
↓
Mapper
↓
SQL

这样 Servlet 阶段就不会像一堆:

Tomcat
8080
404
乱码
路径
请求
响应

混在一起。

下一篇按照课程表继续:

Servlet / JSP

重点进入:

JSP 页面
JSP 运行原理
JSP 脚本
指令
内置对象
Servlet + JSP MVC 配合