1. 项目背景与核心需求
这个JavaWeb实训项目是典型的Servlet控制层开发实战案例,采用经典的MVC分层架构实现图书管理系统。作为高校计算机专业常见的课程作业,它综合考察了Servlet核心API运用、JDBC数据库操作、JSP视图渲染等JavaWeb核心技术栈的掌握程度。
我在实际教学中发现,90%的学生在完成此类作业时会遇到三个典型痛点:一是Servlet与JSP的职责边界模糊导致代码混乱;二是数据库连接管理不当引发资源泄漏;三是缺乏事务处理意识造成数据不一致。本系列教程将针对这些实际问题,通过完整可运行的代码示例演示企业级开发规范。
提示:本Day3教程聚焦Servlet控制层的核心实现,需要读者已掌握Java基础、MySQL基本操作和JSP页面编写能力。配套源码使用IntelliJ IDEA 2022.3 + Tomcat 9.0 + MySQL 8.0环境开发。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与连接池配置
2.1 图书管理系统ER图设计
合理的数据库结构是系统稳定的基石。我们采用符合第三范式的设计,主要包含以下表结构:
sql复制CREATE TABLE `book` (
`id` int NOT NULL AUTO_INCREMENT,
`isbn` varchar(20) COLLATE utf8mb4_general_ci NOT NULL,
`title` varchar(100) COLLATE utf8mb4_general_ci NOT NULL,
`author` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
`publisher` varchar(100) COLLATE utf8mb4_general_ci DEFAULT NULL,
`price` decimal(10,2) DEFAULT NULL,
`stock` int DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `isbn_UNIQUE` (`isbn`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(45) COLLATE utf8mb4_general_ci NOT NULL,
`password` varchar(64) COLLATE utf8mb4_general_ci NOT NULL,
`real_name` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL,
`role` enum('admin','librarian','reader') COLLATE utf8mb4_general_ci DEFAULT 'reader',
PRIMARY KEY (`id`),
UNIQUE KEY `username_UNIQUE` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `borrow_record` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`book_id` int NOT NULL,
`borrow_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`return_time` datetime DEFAULT NULL,
`status` enum('borrowed','returned','overdue') COLLATE utf8mb4_general_ci DEFAULT 'borrowed',
PRIMARY KEY (`id`),
KEY `fk_user_idx` (`user_id`),
KEY `fk_book_idx` (`book_id`),
CONSTRAINT `fk_book` FOREIGN KEY (`book_id`) REFERENCES `book` (`id`),
CONSTRAINT `fk_user` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
2.2 Druid连接池最佳实践
直接使用DriverManager获取连接是学生作业中的常见反模式。我们采用阿里巴巴Druid连接池,在web.xml中配置:
xml复制<context-param>
<param-name>jdbcUrl</param-name>
<param-value>jdbc:mysql://localhost:3306/library?useSSL=false&serverTimezone=UTC</param-value>
</context-param>
<context-param>
<param-name>dataSource.user</param-name>
<param-value>root</param-value>
</context-param>
<context-param>
<param-name>dataSource.password</param-name>
<param-value>123456</param-value>
</context-param>
<!-- 在ServletContextListener中初始化连接池 -->
public class DruidInitializer implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
DruidDataSource ds = new DruidDataSource();
ds.setUrl(sce.getServletContext().getInitParameter("jdbcUrl"));
ds.setUsername(sce.getServletContext().getInitParameter("dataSource.user"));
ds.setPassword(sce.getServletContext().getInitParameter("dataSource.password"));
ds.setInitialSize(5);
ds.setMaxActive(20);
ds.setMaxWait(60000);
sce.getServletContext().setAttribute("dataSource", ds);
}
}
注意:必须实现ServletContextListener接口的contextDestroyed方法,在应用关闭时调用ds.close()释放连接池资源,否则会导致Tomcat热部署时出现连接泄漏。
3. Servlet控制层核心实现
3.1 请求分发设计模式
避免为每个功能创建独立Servlet的臃肿方案,采用前端控制器模式:
java复制@WebServlet("/controller/*")
public class DispatcherServlet extends HttpServlet {
private Map<String, Command> commands = new HashMap<>();
@Override
public void init() throws ServletException {
commands.put("login", new LoginCommand());
commands.put("bookList", new BookListCommand());
// 其他命令注册...
}
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String pathInfo = req.getPathInfo();
String commandKey = pathInfo.substring(1);
Command command = commands.get(commandKey);
if (command != null) {
String viewPage = command.execute(req, resp);
req.getRequestDispatcher("/WEB-INF/views/" + viewPage + ".jsp")
.forward(req, resp);
} else {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
}
public interface Command {
String execute(HttpServletRequest req, HttpServletResponse resp);
}
3.2 事务处理与DAO封装
在图书借阅场景中演示事务控制:
java复制public class BorrowCommand implements Command {
@Override
public String execute(HttpServletRequest req, HttpServletResponse resp) {
Connection conn = null;
try {
DruidDataSource ds = (DruidDataSource)req.getServletContext().getAttribute("dataSource");
conn = ds.getConnection();
conn.setAutoCommit(false);
BookDAO bookDAO = new BookDAOImpl(conn);
BorrowRecordDAO recordDAO = new BorrowRecordDAOImpl(conn);
int bookId = Integer.parseInt(req.getParameter("bookId"));
int userId = (Integer)req.getSession().getAttribute("userId");
// 检查库存
Book book = bookDAO.findById(bookId);
if (book.getStock() <= 0) {
req.setAttribute("error", "该图书已无库存");
return "error";
}
// 创建借阅记录
BorrowRecord record = new BorrowRecord();
record.setBookId(bookId);
record.setUserId(userId);
recordDAO.create(record);
// 更新库存
book.setStock(book.getStock() - 1);
bookDAO.update(book);
conn.commit();
return "borrowSuccess";
} catch (Exception e) {
if (conn != null) try { conn.rollback(); } catch (SQLException ex) {}
throw new RuntimeException("借阅操作失败", e);
} finally {
if (conn != null) try { conn.close(); } catch (SQLException e) {}
}
}
}
3.3 防止重复提交的Token机制
在JSP表单页添加隐藏字段:
jsp复制<%
String token = UUID.randomUUID().toString();
session.setAttribute("token", token);
%>
<form action="controller/borrow" method="post">
<input type="hidden" name="token" value="<%=token%>">
<!-- 其他表单字段 -->
</form>
在Servlet中进行校验:
java复制String clientToken = req.getParameter("token");
String serverToken = (String)req.getSession().getAttribute("token");
if (clientToken == null || !clientToken.equals(serverToken)) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "重复提交");
return;
}
req.getSession().removeAttribute("token"); // 使token失效
4. 常见问题排查与调试技巧
4.1 中文乱码解决方案
在Filter中统一设置编码:
java复制@WebFilter("/*")
public class EncodingFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
chain.doFilter(req, resp);
}
}
4.2 连接泄漏检测
在Druid配置中添加监控:
java复制ds.setFilters("stat,wall,log4j");
// 在监控页面查看 activeCount
4.3 异常统一处理
创建全局异常处理器:
java复制@WebServlet("/errorHandler")
public class ErrorHandlerServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
Throwable exception = (Throwable)req.getAttribute("javax.servlet.error.exception");
// 根据异常类型跳转不同错误页面
}
}
在web.xml中配置:
xml复制<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/errorHandler</location>
</error-page>
5. 项目部署与优化建议
5.1 生产环境配置调整
- 将数据库密码移至JNDI:
xml复制<Context>
<Resource name="jdbc/libraryDB"
auth="Container"
type="javax.sql.DataSource"
factory="com.alibaba.druid.pool.DruidDataSourceFactory"
maxActive="100"
... />
</Context>
- 使用PreparedStatement缓存提升性能:
java复制ds.setPoolPreparedStatements(true);
ds.setMaxPoolPreparedStatementPerConnectionSize(20);
5.2 安全加固措施
- 密码加密存储:
java复制public static String hashPassword(String password) {
return DigestUtils.sha256Hex(password + "SALT_STRING");
}
- 防止SQL注入:
java复制// 必须使用PreparedStatement
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM user WHERE username=?");
stmt.setString(1, username);
- XSS防护:
jsp复制<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
${fn:escapeXml(userInput)}
6. 扩展功能实现思路
6.1 分页查询优化
使用数据库层面分页:
java复制public List<Book> findBooks(int page, int size) {
String sql = "SELECT * FROM book LIMIT ? OFFSET ?";
// MySQL: LIMIT size OFFSET (page-1)*size
// Oracle: 需要使用ROWNUM
}
6.2 文件导入导出
使用Apache POI实现Excel导出:
java复制response.setContentType("application/vnd.ms-excel");
response.setHeader("Content-Disposition", "attachment;filename=books.xls");
HSSFWorkbook workbook = new HSSFWorkbook();
// 创建工作表并填充数据
workbook.write(response.getOutputStream());
6.3 缓存集成
添加Ehcache支持:
java复制@WebListener
public class CacheInitializer implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder()
.withCache("books",
CacheConfigurationBuilder.newCacheConfigurationBuilder(
Integer.class, Book.class,
ResourcePoolsBuilder.heap(100)))
.build();
sce.getServletContext().setAttribute("cacheManager", cacheManager);
}
}
在开发过程中,我发现很多同学容易忽视Servlet线程安全问题。特别注意:Servlet实例是单例的,不要在Servlet类中定义可变的实例变量。所有请求相关的数据应该存储在方法局部变量或请求作用域中。
