JavaWeb实战:图书管理系统全链路开发与架构设计详解
1. 项目概述:从“玩具”到“作品”的实战跨越
如果你已经学完了Java基础、Servlet、JSP,甚至接触过一些简单的框架,但打开IDE面对一个空项目时,依然感到无从下手,那么你正处在一个关键的“瓶颈期”。这个阶段,你需要的不是更多的语法知识,而是一个能串联所有知识点、模拟真实开发流程的 完整实战项目 。今天要拆解的这个“JavaWeb实战项目案例六”,正是为突破这个瓶颈而设计的。它不是一个简单的“增删改查”Demo,而是一个麻雀虽小、五脏俱全的综合性练手项目,旨在让你亲身体验从需求分析、数据库设计、前后端开发到部署上线的全链路过程。
为什么说这类项目至关重要?因为面试官和实际工作场景考察的,从来不是你背下了多少API,而是你 解决一个具体业务问题的综合能力 。一个完整的项目案例,能将你零散的知识点(如JDBC连接池、MVC分层、会话管理、前端交互)编织成一张紧密的网。通过亲手实现它,你不仅能巩固技术栈,更能建立起宝贵的“工程化思维”——如何设计可维护的代码结构、如何处理异常、如何进行基础的性能考量。接下来,我将以一个典型的 图书管理系统 作为蓝本,带你深度拆解这个实战项目的每一个核心环节,分享那些只有真正动手做过才会懂的“坑”与技巧。
2. 项目整体架构与技术选型解析
在动手写第一行代码之前,花时间思考架构和技术选型,是区分“新手”和“有经验的开发者”的关键一步。一个好的架构能让后续开发事半功倍,而随意的堆砌代码则会带来无尽的维护噩梦。
2.1 为什么选择经典三层架构?
对于这个练手项目,我强烈推荐采用经典的 MVC模式 与 三层架构 结合的方式。这不是过时,而是经过无数项目验证的、最适合初学者理解业务逻辑分离的范式。
- 表现层(View/Controller) :使用JSP(或Thymeleaf等模板引擎)负责页面渲染,Servlet作为控制器(Controller)接收请求、调用业务、转发视图。这让你清晰地看到“请求-响应”的完整生命周期。
- 业务逻辑层(Service) :这是项目的“大脑”。所有具体的业务规则(如“借书时检查用户是否超期”、“图书库存不足时不能借阅”)都在这里实现。Service层调用数据访问层,并对上层提供干净的接口。
- 数据访问层(DAO) :负责所有与数据库打交道的操作,即CRUD(增删改查)。它的存在将业务逻辑与特定的数据库技术(如MySQL)解耦,未来更换数据库会容易得多。
注意 :很多新手会图省事,把数据库操作直接写在Servlet里。这会导致Servlet迅速膨胀到几千行,成为难以维护的“上帝类”。严格的分层是项目可扩展性的基石。
2.2 技术栈的“务实”选择
基于“学习”和“实战”的双重目标,我建议以下技术栈组合:
- 后端核心 :Servlet 3.0+、JSP、JDBC。为什么不直接用Spring Boot?因为“地基”要打牢。亲手用原生的Servlet和JDBC处理一遍,你才能真正理解Spring Boot帮你简化了什么,未来遇到问题才能深入排查。
- 数据库 :MySQL 5.7+。社区活跃,资料丰富,是工业界的标配。对于这个项目,完全够用。
- 连接池 : 必选项 ,而非可选项。使用像HikariCP或Druid这样的高性能连接池。直接使用
DriverManager.getConnection()在生产环境下是灾难性的,会导致数据库连接耗尽。HikariCP以轻量和快速著称,是很多项目的首选。 - 前端 :HTML5、CSS3、JavaScript,配合简单的jQuery或原生JS完成Ajax交互。不必追求复杂的前端框架,重点是理解前后端数据交互(JSON格式)和DOM操作。
- 构建与管理 :Maven。用它来管理项目依赖(如数据库驱动、连接池、JSON解析库),保证环境一致性。
- 服务器 :Tomcat 9.x。轻量、稳定,是学习JavaWeb的“老伙伴”。
这个组合没有追赶最新潮的技术,但每一环都是企业级应用的基石,掌握了它们,你再学习任何上层框架都会感到游刃有余。
3. 数据库设计与核心表结构剖析
数据库设计是项目的“骨架”,设计得好,后续开发顺风顺水;设计得差,则举步维艰。我们围绕“图书管理”核心业务,设计以下几张表:
3.1 表结构设计思路
-
用户表 (
sys_user)CREATE TABLE `sys_user` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` varchar(50) NOT NULL UNIQUE COMMENT '用户名', `password` varchar(255) NOT NULL COMMENT '密码(需加密存储)', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `role` tinyint(4) NOT NULL DEFAULT '1' COMMENT '角色:0-管理员,1-普通用户', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:0-禁用,1-正常', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';- 设计要点 :
password字段必须预留足够长度(推荐255),为使用BCrypt等强哈希算法加密做准备。role字段用于实现简单的权限控制(管理员和用户菜单不同)。status实现软删除或账户禁用功能。
- 设计要点 :
-
图书信息表 (
book)CREATE TABLE `book` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '图书ID', `isbn` varchar(20) NOT NULL UNIQUE COMMENT 'ISBN号', `name` varchar(200) NOT NULL COMMENT '图书名称', `author` varchar(100) NOT NULL COMMENT '作者', `publisher` varchar(100) DEFAULT NULL COMMENT '出版社', `price` decimal(10,2) DEFAULT NULL COMMENT '价格', `total_count` int(11) NOT NULL DEFAULT '0' COMMENT '总库存数量', `available_count` int(11) NOT NULL DEFAULT '0' COMMENT '可借阅数量', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '入库时间', PRIMARY KEY (`id`), KEY `idx_name` (`name`), KEY `idx_author` (`author`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书信息表';- 设计要点 :将库存拆分为
total_count和available_count是关键。总数量不变,可借数量随借阅、归还动态变化。这样设计便于统计和避免超借。为name和author建立索引,能大幅提升模糊查询效率。
- 设计要点 :将库存拆分为
-
借阅记录表 (
borrow_record)CREATE TABLE `borrow_record` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '记录ID', `user_id` int(11) NOT NULL COMMENT '借阅用户ID', `book_id` int(11) NOT NULL COMMENT '图书ID', `borrow_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '借出时间', `due_time` datetime NOT NULL COMMENT '应还时间', `return_time` datetime DEFAULT NULL COMMENT '实际归还时间', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0-借出中,1-已归还,2-超期未还', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_book_id` (`book_id`), KEY `idx_due_time` (`due_time`), CONSTRAINT `fk_record_user` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`), CONSTRAINT `fk_record_book` FOREIGN KEY (`book_id`) REFERENCES `book` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书借阅记录表';- 设计要点 :这是核心的业务表。
due_time(应还时间)由业务逻辑计算后存入(如借出时间+30天)。status字段用于快速查询不同状态的记录,而无需每次计算时间。 外键约束 在学习阶段建议加上,它能保证数据的一致性,让你理解关系数据库的关联性。生产环境可能会因性能考虑而不用,但原理必须懂。
- 设计要点 :这是核心的业务表。
3.2 为什么不用外键?—— 一个常见的面试题
在上面的设计中我添加了外键,但在很多互联网公司,开发规范明确 禁止使用数据库外键 。为什么?主要出于性能和高并发的考虑:外键约束会带来额外的锁开销,在分布式、分库分表场景下难以维护。那么如何保证数据一致性?答案是通过 应用层逻辑 来保证,比如在Service层删除用户前,先检查是否有未归还的图书。在项目中了解这两种方式的取舍,本身就是一次很好的学习。
4. 核心功能模块的详细实现与编码实战
有了清晰的设计,我们就可以开始编码了。我将挑几个最具代表性的功能模块,深入讲解实现细节和容易踩的坑。
4.1 用户登录与密码安全
这是系统的门户,安全性至关重要。
1. 密码加密存储 永远不要在数据库中存储明文密码!使用BCrypt算法是当前的最佳实践。它自带盐值(salt),能有效抵御彩虹表攻击。
// 引入BCrypt依赖(如 jbcrypt)
public class PasswordUtil {
private static final int STRENGTH = 12; // 强度因子,值越大越安全但也越慢
public static String hashPassword(String plainPassword) {
return BCrypt.hashpw(plainPassword, BCrypt.gensalt(STRENGTH));
}
public static boolean checkPassword(String plainPassword, String hashedPassword) {
return BCrypt.checkpw(plainPassword, hashedPassword);
}
}
在用户注册或修改密码时,调用 hashPassword 存储哈希值。登录时,调用 checkPassword 进行验证。
2. 会话管理 用户登录成功后,需要将其身份信息存入Session,以便后续请求识别。
// LoginServlet.java (doPost方法部分)
String username = request.getParameter("username");
String password = request.getParameter("password");
User user = userService.login(username, password);
if (user != null) {
// 登录成功
HttpSession session = request.getSession();
session.setAttribute("loginUser", user); // 存储整个用户对象或关键信息
session.setAttribute("userId", user.getId());
session.setAttribute("userRole", user.getRole());
// 设置会话超时时间(单位:秒),例如30分钟
session.setMaxInactiveInterval(30 * 60);
response.sendRedirect("index.jsp"); // 跳转到主页
} else {
request.setAttribute("errorMsg", "用户名或密码错误");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
实操心得 :不要在Session中存放过大对象(如包含大量数据的List),这会增加服务器内存压力。通常只存放用户ID、姓名、角色等关键标识信息。其他信息需要时再从数据库查询。
4.2 图书借阅业务逻辑实现
这是业务核心,涉及事务和并发控制。
Service层核心代码逻辑:
// BookBorrowService.java
public class BookBorrowService {
private BookDao bookDao = new BookDaoImpl();
private BorrowRecordDao recordDao = new BorrowRecordDaoImpl();
// 务必使用同一个Connection保证事务
private Connection connection = DatabaseUtil.getConnection();
public boolean borrowBook(int userId, int bookId) throws SQLException {
boolean success = false;
try {
// 1. 开启事务(手动提交)
connection.setAutoCommit(false);
// 2. 查询图书当前可借数量(悲观锁:for update)
String sql = "SELECT available_count FROM book WHERE id = ? FOR UPDATE";
// ... 执行查询,获取当前数量 currentAvailable
if (currentAvailable <= 0) {
throw new RuntimeException("图书库存不足,无法借阅");
}
// 3. 减少可借数量
String updateSql = "UPDATE book SET available_count = available_count - 1 WHERE id = ?";
// ... 执行更新
// 4. 创建借阅记录
BorrowRecord record = new BorrowRecord();
record.setUserId(userId);
record.setBookId(bookId);
record.setBorrowTime(new Date());
// 计算应还时间:当前时间+30天
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.DAY_OF_MONTH, 30);
record.setDueTime(calendar.getTime());
record.setStatus(0);
recordDao.insert(record, connection);
// 5. 提交事务
connection.commit();
success = true;
} catch (Exception e) {
// 6. 发生异常,回滚事务
connection.rollback();
e.printStackTrace();
throw new RuntimeException("借阅操作失败:" + e.getMessage());
} finally {
// 7. 恢复自动提交模式,并谨慎关闭连接(通常连接由连接池管理,这里不直接关闭)
connection.setAutoCommit(true);
}
return success;
}
}
关键点解析:
- 事务(Transaction) :借书涉及“更新库存”和“插入记录”两个操作,必须作为一个原子单元。使用
connection.setAutoCommit(false)开启手动事务,全部成功则提交(commit),任一失败则回滚(rollback)。 - 并发控制 :在高并发场景下,多个用户同时借同一本最后一本书,可能导致“超借”。这里使用了
SELECT ... FOR UPDATE进行 悲观锁 ,在查询时就直接锁定这条记录,直到事务结束。这是最安全的做法,但会影响性能。另一种思路是使用 乐观锁 (通过版本号字段),冲突时让用户重试,适合并发冲突不高的场景。 - 连接管理 :Service层多个DAO操作必须使用同一个数据库连接,否则事务会失效。通常通过 ThreadLocal 或从连接池获取连接后传递来解决。
4.3 分页查询的通用解决方案
列表查询(如图书列表、借阅记录)必须支持分页,这是基础要求。
后端分页逻辑:
// BaseDao.java 或一个独立的 PageUtil.java
public class PageUtil<T> {
private int pageNum; // 当前页码
private int pageSize; // 每页条数
private int totalCount; // 总记录数
private int totalPage; // 总页数
private List<T> data; // 当前页数据
// 构造方法、getter/setter 省略...
/**
* 生成分页查询的SQL片段(MySQL)
* @param sql 原始查询SQL(不含limit)
* @param pageNum
* @param pageSize
* @return 拼接了limit的SQL
*/
public static String getPagedSql(String sql, int pageNum, int pageSize) {
int offset = (pageNum - 1) * pageSize;
return sql + " LIMIT " + offset + ", " + pageSize;
}
/**
* 计算总页数
*/
public void calculateTotalPage() {
this.totalPage = (int) Math.ceil((double) totalCount / pageSize);
}
}
在Servlet中使用:
// BookListServlet.java
int pageNum = 1;
int pageSize = 10;
try {
pageNum = Integer.parseInt(request.getParameter("pageNum"));
} catch (NumberFormatException e) {
// 使用默认值
}
String keyword = request.getParameter("keyword");
// 创建分页对象
PageUtil<Book> page = new PageUtil<>();
page.setPageNum(pageNum);
page.setPageSize(pageSize);
// 查询总记录数(这是一个独立的COUNT查询,性能关键点)
int totalCount = bookDao.countBooks(keyword);
page.setTotalCount(totalCount);
page.calculateTotalPage();
// 查询当前页数据
List<Book> bookList = bookDao.findBooksByPage(keyword, pageNum, pageSize);
page.setData(bookList);
// 将page对象放入request,转发到JSP
request.setAttribute("page", page);
request.getRequestDispatcher("/admin/book_list.jsp").forward(request, response);
前端JSP分页栏实现(核心片段):
<!-- book_list.jsp -->
<div class="pagination">
<c:if test="${page.pageNum > 1}">
<a href="bookList?pageNum=${page.pageNum - 1}&keyword=${param.keyword}">上一页</a>
</c:if>
<c:forEach begin="1" end="${page.totalPage}" var="i">
<c:choose>
<c:when test="${i == page.pageNum}">
<span class="current">${i}</span>
</c:when>
<c:otherwise>
<a href="bookList?pageNum=${i}&keyword=${param.keyword}">${i}</a>
</c:otherwise>
</c:choose>
</c:forEach>
<c:if test="${page.pageNum < page.totalPage}">
<a href="bookList?pageNum=${page.pageNum + 1}&keyword=${param.keyword}">下一页</a>
</c:if>
共 ${page.totalCount} 条记录, 第 ${page.pageNum}/${page.totalPage} 页
</div>
注意事项 :
COUNT(*)查询在数据量大时可能很慢。如果表数据量极大(百万级以上),需要考虑其他方案,如使用估算值、或从搜索引擎/缓存中获取总数。
5. 开发环境搭建与项目配置要点
“工欲善其事,必先利其器”。一个顺畅的开发环境能极大提升效率。
5.1 使用Maven创建Web项目骨架
在IntelliJ IDEA中:
File -> New -> Project,选择Maven,勾选Create from archetype,找到org.apache.maven.archetypes:maven-archetype-webapp。- 填写
GroupId(如com.yourname)、ArtifactId(如library-management)。 - 创建完成后,补全标准的Maven目录结构:
src/main/java —— 存放Java源代码 src/main/resources —— 存放配置文件(.properties, .xml) src/main/webapp —— 存放Web资源(JSP, HTML, CSS, JS, WEB-INF) src/test/java —— 存放测试代码 - 在
pom.xml中添加关键依赖:<dependencies> <!-- Servlet & JSP API --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> <!-- JSTL 标签库,用于简化JSP中的逻辑 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- HikariCP 连接池 --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.0.1</version> </dependency> <!-- BCrypt 密码加密 --> <dependency> <groupId>org.mindrot</groupId> <artifactId>jbcrypt</artifactId> <version>0.4</version> </dependency> <!-- JSON处理(用于可能的Ajax接口) --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.2</version> </dependency> </dependencies>
5.2 数据库连接池的配置与封装
在 src/main/resources 下创建 db.properties :
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/library_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
jdbc.username=root
jdbc.password=your_password
# HikariCP 配置
hikari.maximumPoolSize=10
hikari.minimumIdle=5
hikari.idleTimeout=300000
hikari.connectionTimeout=20000
hikari.maxLifetime=1800000
创建数据库连接工具类 DatabaseUtil.java :
public class DatabaseUtil {
private static HikariDataSource dataSource;
static {
try {
Properties props = new Properties();
// 从类路径加载配置文件
InputStream input = DatabaseUtil.class.getClassLoader()
.getResourceAsStream("db.properties");
props.load(input);
HikariConfig config = new HikariConfig();
config.setDriverClassName(props.getProperty("jdbc.driver"));
config.setJdbcUrl(props.getProperty("jdbc.url"));
config.setUsername(props.getProperty("jdbc.username"));
config.setPassword(props.getProperty("jdbc.password"));
// 可选:设置连接池参数
config.setMaximumPoolSize(Integer.parseInt(props.getProperty("hikari.maximumPoolSize", "10")));
config.setMinimumIdle(Integer.parseInt(props.getProperty("hikari.minimumIdle", "5")));
dataSource = new HikariDataSource(config);
} catch (IOException e) {
throw new ExceptionInInitializerError("Failed to load database configuration.");
}
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
public static void closeConnection(Connection conn, Statement stmt, ResultSet rs) {
// 关闭资源的通用方法,注意关闭顺序:ResultSet -> Statement -> Connection
try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); }
try { if (stmt != null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); }
try { if (conn != null) conn.close(); } // 这里是将连接还给连接池,并非真正关闭
catch (SQLException e) { e.printStackTrace(); }
}
}
踩坑提醒 :
db.properties文件中的serverTimezone参数非常重要,如果没设置或设置错误,可能会导致数据库插入时间时差8小时的问题。useSSL=false在本地开发环境可以关闭以简化配置。
6. 前端页面交互与用户体验优化
虽然重点是后端,但一个看得过去、交互友好的前端能让项目增色不少,也更接近真实项目。
6.1 使用JSTL和EL表达式简化JSP
避免在JSP中写大量的Java脚本( <% ... %> ),使用EL表达式( ${} )和JSTL标签。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html>
<html>
<head>
<title>图书列表</title>
</head>
<body>
<h1>欢迎您,${sessionScope.loginUser.realName}</h1>
<!-- 使用JSTL的c:if进行条件判断 -->
<c:if test="${sessionScope.userRole == 0}">
<a href="bookAdd.jsp">添加新书</a>
</c:if>
<table border="1">
<tr>
<th>ID</th><th>书名</th><th>作者</th><th>价格</th><th>可借数量</th><th>操作</th>
</tr>
<!-- 使用JSTL的c:forEach遍历列表 -->
<c:forEach items="${page.data}" var="book">
<tr>
<td>${book.id}</td>
<td>${book.name}</td>
<td>${book.author}</td>
<td>
<!-- 使用JSTL的fmt:formatNumber格式化价格 -->
<fmt:formatNumber value="${book.price}" type="currency" pattern="¥#,##0.00"/>
</td>
<td>${book.availableCount}</td>
<td>
<a href="bookDetail?id=${book.id}">详情</a>
<c:if test="${book.availableCount > 0}">
<a href="javascript:borrowBook(${book.id})">借阅</a>
</c:if>
</td>
</tr>
</c:forEach>
</table>
<!-- 引入分页片段 -->
<jsp:include page="/common/pagination.jsp">
<jsp:param name="pageUrl" value="bookList"/>
</jsp:include>
<script>
function borrowBook(bookId) {
if(confirm('确定要借阅这本书吗?')) {
// 使用Fetch API或jQuery Ajax发起异步请求
fetch('borrowBook?bookId=' + bookId, {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'}
})
.then(response => response.json())
.then(data => {
if(data.success) {
alert('借阅成功!');
location.reload(); // 刷新页面更新库存
} else {
alert('借阅失败:' + data.message);
}
})
.catch(error => console.error('Error:', error));
}
}
</script>
</body>
</html>
6.2 利用Ajax实现无刷新交互
如上例中的 borrowBook 函数,使用 fetch 或 jQuery.ajax 与后端交互,可以极大提升用户体验。后端Servlet需要返回JSON格式的数据。
// BorrowBookServlet.java
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.setContentType("application/json;charset=utf-8");
PrintWriter out = response.getWriter();
Map<String, Object> result = new HashMap<>();
try {
int userId = (int) request.getSession().getAttribute("userId");
int bookId = Integer.parseInt(request.getParameter("bookId"));
boolean success = borrowService.borrowBook(userId, bookId);
if(success) {
result.put("success", true);
result.put("message", "借阅成功");
} else {
result.put("success", false);
result.put("message", "借阅失败,请重试");
}
} catch (Exception e) {
result.put("success", false);
result.put("message", "系统错误:" + e.getMessage());
}
// 使用Jackson将Map转换为JSON字符串
ObjectMapper mapper = new ObjectMapper();
out.print(mapper.writeValueAsString(result));
out.flush();
}
7. 项目部署、测试与性能调优入门
开发完成只是第一步,让项目稳定运行并具备一定抗压能力,才是实战的完结。
7.1 将项目打包并部署到Tomcat
-
打包 :在项目根目录(包含
pom.xml的目录)下执行Maven命令:mvn clean package成功后,会在
target目录下生成一个项目名.war文件(如library-management.war)。 -
部署 :
- 方式一(热部署) :将
.war文件复制到Tomcat的webapps目录下,启动Tomcat,它会自动解压并部署。 - 方式二(Manager App) :访问Tomcat管理页面(
http://localhost:8080/manager/html),上传WAR文件进行部署。 - 方式三(IDEA集成) :在IDEA中配置Tomcat Server,直接使用
Debug或Run模式启动,适合开发阶段。
- 方式一(热部署) :将
-
访问 :部署成功后,通过
http://localhost:8080/你的项目名/访问应用。如果WAR文件名为library-management.war,则访问路径为http://localhost:8080/library-management/。
7.2 基础功能测试点
不要只测试“正确路径”,更要测试“异常路径”。
- 边界测试 :输入超长用户名、密码;借阅库存为0的图书;查询不存在的图书。
- 并发测试 :模拟两个用户同时借阅最后一本相同的书,观察是否会发生超借。
- 安全性测试 :
- SQL注入 :在登录名输入
' or '1'='1,看是否能绕过登录。 - XSS攻击 :在图书名输入
<script>alert('xss')</script>,看页面是否会被执行脚本。 - 越权访问 :普通用户登录后,尝试直接访问管理员页面的URL(如
/admin/userList),检查是否有权限控制。
- SQL注入 :在登录名输入
- 会话测试 :登录后关闭浏览器再打开,访问需要登录的页面,是否跳转到登录页。
7.3 初级的性能考量与优化方向
当项目能跑起来后,可以思考以下优化点,这会让你的项目在面试中脱颖而出:
- 数据库查询优化 :
- 索引 :为
borrow_record表的user_id,book_id,due_time字段添加索引,能极大提升关联查询和按时间范围查询的速度。 - **避免SELECT ***:只查询需要的字段,减少网络传输和内存消耗。
- 批量操作 :如需插入大量初始数据,使用
PreparedStatement的addBatch()和executeBatch()。
- 索引 :为
- 缓存引入 :对于一些不常变化但频繁访问的数据,如图书分类、系统配置,可以引入简单的缓存。从最简单的
HashMap(注意并发问题)开始,到使用Caffeine或Ehcache本地缓存,理解缓存穿透、击穿、雪崩的概念及应对策略。 - 静态资源处理 :将CSS、JS、图片等静态文件放在
webapp下的特定目录(如/static),并在web.xml中配置Tomcat的DefaultServlet来处理,避免经过你的应用Servlet,提升响应速度。 - 连接池监控 :如果使用Druid连接池,它自带监控界面。可以查看活跃连接数、执行慢SQL等信息,帮助定位性能瓶颈。
8. 常见问题排查与调试技巧实录
开发过程中,你一定会遇到各种“诡异”的问题。这里记录几个最常见的问题和排查思路。
8.1 中文乱码问题——永远的“坑”
现象 :页面显示问号 ?? ,或数据库里存的是乱码。 解决方案(四步检查法) :
- JSP页面编码 :确保JSP文件顶部有
<%@ page contentType="text/html;charset=UTF-8" language="java" %>,并且文件本身的物理编码也是UTF-8(在IDE中设置)。 - Servlet请求/响应编码 :在
doGet/doPost方法最开始,设置请求和响应的编码。
对于request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8");application/json响应,则用response.setContentType("application/json;charset=utf-8");。 - 数据库连接编码 :JDBC URL中必须指定字符集,如
jdbc:mysql://...?characterEncoding=UTF-8&useUnicode=true。 - 数据库表/字段编码 :确保MySQL数据库、表和字段的字符集是
utf8mb4(支持所有emoji和生僻字)。
8.2 空指针异常(NullPointerException)
这是Java中最常见的运行时异常。
- 场景 :从
request.getParameter()获取参数,未判空就直接使用;从Session中获取对象,未判空就调用其方法。 - 防御性编程 :养成习惯,对可能为null的对象先进行判断。
String idStr = request.getParameter("id"); if (idStr == null || idStr.trim().isEmpty()) { // 处理参数缺失的情况,如返回错误信息或默认值 response.sendError(HttpServletResponse.SC_BAD_REQUEST, "参数id不能为空"); return; } int id = Integer.parseInt(idStr); // 此时idStr肯定不为空
8.3 事务不生效
现象 :代码中抛出了异常,但数据库数据还是被修改了。 排查 :
- 检查是否使用了
connection.setAutoCommit(false)开启了手动事务。 - 检查Service方法中多个DAO操作使用的是否是 同一个Connection 对象。如果每个DAO都自己从连接池获取新连接,事务必然失效。解决方法是通过参数传递Connection,或使用
ThreadLocal绑定连接。 - 检查是否在
catch块中进行了connection.rollback(),并在finally块中恢复了connection.setAutoCommit(true)。 - 确保你操作的数据库表是 支持事务的存储引擎 (如InnoDB),而不是MyISAM。
8.4 404错误(资源找不到)
现象 :浏览器显示404,但确定文件存在。 排查 :
- 路径错误 :这是最常见的原因。检查JSP页面中的链接、表单的
action、Servlet的@WebServlet注解或web.xml中的<url-pattern>是否匹配。- 相对路径和绝对路径(以
/开头)的区别。/代表应用根路径(http://host:port/你的应用名/)。
- 相对路径和绝对路径(以
- 未编译或部署失败 :检查
target目录下或Tomcat的webapps目录下,你的类文件(.class)是否成功生成。清理项目并重新构建(mvn clean compile)。 - Servlet未配置或注解未扫描 :如果使用
@WebServlet注解,确保你的Servlet类在src/main/java下,且Tomcat能扫描到。或者检查web.xml中<servlet>和<servlet-mapping>的配置是否正确。
8.5 500错误(服务器内部错误)
现象 :浏览器显示500,Tomcat控制台打印了异常堆栈信息。 这是最好的情况 ,因为错误信息一目了然。
- 看控制台 :直接阅读Tomcat日志(通常在IDEA的
Run或Console窗口,或Tomcat的logs/catalina.out文件),找到Caused by:后面的根本原因。 - 常见原因 :
ClassNotFoundException:缺少某个JAR包依赖,检查pom.xml和WEB-INF/lib。SQLException:SQL语法错误、连接失败。检查SQL语句和在数据库客户端能否执行。NumberFormatException:将非数字字符串转为数字,如前文的id参数判空。JasperException:JSP编译错误,检查JSP页面语法。
掌握这些排查方法,你就能独立解决开发中80%以上的问题。记住, 耐心阅读错误信息 是程序员最重要的能力之一。
更多推荐



所有评论(0)