Servlet详解
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 配合