Java Web 基础学习笔记
本篇整理 Java Web 开发中的基础知识,包括 XML、HTTP、Tomcat、Servlet、Cookie 与 Session、监听器、过滤器、文件上传下载、JSP、EL、JSTL、JSON,以及一个商城项目中的典型设计。
XML
XML(Extensible Markup Language)是一种可扩展标记语言,能够描述层级化数据。它曾广泛用于程序间交换数据,现在仍常见于 Spring、Maven 等框架的配置文件。
基本语法
XML 文档通常以声明开头,并且有且只有一个根元素:
1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<students>
<student id="100">
<name>Alice</name>
<age>20</age>
</student>
</students>
需要注意:
- 标签必须正确闭合,标签名区分大小写。
- 属性值必须使用单引号或双引号包裹。
- 同一个标签中不能出现同名属性。
<、>、&等字符需要使用实体转义。
| 字符 | XML 实体 |
|---|---|
< |
< |
> |
> |
& |
& |
" |
" |
' |
' |
不希望内容被解析为 XML 标记时,可以使用 CDATA:
1
2
3
4
5
<![CDATA[
if (left < right && right > 0) {
// 原样保存
}
]]>
DOM 与 SAX
| 解析方式 | 特点 | 适用场景 |
|---|---|---|
| DOM | 将整个文档加载为树形结构,便于随机访问和修改 | 文件较小,需要增删改查 |
| SAX | 按事件顺序读取,不保存完整文档树 | 文件较大,只需顺序读取 |
DOM4J 对 DOM 操作进行了封装,API 较为简洁。
DOM4J 解析
创建或读取 Document:
1
2
3
4
5
6
7
8
9
10
11
12
SAXReader reader = new SAXReader();
// 从文件读取
Document document = reader.read(new File("students.xml"));
// 从文本读取
Document textDocument =
DocumentHelper.parseText("<student><name>Alice</name></student>");
// 创建新文档
Document newDocument = DocumentHelper.createDocument();
newDocument.addElement("students");
遍历元素:
1
2
3
4
5
6
7
8
9
Element root = document.getRootElement();
for (Element student : root.elements("student")) {
String id = student.attributeValue("id");
String name = student.elementText("name");
String age = student.elementText("age");
System.out.printf("%s, %s, %s%n", id, name, age);
}
增删改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 添加
Element student = root.addElement("student");
student.addAttribute("id", "300");
student.addElement("name").setText("Tom");
student.addElement("age").setText("21");
// 修改
Element first = root.elements("student").get(0);
first.element("age").setText("23");
// 删除
Element last = root.elements("student")
.get(root.elements("student").size() - 1);
last.getParent().remove(last);
保存文档:
1
2
3
4
5
6
7
8
9
10
11
OutputFormat format = OutputFormat.createPrettyPrint();
format.setEncoding("UTF-8");
try (
XMLWriter writer = new XMLWriter(
new FileOutputStream("students.xml"),
format
)
) {
writer.write(document);
}
HTTP 协议
HTTP 是浏览器与服务器交换资源的应用层协议。一次通信由请求和响应组成。
1
curl -i https://example.com/
HTTP 请求
典型请求报文:
1
2
3
4
5
6
7
GET /users?page=1 HTTP/1.1
Host: example.com
Accept: application/json
Accept-Language: zh-CN
Connection: keep-alive
Cookie: JSESSIONID=example-session-id
请求由以下部分组成:
- 请求行:请求方法、资源路径和协议版本。
- 请求头:客户端能力、认证信息、内容格式等元数据。
- 空行:分隔请求头和请求体。
- 请求体:POST、PUT 等请求携带的数据。
常见请求头:
| 请求头 | 作用 |
|---|---|
Host |
目标主机和端口 |
User-Agent |
客户端信息 |
Accept |
客户端能够接收的响应类型 |
Accept-Language |
客户端偏好的语言 |
Accept-Encoding |
客户端支持的压缩格式 |
Content-Type |
请求体的媒体类型 |
Content-Length |
请求体的字节长度 |
Cookie |
发送当前域名下匹配的 Cookie |
Referer |
当前请求来源页面 |
Origin |
发起跨域请求的源 |
HTTP 响应
典型响应报文:
1
2
3
4
5
6
7
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 53
Connection: keep-alive
<!doctype html>
<html><body><h1>Hello</h1></body></html>
常见响应头:
| 响应头 | 作用 |
|---|---|
Content-Type |
响应体的媒体类型和字符集 |
Content-Length |
响应体字节长度 |
Location |
重定向目标地址 |
Set-Cookie |
要求浏览器保存 Cookie |
ETag |
资源版本标识 |
Last-Modified |
资源最后修改时间 |
Cache-Control |
缓存策略 |
状态码
| 范围 | 含义 | 常见状态码 |
|---|---|---|
1xx |
信息响应 | 101 Switching Protocols |
2xx |
请求成功 | 200 OK、201 Created、204 No Content |
3xx |
重定向或缓存 | 301、302、304 |
4xx |
客户端错误 | 400、401、403、404 |
5xx |
服务端错误 | 500、502、503 |
302 响应通常携带 Location,浏览器根据它发起第二次请求。304 表示资源未修改,响应通常不再传输完整资源内容。
页面中每个外部资源都可能产生独立请求:
1
2
<img src="first.jpg" alt="">
<img src="second.jpg" alt="">
访问该页面至少会请求 HTML 和两张图片。
GET 与 POST
| 对比项 | GET | POST |
|---|---|---|
| 主要用途 | 查询资源 | 提交或修改数据 |
| 参数位置 | 通常在 URL 查询字符串 | 通常在请求体 |
| 浏览器缓存 | 更容易被缓存 | 默认不以相同方式缓存 |
| 幂等性 | 应保持幂等 | 不要求幂等 |
| 数据可见性 | URL 中可见 | 不直接显示在 URL |
URL 不适合保存密码等敏感数据,但 POST 也不等于加密。敏感通信仍必须使用 HTTPS。
MIME 类型
MIME 类型通过 Content-Type 描述数据格式:
| MIME 类型 | 数据 |
|---|---|
text/html |
HTML |
text/plain |
纯文本 |
text/css |
CSS |
application/json |
JSON |
application/xml |
XML |
application/octet-stream |
通用二进制文件 |
image/jpeg |
JPEG 图片 |
multipart/form-data |
包含文件的表单 |
Tomcat
Tomcat 是 Servlet 容器。它负责监听端口、解析 HTTP、创建请求和响应对象、管理 Servlet 生命周期,并将业务代码产生的结果写回客户端。
目录结构
| 目录 | 作用 |
|---|---|
bin |
启动和关闭脚本 |
conf |
server.xml、web.xml 等配置 |
lib |
Tomcat 运行所需类库 |
logs |
运行日志 |
webapps |
默认应用部署目录 |
work |
JSP 编译等运行时工作目录 |
temp |
临时文件 |
传统 Web 应用的基本结构:
1
2
3
4
5
6
7
8
9
webapp/
├── css/
├── js/
├── images/
├── index.html
└── WEB-INF/
├── classes/
├── lib/
└── web.xml
WEB-INF 下的资源不能被浏览器直接访问,只能由服务端转发或内部读取。
访问地址通常由以下部分组成:
1
http://主机:端口/应用上下文/资源路径
请求处理过程
- Tomcat 启动并监听端口。
- 客户端建立连接并发送 HTTP 请求。
- Tomcat 解析请求行、请求头和请求体。
- 静态资源交给默认 Servlet 处理。
- 动态请求根据映射找到业务 Servlet。
- Tomcat 创建
HttpServletRequest和HttpServletResponse。 - 容器在线程中调用 Servlet 的
service()。 - 响应数据被编码为 HTTP 响应并写回客户端。
简易 Servlet 容器的核心思路
手写简化版 Tomcat 可以帮助理解容器职责:
1
2
3
4
5
6
7
ServerSocket
-> 接收 Socket
-> 解析 HTTP 请求
-> 根据 URL 查找 Servlet
-> 首次访问时反射创建 Servlet
-> 调用 service(request, response)
-> 输出 HTTP 响应
简化接口:
1
2
3
4
5
6
7
public interface MyServlet {
void init();
void service(MyHttpRequest request, MyHttpResponse response);
void destroy();
}
根据请求方法分发:
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
public abstract class MyHttpServlet implements MyServlet {
@Override
public void service(
MyHttpRequest request,
MyHttpResponse response
) {
if ("GET".equals(request.getMethod())) {
doGet(request, response);
} else if ("POST".equals(request.getMethod())) {
doPost(request, response);
}
}
protected void doGet(
MyHttpRequest request,
MyHttpResponse response
) {
}
protected void doPost(
MyHttpRequest request,
MyHttpResponse response
) {
}
}
Servlet 映射可以保存在两个 Map 中:
1
2
3
4
5
private static final Map<String, String> URL_CLASS_MAP =
new HashMap<>();
private static final Map<String, MyServlet> URL_SERVLET_MAP =
new ConcurrentHashMap<>();
启动时解析 web.xml 得到 URL 与类名的映射,首次请求时通过反射创建对象:
1
2
3
4
5
6
7
8
9
10
public static MyServlet createServlet(String className)
throws ReflectiveOperationException {
Class<?> type = Class.forName(className);
Constructor<?> constructor = type.getDeclaredConstructor();
constructor.setAccessible(true);
MyServlet servlet = (MyServlet) constructor.newInstance();
servlet.init();
return servlet;
}
这只是帮助理解原理的教学模型。真实容器还要处理连接复用、协议解析、线程池、异步 IO、安全、类加载、会话和规范兼容等大量细节。
Servlet
Servlet 是 Java Web 的核心规范之一。开发者编写 Servlet 处理业务请求,容器负责实例化、初始化、调用和销毁。
生命周期
| 方法 | 调用时机 | 调用次数 |
|---|---|---|
init() |
Servlet 创建后 | 通常一次 |
service() |
每次匹配请求到达 | 多次 |
destroy() |
应用停止或 Servlet 被卸载 | 通常一次 |
Servlet 默认通常按单例管理,但会同时处理多个线程的请求,因此不要把请求相关的可变数据保存在 Servlet 实例字段中。
注解配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@WebServlet(
name = "helloServlet",
urlPatterns = {"/hello", "/greeting"},
loadOnStartup = 1
)
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(
HttpServletRequest request,
HttpServletResponse response
) throws IOException {
response.setContentType("text/plain;charset=UTF-8");
response.getWriter().println("Hello");
}
}
HttpServlet.service() 会根据 HTTP 方法分发到 doGet()、doPost() 等方法。
URL 映射
| 匹配方式 | 示例 | 说明 |
|---|---|---|
| 精确匹配 | /users |
只匹配指定路径 |
| 路径匹配 | /users/* |
匹配指定前缀 |
| 扩展名匹配 | *.action |
匹配指定后缀 |
| 默认匹配 | / |
可能影响静态资源处理 |
优先级通常为:精确匹配、最长路径匹配、扩展名匹配、默认匹配。
HttpServletRequest
HttpServletRequest 封装客户端请求。
| 方法 | 作用 |
|---|---|
getMethod() |
获取请求方法 |
getRequestURI() |
获取请求 URI |
getRequestURL() |
获取完整请求 URL |
getContextPath() |
获取应用上下文路径 |
getParameter(name) |
获取单值请求参数 |
getParameterValues(name) |
获取多值参数 |
getHeader(name) |
获取请求头 |
getCookies() |
获取 Cookie |
getRemoteAddr() |
获取客户端地址 |
setAttribute() / getAttribute() |
操作请求域数据 |
getRequestDispatcher() |
获取请求转发器 |
处理表单请求体中的中文参数时,应在读取参数前设置编码:
1
request.setCharacterEncoding("UTF-8");
HttpServletResponse
HttpServletResponse 用于构造响应:
1
2
3
4
response.setStatus(HttpServletResponse.SC_OK);
response.setHeader("X-Request-Id", requestId);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"success\":true}");
字符流和字节流不能在同一次响应中混用:
getWriter():输出文本。getOutputStream():输出图片、压缩包等二进制数据。
ServletConfig 与 ServletContext
| 对象 | 范围 | 作用 |
|---|---|---|
ServletConfig |
单个 Servlet | 获取该 Servlet 的初始化参数 |
ServletContext |
整个 Web 应用 | 获取应用参数、资源和应用域数据 |
Servlet 初始化参数:
1
2
3
4
5
6
7
8
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>com.example.HelloServlet</servlet-class>
<init-param>
<param-name>message</param-name>
<param-value>Hello</param-value>
</init-param>
</servlet>
1
2
String message = getServletConfig()
.getInitParameter("message");
应用级参数:
1
2
3
4
<context-param>
<param-name>applicationName</param-name>
<param-value>Java Web Demo</param-value>
</context-param>
1
2
3
4
ServletContext context = getServletContext();
String applicationName =
context.getInitParameter("applicationName");
String contextPath = context.getContextPath();
应用域数据:
1
2
3
context.setAttribute("onlineCount", 10);
Integer onlineCount =
(Integer) context.getAttribute("onlineCount");
转发与重定向
| 对比项 | 转发 | 重定向 |
|---|---|---|
| 发起方 | 服务器内部 | 浏览器 |
| 请求次数 | 一次 | 两次或更多 |
| 地址栏 | 不变 | 改变 |
| Request 域 | 可以共享 | 不能共享 |
WEB-INF |
可以转发进入 | 不能直接重定向进入 |
| 外部站点 | 不支持 | 支持 |
转发:
1
2
3
request.setAttribute("user", user);
request.getRequestDispatcher("/WEB-INF/views/user.jsp")
.forward(request, response);
重定向:
1
2
3
response.sendRedirect(
request.getContextPath() + "/users"
);
保存数据后通常采用 PRG(Post/Redirect/Get)模式,避免刷新页面时重复提交表单。
Cookie 与 Session
HTTP 本身是无状态协议。Cookie 和 Session 用于在多次请求之间识别用户和保存会话数据。
Cookie
服务器通过响应头要求浏览器保存 Cookie:
1
2
3
4
5
6
7
Cookie cookie = new Cookie("theme", "dark");
cookie.setMaxAge(7 * 24 * 60 * 60);
cookie.setPath(request.getContextPath());
cookie.setHttpOnly(true);
cookie.setSecure(true);
response.addCookie(cookie);
浏览器随后在匹配域名和路径的请求中携带:
1
Cookie: theme=dark; JSESSIONID=example-session-id
常用属性:
| 属性 | 作用 |
|---|---|
Max-Age / Expires |
控制过期时间 |
Path |
限制 Cookie 生效路径 |
Domain |
限制生效域名 |
HttpOnly |
禁止客户端 JavaScript 读取 |
Secure |
仅通过 HTTPS 发送 |
SameSite |
限制跨站请求携带方式 |
读取 Cookie:
1
2
3
4
5
6
7
8
9
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("theme".equals(cookie.getName())) {
System.out.println(cookie.getValue());
}
}
}
删除 Cookie 时需要使用相同名称和路径,并将存活时间设为 0。
Cookie 容量有限,不应存储密码、权限判断依据或大量数据。权限必须由服务端验证。
Session
Session 在服务端保存会话数据:
1
2
3
4
5
HttpSession session = request.getSession();
session.setAttribute("loginUser", user);
User loginUser =
(User) session.getAttribute("loginUser");
典型过程:
- 浏览器首次访问且没有有效
JSESSIONID。 - 服务端创建 Session,并生成唯一 ID。
- 响应通过
Set-Cookie写入JSESSIONID。 - 浏览器后续请求携带该 ID。
- 服务端根据 ID 找到对应 Session。
常用方法:
| 方法 | 作用 |
|---|---|
getId() |
获取会话 ID |
isNew() |
判断是否刚创建 |
setAttribute() |
保存会话数据 |
getAttribute() |
获取会话数据 |
removeAttribute() |
删除指定属性 |
setMaxInactiveInterval() |
设置最大空闲时间 |
invalidate() |
立即销毁 Session |
Session 超时表示两次访问之间允许的最大空闲时长,不是从创建开始累计的固定时长。
监听器与过滤器
监听器
监听器观察 ServletContext、Session、Request 的生命周期或属性变化。
| 监听器 | 监听内容 | 常见用途 |
|---|---|---|
ServletContextListener |
应用启动和停止 | 初始化资源、关闭资源 |
ServletContextAttributeListener |
应用域属性变化 | 记录共享状态 |
HttpSessionListener |
Session 创建和销毁 | 在线人数统计 |
HttpSessionAttributeListener |
Session 属性变化 | 登录状态审计 |
ServletRequestListener |
Request 创建和销毁 | 请求日志和耗时统计 |
ServletRequestAttributeListener |
Request 属性变化 | 调试和监控 |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@WebListener
public class ApplicationListener
implements ServletContextListener {
@Override
public void contextInitialized(
ServletContextEvent event
) {
System.out.println("application started");
}
@Override
public void contextDestroyed(
ServletContextEvent event
) {
System.out.println("application stopped");
}
}
Filter
Filter 可以在目标资源执行前后统一处理请求和响应,常用于:
- 登录与权限校验。
- 字符集处理。
- 请求日志和耗时统计。
- 跨域配置。
- 事务边界。
- 内容压缩或安全响应头。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@WebFilter("/*")
public class EncodingFilter implements Filter {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
chain.doFilter(request, response);
}
}
调用 chain.doFilter() 表示继续执行后续过滤器或目标资源;不调用就会中断请求链。
过滤器链
1
2
3
4
5
6
7
请求
-> Filter A 前置逻辑
-> Filter B 前置逻辑
-> Servlet / JSP
-> Filter B 后置逻辑
-> Filter A 后置逻辑
-> 响应
多个过滤器和目标资源处理的是同一次请求,并共享同一个 Request 对象。
传统 web.xml 可以配置 DispatcherType:
1
2
3
4
5
6
7
<filter-mapping>
<filter-name>AuthFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
登录鉴权示例
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
@WebFilter("/manage/*")
public class AuthFilter implements Filter {
@Override
public void doFilter(
ServletRequest servletRequest,
ServletResponse servletResponse,
FilterChain chain
) throws IOException, ServletException {
HttpServletRequest request =
(HttpServletRequest) servletRequest;
HttpServletResponse response =
(HttpServletResponse) servletResponse;
User user =
(User) request.getSession()
.getAttribute("loginUser");
if (user != null) {
chain.doFilter(request, response);
return;
}
response.sendRedirect(
request.getContextPath() + "/login.html"
);
}
}
文件上传与下载
文件上传
上传文件的表单必须使用 POST 和 multipart/form-data:
1
2
3
4
5
6
7
8
9
<form
action="/files/upload"
method="post"
enctype="multipart/form-data"
>
<input type="text" name="description">
<input type="file" name="file">
<button type="submit">上传</button>
</form>
Servlet 3 可以通过 Part 处理上传:
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
@MultipartConfig(
maxFileSize = 10 * 1024 * 1024,
maxRequestSize = 20 * 1024 * 1024
)
@WebServlet("/files/upload")
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(
HttpServletRequest request,
HttpServletResponse response
) throws IOException, ServletException {
Part part = request.getPart("file");
String submittedName = Path.of(
part.getSubmittedFileName()
).getFileName().toString();
String savedName =
UUID.randomUUID() + "-" + submittedName;
Path uploadDirectory = Path.of(
getServletContext().getRealPath("/uploads")
);
Files.createDirectories(uploadDirectory);
part.write(
uploadDirectory.resolve(savedName).toString()
);
response.sendRedirect(
request.getContextPath() + "/upload-success.html"
);
}
}
上传功能需要额外防护:
- 限制文件大小、数量和允许类型。
- 不信任客户端文件名,防止路径穿越。
- 使用随机名称避免覆盖。
- 上传目录尽量与可执行代码隔离。
- 必要时检查文件真实内容,而不只检查扩展名。
文件下载
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
Path baseDirectory = Path.of(
getServletContext().getRealPath("/downloads")
).normalize();
String requestedName = request.getParameter("filename");
Path file = baseDirectory.resolve(requestedName).normalize();
if (!file.startsWith(baseDirectory) || !Files.isRegularFile(file)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
String contentType = Files.probeContentType(file);
response.setContentType(
contentType != null
? contentType
: "application/octet-stream"
);
String encodedName = URLEncoder.encode(
file.getFileName().toString(),
StandardCharsets.UTF_8
).replace("+", "%20");
response.setHeader(
"Content-Disposition",
"attachment; filename*=UTF-8''" + encodedName
);
response.setContentLengthLong(Files.size(file));
try (InputStream input = Files.newInputStream(file)) {
input.transferTo(response.getOutputStream());
}
下载接口同样需要验证路径,不能直接把用户输入拼接为任意磁盘路径。
Web 应用中的路径
路径最容易混淆的是第一个 / 由谁解析。
浏览器解析
浏览器把 / 解释为当前站点根路径:
1
<a href="/app/users">用户列表</a>
对应:
1
http://主机:端口/app/users
服务端解析
服务端转发中的 / 通常相对于当前 Web 应用:
1
2
request.getRequestDispatcher("/WEB-INF/views/users.jsp")
.forward(request, response);
推荐写法
JSP 中使用上下文路径生成客户端地址:
1
2
3
<a href="${pageContext.request.contextPath}/users">
用户列表
</a>
重定向也应包含上下文路径:
1
2
3
response.sendRedirect(
request.getContextPath() + "/users"
);
<base> 可以改变相对地址解析基准,但容易影响页面中所有链接,使用时要谨慎。
JSP、EL 与 JSTL
JSP(JavaServer Pages)是一种服务端页面模板。Tomcat 首次访问 JSP 时,会将其转换为 Servlet 源码并编译。
现代项目更常使用前后端分离或 Thymeleaf 等模板技术,但理解 JSP 有助于理解服务端渲染和 Servlet 运行机制。
JSP 指令
1
2
3
4
5
<%@ page
contentType="text/html;charset=UTF-8"
pageEncoding="UTF-8"
language="java"
%>
JSP 传统脚本:
| 语法 | 作用 |
|---|---|
<%! ... %> |
声明成员 |
<% ... %> |
编写 Java 代码 |
<%= ... %> |
输出表达式 |
<%-- ... --%> |
JSP 注释 |
不建议在 JSP 中大量编写 Java 代码,应优先使用 EL 和 JSTL。
EL 表达式
EL(Expression Language)用于读取域对象数据:
1
2
3
${requestScope.user.name}
${sessionScope.loginUser.username}
${applicationScope.applicationName}
未指定域时,按以下顺序查找:
1
page -> request -> session -> application
常用运算:
1
2
3
${empty requestScope.users}
${user.age >= 18 ? "成年" : "未成年"}
${pageContext.request.contextPath}
JSTL
引入核心标签库:
1
2
3
4
<%@ taglib
prefix="c"
uri="http://java.sun.com/jsp/jstl/core"
%>
条件:
1
2
3
<c:if test="${not empty sessionScope.loginUser}">
欢迎,${sessionScope.loginUser.username}
</c:if>
多分支:
1
2
3
4
5
6
7
8
9
10
11
<c:choose>
<c:when test="${score >= 80}">
优秀
</c:when>
<c:when test="${score >= 60}">
及格
</c:when>
<c:otherwise>
不及格
</c:otherwise>
</c:choose>
遍历:
1
2
3
<c:forEach items="${requestScope.users}" var="user">
<p>${user.id} - ${user.name}</p>
</c:forEach>
JSP 内置对象
| 对象 | 类型或作用 |
|---|---|
request |
HttpServletRequest |
response |
HttpServletResponse |
session |
HttpSession |
application |
ServletContext |
pageContext |
当前 JSP 上下文和页面域 |
config |
ServletConfig |
out |
JspWriter |
page |
当前 Servlet 实例 |
exception |
错误页面中的异常对象 |
四大域对象
| 域 | 有效范围 |
|---|---|
pageContext |
当前 JSP 页面 |
request |
当前请求,包括内部转发 |
session |
当前会话中的多次请求 |
application |
整个 Web 应用运行期间 |
数据范围从小到大:
1
pageContext < request < session < application
应尽量把数据放在满足需求的最小范围中。
JSON 与 Jackson
Jackson 可以完成 Java 对象与 JSON 之间的转换。
序列化
1
2
3
4
ObjectMapper objectMapper = new ObjectMapper();
Book book = new Book(1L, "Java Web");
String json = objectMapper.writeValueAsString(book);
反序列化
单个对象:
1
2
3
4
Book book = objectMapper.readValue(
json,
Book.class
);
列表:
1
2
3
4
5
List<Book> books = objectMapper.readValue(
json,
new TypeReference<List<Book>>() {
}
);
Map:
1
2
3
4
5
Map<String, Book> books = objectMapper.readValue(
json,
new TypeReference<Map<String, Book>>() {
}
);
实体类通常需要可访问的属性、getter/setter 或明确的 Jackson 注解。
Servlet 返回 JSON:
1
2
3
4
5
6
7
8
response.setContentType(
"application/json;charset=UTF-8"
);
objectMapper.writeValue(
response.getOutputStream(),
result
);
商城项目实践
MVC 与分层架构
| 层 | 职责 |
|---|---|
| View | 展示页面并收集用户输入 |
| Controller / Servlet | 接收请求、校验参数、调用业务层、组织响应 |
| Service | 实现业务规则和事务边界 |
| DAO | 访问数据库 |
| Domain / DTO / VO | 在不同层之间承载数据 |
| Util | 放置无状态通用工具 |
MVC 的核心目的是分离展示、调度和业务数据,让各层可以独立维护和测试。
合并同类 Servlet
如果每个操作都创建一个 Servlet,类数量会快速增加。可以让一个资源 Servlet 根据路径末尾分发到不同业务方法。
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
public abstract class BaseServlet extends HttpServlet {
@Override
protected void service(
HttpServletRequest request,
HttpServletResponse response
) throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
String uri = request.getRequestURI();
String methodName =
uri.substring(uri.lastIndexOf('/') + 1);
try {
Method method = getClass().getMethod(
methodName,
HttpServletRequest.class,
HttpServletResponse.class
);
method.invoke(this, request, response);
} catch (ReflectiveOperationException exception) {
throw new ServletException(exception);
}
}
}
该方案适合学习反射分发原理。成熟项目通常使用 Spring MVC 等框架完成路由和参数绑定。
参数绑定与服务端校验
客户端校验只能改善交互,不能替代服务端校验。服务端至少要检查:
- 必填值是否为空。
- 字符串长度和格式。
- 数字范围。
- 枚举值是否合法。
- 当前用户是否有操作权限。
- 数据库中的资源是否存在。
BeanUtils.populate() 可以把请求参数映射到 JavaBean,但类型转换失败和默认值可能掩盖错误,仍需显式校验。
分页模型
分页结果通常包含:
1
2
3
4
5
6
7
public class PageResult<T> {
private int pageNumber;
private int pageSize;
private long totalRows;
private long totalPages;
private List<T> records;
}
总页数:
1
2
long totalPages =
(totalRows + pageSize - 1) / pageSize;
登录与权限
登录成功后,把用户身份保存到 Session:
1
2
request.getSession()
.setAttribute("loginUser", user);
Filter 根据登录状态和权限控制路径访问。前端是否显示菜单不能作为权限依据,服务端必须再次验证。
验证码
验证码的一般流程:
- 服务端生成验证码文本和图片。
- 文本保存到 Session,图片返回浏览器。
- 用户提交验证码。
- 服务端读取并立即删除 Session 中的验证码。
- 比较用户输入和服务端保存值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private boolean checkCode(HttpServletRequest request) {
String input = request.getParameter("code");
HttpSession session = request.getSession();
String expected = (String) session.getAttribute(
Constants.KAPTCHA_SESSION_KEY
);
session.removeAttribute(
Constants.KAPTCHA_SESSION_KEY
);
return expected != null
&& expected.equalsIgnoreCase(input);
}
刷新验证码图片时可以追加时间戳,避免浏览器复用缓存:
1
2
3
image.src = contextPath
+ "/captcha?t="
+ Date.now();
购物车设计
Session 版购物车适合未登录用户:
1
2
3
4
5
6
7
8
Cart
├── items: Map<productId, CartItem>
├── addItem()
├── removeItem()
├── updateCount()
├── clear()
├── getTotalCount()
└── getTotalPrice()
CartItem 通常包含商品 ID、名称、图片、单价、数量和小计。
订单模型
用户与订单是一对多关系,订单与订单项也是一对多关系。
订单表:
1
2
3
4
5
6
7
8
create table tbl_order (
id bigint primary key auto_increment,
order_code varchar(32) not null unique,
user_id bigint not null,
amount decimal(12, 2) not null default 0,
status tinyint not null default 0,
create_time timestamp not null default current_timestamp
);
订单项表:
1
2
3
4
5
6
7
8
9
create table tbl_order_item (
id bigint primary key auto_increment,
order_code varchar(32) not null,
product_id bigint not null,
product_name varchar(128) not null,
quantity int not null,
price decimal(12, 2) not null,
total_price decimal(12, 2) not null
);
下单过程通常需要在一个事务中完成:
- 验证用户和购物车。
- 查询并校验库存。
- 创建订单。
- 创建订单项。
- 扣减库存并更新销量。
- 提交事务。
- 清空购物车。
Filter 与 ThreadLocal 管理事务
一次同步请求中的 Servlet、Service 和 DAO 通常在同一线程执行,可以使用 ThreadLocal<Connection> 让各层共享同一个数据库连接。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static final ThreadLocal<Connection> CONNECTION_HOLDER =
new ThreadLocal<>();
public static Connection getConnection() throws SQLException {
Connection connection = CONNECTION_HOLDER.get();
if (connection == null) {
connection = dataSource.getConnection();
connection.setAutoCommit(false);
CONNECTION_HOLDER.set(connection);
}
return connection;
}
提交与回滚必须清理 ThreadLocal:
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
public static void commitAndClose() throws SQLException {
Connection connection = CONNECTION_HOLDER.get();
try {
if (connection != null) {
connection.commit();
}
} finally {
closeAndRemove(connection);
}
}
public static void rollbackAndClose() throws SQLException {
Connection connection = CONNECTION_HOLDER.get();
try {
if (connection != null) {
connection.rollback();
}
} finally {
closeAndRemove(connection);
}
}
private static void closeAndRemove(Connection connection)
throws SQLException {
try {
if (connection != null) {
connection.close();
}
} finally {
CONNECTION_HOLDER.remove();
}
}
事务过滤器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@WebFilter("/*")
public class TransactionFilter implements Filter {
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
try {
chain.doFilter(request, response);
JdbcUtils.commitAndClose();
} catch (Exception exception) {
try {
JdbcUtils.rollbackAndClose();
} catch (SQLException rollbackException) {
exception.addSuppressed(rollbackException);
}
throw new ServletException(exception);
}
}
}
线程池会复用线程,所以无论成功还是失败,都必须调用 remove()。
错误页面
传统 web.xml 可以统一配置错误页面:
1
2
3
4
5
6
7
8
9
<error-page>
<error-code>404</error-code>
<location>/views/error/404.html</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/views/error/500.html</location>
</error-page>
异常必须继续向容器传播,否则统一错误处理无法感知。
AJAX 与重定向
Servlet 对 AJAX 请求调用 sendRedirect(),改变的是该 AJAX 请求的响应过程,不会自动让顶层浏览器页面跳转。
服务端可以返回跳转地址:
1
2
3
4
{
"success": false,
"redirectUrl": "/login.html"
}
前端显式跳转:
1
2
3
4
5
6
7
8
9
10
11
fetch("/cart/items")
.then((response) => response.json())
.then((result) => {
if (result.redirectUrl) {
window.location.href = result.redirectUrl;
return;
}
document.querySelector("#cart-count").textContent =
result.totalCount;
});
这种方式比依赖 AJAX 请求内部的转发或重定向更清晰。