【JAVA项目实战】【图书管理系统】借阅管理功能【Servlet】+【Ajax】+【MySql】+【Session】
'# 【JAVA项目实战】【图书管理系统】借阅管理功能【Servlet】+【Ajax】+【MySql】+【Session】
一、背景与问题
图书管理系统是典型的业务系统,其核心功能之一是借阅管理。在实际开发中,借阅管理需要处理以下关键问题:
- 用户身份验证:需要确保只有登录用户才能操作借阅功能
- 并发控制:同一本书可能被多个用户同时借阅,需要确保库存更新的原子性
- 状态同步:需要实时更新图书状态(如是否可借)
- 用户体验优化:避免页面刷新,通过Ajax实现无刷新操作
传统Web开发中,Servlet作为后端处理逻辑的基石,结合Ajax实现异步通信,配合MySQL存储数据,通过Session管理用户状态,构成了一个完整的解决方案。本文将深入探讨这种技术组合的工作原理、实现细节和工程实践。
二、基本原理
1. Servlet的工作机制
Servlet是Java Web开发的核心组件,其工作流程如下:
- 客户端发送HTTP请求到服务器
- 服务器根据URL匹配Servlet映射
- Web容器加载Servlet并初始化
- Servlet处理请求,生成响应
- 响应返回给客户端
Servlet的生命周期包含init()、service()、destroy()三个核心方法,其中service()负责处理所有HTTP请求类型。
2. Ajax的异步通信
Ajax(Asynchronous JavaScript and XML)通过XMLHttpRequest对象实现异步通信,其核心原理是:
- 通过JavaScript发起HTTP请求
- 服务器返回数据(通常是JSON格式)
- 前端动态更新页面内容
这种模式避免了页面整体刷新,提高了用户体验。
3. Session机制
Session是服务器端存储用户状态的机制,其工作原理如下:
- 服务器生成唯一Session ID
- 通过Cookie或URL重写将Session ID发送给客户端
- 客户端后续请求携带Session ID
- 服务器根据Session ID查找对应的会话数据
Session的生命周期由服务器配置决定,通常设置为30分钟。
4. MySQL的事务管理
在借阅操作中,需要确保:
- 检查图书库存
- 更新库存状态
- 记录借阅日志
这三个操作需要放在一个事务中,使用BEGIN TRANSACTION和COMMIT保证原子性。
三、环境准备
1. 开发环境配置
- JDK 1.8+
- Apache Tomcat 9.x
- MySQL 8.x
- IDE:IntelliJ IDEA 或 Eclipse
- 构建工具:Maven
2. 数据库设计
CREATE DATABASE library;
USE library;
-- 用户表
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(100) NOT NULL,
role VARCHAR(10) NOT NULL
);
-- 图书表
CREATE TABLE books (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(100) NOT NULL,
author VARCHAR(100) NOT NULL,
category VARCHAR(50),
stock INT DEFAULT 10,
status ENUM('available', 'borrowed') DEFAULT 'available'
);
-- 借阅记录表
CREATE TABLE borrow_records (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
book_id INT,
borrow_time DATETIME,
return_time DATETIME,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (book_id) REFERENCES books(id)
);3. Maven依赖
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
</dependencies>四、核心实现
1. Servlet处理借阅请求
@WebServlet("/borrow")
public class BorrowServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private Connection connection;
@Override
public void init() throws ServletException {
try {
// 初始化数据库连接
String url = "jdbc:mysql://localhost:3306/library?useSSL=false&serverTimezone=UTC";
String user = "root";
String password = "password";
connection = DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
throw new ServletException("数据库连接失败", e);
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取用户信息
HttpSession session = request.getSession();
Integer userId = (Integer) session.getAttribute("user_id");
if (userId == null) {
response.getWriter().write("{\"error\": \"请先登录\"}");
return;
}
// 获取图书ID
String bookIdStr = request.getParameter("bookId");
int bookId;
try {
bookId = Integer.parseInt(bookIdStr);
} catch (NumberFormatException e) {
response.getWriter().write("{\"error\": \"无效的图书ID\"}");
return;
}
// 检查图书库存
String checkQuery = "SELECT stock, status FROM books WHERE id = ?";
try (PreparedStatement stmt = connection.prepareStatement(checkQuery)) {
stmt.setInt(1, bookId);
ResultSet rs = stmt.executeQuery();
if (!rs.next()) {
response.getWriter().write("{\"error\": \"图书不存在\"}");
return;
}
int stock = rs.getInt("stock");
String status = rs.getString("status");
if (status.equals("borrowed") || stock <= 0) {
response.getWriter().write("{\"error\": \"该书已借出\"}");
return;
}
} catch (SQLException e) {
response.getWriter().write("{\"error\": \"数据库查询失败\"}");
return;
}
// 开始事务
try {
connection.setAutoCommit(false);
// 更新图书库存
String updateQuery = "UPDATE books SET stock = stock - 1, status = 'borrowed' WHERE id = ?";
try (PreparedStatement stmt = connection.prepareStatement(updateQuery)) {
stmt.setInt(1, bookId);
stmt.executeUpdate();
}
// 记录借阅日志
String insertQuery = "INSERT INTO borrow_records (user_id, book_id, borrow_time) VALUES (?, ?, NOW())";
try (PreparedStatement stmt = connection.prepareStatement(insertQuery)) {
stmt.setInt(1, userId);
stmt.setInt(2, bookId);
stmt.executeUpdate();
}
connection.commit();
response.getWriter().write("{\"success\": true}");
} catch (SQLException e) {
try {
connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
response.getWriter().write("{\"error\": \"操作失败\"}");
} finally {
try {
connection.setAutoCommit(true);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}关键代码解释:
init()方法初始化数据库连接,使用连接池可以进一步优化- 通过
HttpSession获取用户身份,确保只有登录用户才能操作 - 使用PreparedStatement防止SQL注入
- 事务管理确保库存更新和日志记录的原子性
- 异常处理机制防止数据库连接泄漏
2. Ajax前端交互
<!-- 借阅按钮 -->
<button id="borrowBtn" onclick="borrowBook(123)">借阅</button>
<script>
function borrowBook(bookId) {
const xhr = new XMLHttpRequest();
xhr.open("POST", "/borrow", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.success) {
alert("借阅成功");
} else {
alert("错误: " + response.error);
}
}
}
};
xhr.send("bookId=" + encodeURIComponent(bookId));
}
</script>关键点:
- 设置
Content-Type为application/x-www-form-urlencoded,确保服务器正确解析 - 使用
encodeURIComponent防止特殊字符破坏请求参数 - 异步请求不会阻塞页面加载
3. Session管理实现
// 用户登录Servlet
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private Connection connection;
@Override
public void init() throws ServletException {
try {
String url = "jdbc:mysql://localhost:3306/library?useSSL=false&serverTimezone=UTC";
String user = "root";
String password = "password";
connection = DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
throw new ServletException("数据库连接失败", e);
}
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
String query = "SELECT id FROM users WHERE username = ? AND password = ?";
try (PreparedStatement stmt = connection.prepareStatement(query)) {
stmt.setString(1, username);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
HttpSession session = request.getSession();
session.setAttribute("user_id", rs.getInt("id"));
response.sendRedirect("books.jsp");
} else {
response.getWriter().write("登录失败");
}
} catch (SQLException e) {
response.getWriter().write("数据库错误");
}
}
}关键实现:
- 使用PreparedStatement进行用户验证
- 通过
HttpSession存储用户ID - 使用
sendRedirect进行页面跳转
五、完整案例
1. 系统架构图
+-------------------+ +-------------------+ +-------------------+
| 用户浏览器 | | 前端页面 | | Servlet层 |
| (Chrome/Firefox) | | (HTML/CSS/JS) | | (BorrowServlet) |
+----------+--------+ +----------+--------+ +----------+--------+
| | |
| | |
v v v
+-------------------+ +-------------------+ +-------------------+
| Tomcat服务器 | | 数据库连接池 | | MySQL数据库 |
| (Servlet容器) | | (连接池配置) | | (library数据库) |
+-------------------+ +-------------------+ +-------------------+2. 完整案例流程
- 用户访问
login.jsp输入用户名密码 LoginServlet验证用户,成功后创建Session- 跳转至
books.jsp,展示图书列表 - 点击"借阅"按钮触发Ajax请求
BorrowServlet处理借阅逻辑,更新数据库- 返回成功响应后,前端更新页面状态
3. 完整代码示例
<!-- books.jsp -->
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.util.List" %>
<%@ page import="java.util.ArrayList" %>
<%@ page import="java.sql.*" %>
<%
HttpSession session = request.getSession();
Integer userId = (Integer) session.getAttribute("user_id");
if (userId == null) {
response.sendRedirect("login.jsp");
return;
}
Connection conn = null;
try {
String url = "jdbc:mysql://localhost:3306/library?useSSL=false&serverTimezone=UTC";
String user = "root";
String password = "password";
conn = DriverManager.getConnection(url, user, password);
String query = "SELECT * FROM books";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query);
List<Book> books = new ArrayList<>();
while (rs.next()) {
Book book = new Book();
book.setId(rs.getInt("id"));
book.setTitle(rs.getString("title"));
book.setAuthor(rs.getString("author"));
book.setStock(rs.getInt("stock"));
book.setStatus(rs.getString("status"));
books.add(book);
}
%>
<!DOCTYPE html>
<html>
<head>
<title>图书列表</title>
</head>
<body>
<h1>图书列表</h1>
<table border="1">
<tr>
<th>书名</th>
<th>作者</th>
<th>库存</th>
<th>状态</th>
<th>操作</th>
</tr>
<% for (Book book : books) { %>
<tr>
<td><%= book.getTitle() %></td>
<td><%= book.getAuthor() %></td>
<td><%= book.getStock() %></td>
<td><%= book.getStatus() %></td>
<td>
<button onclick="borrowBook(<%= book.getId() %>)">借阅</button>
</td>
</tr>
<% } %>
</table>
<script>
function borrowBook(bookId) {
const xhr = new XMLHttpRequest();
xhr.open("POST", "/borrow", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
const response = JSON.parse(xhr.responseText);
if (response.success) {
alert("借阅成功");
// 刷新页面
location.reload();
} else {
alert("错误: " + response.error);
}
}
}
};
xhr.send("bookId=" + encodeURIComponent(bookId));
}
</script>
</body>
</html>
<%
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
%>六、源码解析
1. Servlet的生命周期管理
在init()方法中初始化数据库连接,这里可以改进为使用连接池:
// 使用连接池配置
String url = "jdbc:mysql://localhost:3306/library?useSSL=false&serverTimezone=UTC";
String user = "root";
String password = "password";
// 使用HikariCP连接池
HikariConfig config = new HikariConfig();
config.setJdbcUrl(url);
config.setUsername(user);
config.setPassword(password);
connectionPool = new HikariDataSource(config);2. 事务管理的细节
在事务处理中,需要处理可能出现的异常:
try {
connection.setAutoCommit(false);
// 执行数据库操作
connection.commit();
} catch (SQLException e) {
try {
connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
response.getWriter().write("操作失败");
} finally {
try {
connection.setAutoCommit(true);
} catch (SQLException e) {
e.printStackTrace();
}
}3. SQL注入防范
使用PreparedStatement防止SQL注入:
String query = "SELECT * FROM books WHERE id = ?";
PreparedStatement stmt = connection.prepareStatement(query);
stmt.setInt(1, bookId);
ResultSet rs = stmt.executeQuery();七、进阶使用
1. 增加日志记录
在借阅操作时记录详细日志:
String logQuery = "INSERT INTO borrow_logs (user_id, book_id, action, timestamp) VALUES (?, ?, 'borrow', NOW())";
PreparedStatement stmt = connection.prepareStatement(logQuery);
stmt.setInt(1, userId);
stmt.setInt(2, bookId);
stmt.executeUpdate();2. 增加事务回滚机制
在异常捕获时添加更详细的错误日志:
catch (SQLException e) {
// 记录错误日志
logger.error("借阅操作失败: " + e.getMessage(), e);
try {
connection.rollback();
} catch (SQLException ex) {
ex.printStackTrace();
}
response.getWriter().write("操作失败");
}3. 增加并发控制
使用乐观锁控制库存更新:
String updateQuery = "UPDATE books SET stock = stock - 1, status = 'borrowed', last_modified = NOW() WHERE id = ? AND stock > 0";
PreparedStatement stmt = connection.prepareStatement(updateQuery);
stmt.setInt(1, bookId);
int rows = stmt.executeUpdate();
if (rows == 0) {
response.getWriter().write("库存不足");
}八、性能与工程实践
1. 性能优化方案
数据库优化:
- 为
books表的id和status字段创建索引 - 使用
EXPLAIN分析查询计划 - 对频繁查询的字段建立复合索引
- 为
缓存优化:
- 使用Redis缓存热门图书信息
- 对借阅状态进行缓存,减少数据库访问
连接池配置:
- 使用HikariCP连接池
- 设置最大连接数为100
- 配置空闲连接超时时间
2. 安全风险分析
SQL注入:
- 原因:直接拼接SQL语句
- 防范:使用PreparedStatement
XSS攻击:
- 原因:未过滤用户输入
- 防范:使用
htmlspecialchars()函数转义
CSRF攻击:
- 原因:未验证请求来源
- 防范:添加CSRF Token验证
3. 性能调优实践
-- 创建索引
CREATE INDEX idx_books_status ON books(status);
CREATE INDEX idx_books_stock ON books(stock);
-- 查询优化
EXPLAIN SELECT * FROM books WHERE status = 'available' AND stock > 0;九、常见问题与踩坑
1. Session失效问题
错误现象:用户登录后刷新页面,提示未登录
原因分析:Session未正确维护,或配置了过短的会话超时时间
解决方案:
- 在
web.xml中配置Session超时时间 - 使用
HttpSession.setMaxInactiveInterval(3600)设置1小时超时 - 使用Cookie存储Session ID时确保域名正确
2. Ajax请求跨域问题
错误现象:浏览器报No 'Access-Control-Allow-Origin' header错误
解决方案:
在Servlet中添加CORS头
response.setHeader("Access-Control-Allow-Origin", "*"); response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS"); response.setHeader("Access-Control-Allow-Headers", "Content-Type");
3. 数据库连接泄漏
错误现象:应用运行一段时间后出现Too many open connections错误
解决方案:
- 使用连接池代替直接连接
- 在
finally块中关闭连接 - 配置连接池的最大连接数和空闲超时
十、最佳实践
1. 推荐实践
- 使用连接池:推荐使用HikariCP或Druid,配置最大连接数和空闲超时
- 事务管理:关键操作必须放在事务中,确保数据一致性
- 安全措施:使用PreparedStatement防止SQL注入,转义用户输入防止XSS
- 日志记录:关键操作记录日志,便于排查问题
- 性能优化:对高频查询建立索引,使用缓存减少数据库访问
2. 不推荐实践
- 直接使用数据库连接:可能导致连接泄漏和性能问题
- 不使用事务:在库存更新和日志记录等操作中不使用事务
- 不处理异常:忽略异常可能导致程序崩溃和数据不一致
- 不进行输入验证:可能导致SQL注入和XSS攻击
十一、总结
本文深入探讨了基于Servlet、Ajax、MySQL和Session的图书管理系统借阅管理功能的实现。通过具体代码示例,分析了各个技术组件的工作原理和协同机制,讨论了实际开发中可能遇到的问题和解决方案。本文的重点在于:
- 深入理解Servlet的生命周期和请求处理机制
- 掌握Ajax异步通信的实现方式
- 熟悉MySQL的事务管理和索引优化
- 理解Session在用户状态管理中的作用
在实际开发中,这种技术组合适用于中小型系统,特别是需要用户状态管理和实时更新的场景。但需要注意避免在不需要的场景中过度使用Session,同时要特别注意安全防护和性能优化。通过合理的架构设计和代码实现,可以构建出稳定、高效的图书管理系统。
评论已关闭