JavaWeb实现图书管理系统的增删改查与分页
在企业级JavaWeb开发中,一个典型的CRUD(增删改查)系统不仅是学习技术栈的试金石,更是理解MVC分层架构、数据库交互与权限控制的绝佳入口。本文将带你从零构建一个功能完整的图书管理系统,涵盖多条件查询、分页展示、登录验证和防重复提交等实用特性。
项目采用经典的JSP + Servlet + JDBC技术组合,结合 C3P0 连接池提升数据库性能,使用 DBUtils 简化SQL操作,并通过 Filter 实现请求拦截,完整还原了传统JavaWeb项目的开发流程。整个过程不依赖Spring等高级框架,适合初学者掌握底层原理。
一、环境准备与依赖配置
要运行该项目,需确保开发环境中已安装 JDK、Tomcat 和 MySQL。所有第三方库均以 JAR 包形式引入,放置于/WEB-INF/lib/目录下即可生效:
- c3p0-0.9.1.2.jar:轻量级数据库连接池,避免频繁创建连接带来的资源开销。
- commons-dbutils-1.6.jar:Apache 提供的 JDBC 工具类,极大简化了 ResultSet 到 JavaBean 的映射。
- jstl.jar与standard.jar:支持 JSP 页面中的
<c:if>、<c:forEach>标签,让页面逻辑更清晰。 - mysql-connector-java-5.1.38-bin.jar:MySQL 官方驱动包,用于建立数据库连接。
这些是经典JavaWeb开发的核心组件,在没有ORM框架的时代被广泛使用,至今仍具备教学和维护价值。
二、数据库设计与初始化
系统包含两张核心表:books存储图书信息,users用于用户登录认证。
CREATE DATABASE IF NOT EXISTS library CHARACTER SET utf8 COLLATE utf8_general_ci; USE library; -- 图书表 CREATE TABLE books ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '书名', zuoze VARCHAR(50) NOT NULL COMMENT '作者', price DOUBLE NOT NULL COMMENT '价格', riqi DATE NOT NULL COMMENT '出版日期' ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 用户表 CREATE TABLE users ( yhname VARCHAR(50) PRIMARY KEY COMMENT '用户名', pwd VARCHAR(50) NOT NULL COMMENT '密码' ); -- 插入默认管理员账户 INSERT INTO users VALUES ('admin', '123456');⚠️ 注意:生产环境中密码应加密存储(如MD5或BCrypt),此处为演示简化处理。
三、Java代码实现
实体类定义
Book.java
表示一本图书的基本属性,字段与数据库一一对应。
package org.wenyuan.entity; import java.util.Date; public class Book { private int id; private String name; private String zuoze; private double price; private Date riqi; // 构造方法 public Book() {} public Book(String name, String zuoze, double price, Date riqi) { this.name = name; this.zuoze = zuoze; this.price = price; this.riqi = riqi; } public Book(int id, String name, String zuoze, double price, Date riqi) { this.id = id; this.name = name; this.zuoze = zuoze; this.price = price; this.riqi = riqi; } // Getter 和 Setter 方法 public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getZuoze() { return zuoze; } public void setZuoze(String zuoze) { this.zuoze = zuoze; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public Date getRiqi() { return riqi; } public void setRiqi(Date riqi) { this.riqi = riqi; } @Override public String toString() { return "Book [id=" + id + ", name=" + name + ", zuoze=" + zuoze + ", price=" + price + ", riqi=" + riqi + "]"; } }User.java
用户实体,仅包含用户名和密码。
package org.wenyuan.entity; public class User { private String yhname; private String pwd; public User() {} public User(String yhname, String pwd) { this.yhname = yhname; this.pwd = pwd; } public String getYhname() { return yhname; } public void setYhname(String yhname) { this.yhname = yhname; } public String getPwd() { return pwd; } public void setPwd(String pwd) { this.pwd = pwd; } }BookPage.java
封装分页与搜索条件的对象,是前后端数据交换的关键载体。
package org.wenyuan.entity; import java.util.ArrayList; import java.util.List; public class BookPage { private String key; private String zuoze; private double minprice; private double maxprice; private int pageNo; private int pageSize; private int maxPagNo; private List<Book> books = new ArrayList<>(); // getter/setter... public String getKey() { return key; } public void setKey(String key) { this.key = key; } public String getZuoze() { return zuoze; } public void setZuoze(String zuoze) { this.zuoze = zuoze; } public double getMinprice() { return minprice; } public void setMinprice(double minprice) { this.minprice = minprice; } public double getMaxprice() { return maxprice; } public void setMaxprice(double maxprice) { this.maxprice = maxprice; } public int getPageNo() { return pageNo; } public void setPageNo(int pageNo) { this.pageNo = pageNo; } public int getPageSize() { return pageSize; } public void setPageSize(int pageSize) { this.pageSize = pageSize; } public int getMaxPagNo() { return maxPagNo; } public void setMaxPagNo(int maxPagNo) { this.maxPagNo = maxPagNo; } public List<Book> getBooks() { return books; } public void setBooks(List<Book> books) { this.books = books; } public BookPage() {} }数据访问层(DAO)
BookDao.java
基于 DBUtils 封装对books表的操作。其中getPage()方法实现了带条件的分页查询。
package org.wenyuan.dao; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.Map; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.BeanHandler; import org.apache.commons.dbutils.handlers.MapListHandler; import org.apache.commons.dbutils.handlers.ScalarHandler; import org.wenyuan.entity.Book; import org.wenyuan.entity.BookPage; import org.wenyuan.util.JdbcUtil; public class BookDao { QueryRunner qr = JdbcUtil.qr; public void insert(Book book) throws SQLException { String sql = "INSERT INTO books VALUES(NULL, ?, ?, ?, ?)"; qr.update(sql, book.getName(), book.getZuoze(), book.getPrice(), book.getRiqi()); } public void delete(int id) throws SQLException { String sql = "DELETE FROM books WHERE id=?"; qr.update(sql, id); } public void update(Book book) throws SQLException { String sql = "UPDATE books SET name=?, zuoze=?, price=?, riqi=? WHERE id=?"; qr.update(sql, book.getName(), book.getZuoze(), book.getPrice(), book.getRiqi(), book.getId()); } public Book get(int id) throws SQLException { String sql = "SELECT * FROM books WHERE id=?"; BeanHandler<Book> rsh = new BeanHandler<>(Book.class); return qr.query(sql, rsh, id); } public List<Book> getAll() throws SQLException { MapListHandler rsh = new MapListHandler(); String sql = "SELECT * FROM books"; List<Map<String, Object>> list = qr.query(sql, rsh); List<Book> books = new ArrayList<>(); for (Map<String, Object> map : list) { books.add(mapToBook(map)); } return books; } private Book mapToBook(Map<String, Object> map) { if (map.isEmpty()) return null; Book book = new Book(); try { Class<?> c = Book.class; for (Map.Entry<String, Object> entry : map.entrySet()) { String fieldName = entry.getKey(); Object fieldValue = entry.getValue(); java.lang.reflect.Field field = c.getDeclaredField(fieldName); field.setAccessible(true); field.set(book, fieldValue); } } catch (Exception e) { e.printStackTrace(); } return book; } public void getPage(BookPage bp) throws SQLException { String key = bp.getKey(); String zuoze = bp.getZuoze(); double minPrice = bp.getMinprice(); double maxPrice = bp.getMaxprice(); int pageNo = bp.getPageNo(); int pageSize = bp.getPageSize(); String sql1 = "SELECT * FROM books " + "WHERE (name LIKE ? OR zuoze LIKE ?) " + "AND zuoze LIKE ? AND price >= ? AND price <= ? " + "LIMIT ?, ?"; MapListHandler rsh = new MapListHandler(); List<Map<String, Object>> list = qr.query(sql1, rsh, "%" + key + "%", "%" + key + "%", "%" + zuoze + "%", minPrice, maxPrice, (pageNo - 1) * pageSize, pageSize); List<Book> books = new ArrayList<>(); for (Map<String, Object> map : list) { books.add(mapToBook(map)); } bp.setBooks(books); String sql2 = "SELECT COUNT(*) FROM books " + "WHERE (name LIKE ? OR zuoze LIKE ?) " + "AND zuoze LIKE ? AND price >= ? AND price <= ?"; ScalarHandler<Long> rsh2 = new ScalarHandler<>(); Long count = qr.query(sql2, rsh2, "%" + key + "%", "%" + key + "%", "%" + zuoze + "%", minPrice, maxPrice); int maxPageNo = (int)(count % pageSize == 0 ? count / pageSize : count / pageSize + 1); bp.setMaxPagNo(maxPageNo); } }UserDao.java
负责用户登录验证。
package org.wenyuan.dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import org.wenyuan.entity.User; import org.wenyuan.util.JdbcUtil; public class UserDao { public User login(String userName, String userPwd) throws SQLException { Connection conn = JdbcUtil.getConnection(); String sql = "SELECT yhname, pwd FROM users WHERE yhname=? AND pwd=?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, userName); ps.setString(2, userPwd); ResultSet rs = ps.executeQuery(); User u = null; if (rs.next()) { u = new User(rs.getString(1), rs.getString(2)); } rs.close(); ps.close(); conn.close(); return u; } }业务逻辑层(Service)
BookService.java
调用 DAO 层完成具体业务,异常统一捕获并打印日志。
package org.wenyuan.service; import java.sql.SQLException; import java.util.List; import org.wenyuan.dao.BookDao; import org.wenyuan.entity.Book; import org.wenyuan.entity.BookPage; public class BookService { private BookDao dao = new BookDao(); public void insertOrUpdate(Book book) { try { if (book.getId() != 0) { dao.update(book); } else { dao.insert(book); } } catch (SQLException e) { e.printStackTrace(); } } public void delete(int id) { try { dao.delete(id); } catch (SQLException e) { e.printStackTrace(); } } public Book get(int id) { try { return dao.get(id); } catch (SQLException e) { e.printStackTrace(); } return null; } public List<Book> getAll() { try { return dao.getAll(); } catch (SQLException e) { e.printStackTrace(); } return null; } public void getPage(BookPage bp) { try { dao.getPage(bp); } catch (SQLException e) { e.printStackTrace(); } } }UserService.java
封装用户登录逻辑。
package org.wenyuan.service; import java.sql.SQLException; import org.wenyuan.dao.UserDao; import org.wenyuan.entity.User; public class UserService { public User user(String username, String pwd) { try { return new UserDao().login(username, pwd); } catch (SQLException e) { e.printStackTrace(); } return null; } }控制器层(Servlet)
BookServlet.java
采用反射机制动态调用方法,减少冗余代码。
package org.wenyuan.serlvet; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.SimpleDateFormat; import java.util.Date; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.wenyuan.entity.Book; import org.wenyuan.entity.BookPage; import org.wenyuan.entity.User; import org.wenyuan.service.BookService; import org.wenyuan.util.DataUtil; @WebServlet("/BookServlet/*") public class BookServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String uri = request.getRequestURI(); String methodName = uri.substring(uri.lastIndexOf("/") + 1); try { Method method = getClass().getDeclaredMethod(methodName, HttpServletRequest.class, HttpServletResponse.class); method.invoke(this, request, response); } catch (Exception e) { e.printStackTrace(); } } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } public void submit(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(); String token = (String) session.getAttribute("code"); String vCode = request.getParameter("vCode"); if (!vCode.equalsIgnoreCase(token)) { response.sendRedirect(request.getContextPath() + "/info.jsp"); return; } if (token != null) { session.removeAttribute("code"); String id = request.getParameter("id"); String name = request.getParameter("name"); String zuoze = request.getParameter("zuoze"); String price = request.getParameter("price"); String dateStr = request.getParameter("date"); Date birth = DataUtil.testStringToDate(dateStr); Book book = new Book(name, zuoze, Double.parseDouble(price), birth); if (id != null && !id.trim().isEmpty()) { book.setId(Integer.parseInt(id)); } new BookService().insertOrUpdate(book); response.sendRedirect(request.getContextPath() + "/index.jsp"); } } public void delete(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String id = request.getParameter("id"); new BookService().delete(Integer.parseInt(id)); response.sendRedirect(request.getContextPath() + "/index.jsp"); } public void update(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String strId = request.getParameter("id"); Book book = new BookService().get(Integer.parseInt(strId)); request.setAttribute("book", book); request.getRequestDispatcher("/m.jsp").forward(request, response); } public void getAll(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String key = request.getParameter("key") == null ? "" : request.getParameter("key"); String zuoze = request.getParameter("zuoze") == null ? "" : request.getParameter("zuoze"); String minPrice = request.getParameter("minPrice"); String maxPrice = request.getParameter("maxPrice"); if (minPrice == null || minPrice.trim().isEmpty()) minPrice = "0"; if (maxPrice == null || maxPrice.trim().isEmpty()) maxPrice = Double.MAX_VALUE + ""; int pageNo = Integer.parseInt(request.getParameter("pageNo")); int pageSize = Integer.parseInt(request.getParameter("pageSize")); BookPage bp = new BookPage(key, zuoze, Double.parseDouble(minPrice), Double.parseDouble(maxPrice), pageNo, pageSize); new BookService().getPage(bp); request.setAttribute("books", bp); request.getRequestDispatcher("/all.jsp").forward(request, response); } }Deng.java(登录处理)
package org.wenyuan.serlvet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.wenyuan.entity.User; import org.wenyuan.service.UserService; @WebServlet("/Deng") public class Deng extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("yhname"); String pwd = request.getParameter("pwd"); User u = new UserService().user(username, pwd); if (u != null) { HttpSession session = request.getSession(); session.setAttribute("user", u); session.setMaxInactiveInterval(1800); Cookie cookie = new Cookie("JSESSIONID", session.getId()); cookie.setPath("/"); cookie.setMaxAge(1800); response.addCookie(cookie); response.sendRedirect(request.getContextPath() + "/index.jsp"); } else { response.sendRedirect("sb.jsp"); } } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } }ValidateCodeServlet.java(验证码生成)
防止机器人暴力提交。
package org.wenyuan.serlvet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.wenyuan.util.ValidateCode; @WebServlet("/vCode") public class ValidateCodeServlet extends HttpServlet { private static final long serialVersionUID = 1L; @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("image/jpeg"); response.setHeader("Pragma", "no-cache"); response.setHeader("Cache-Control", "no-cache"); response.setDateHeader("Expires", 0); HttpSession session = request.getSession(); ValidateCode vCode = new ValidateCode(120, 40, 5, 150); session.setAttribute("code", vCode.getCode()); vCode.write(response.getOutputStream()); } }工具类
JdbcUtil.java
C3P0 配置与全局 QueryRunner 实例。
package org.wenyuan.util; import com.mchange.v2.c3p0.ComboPooledDataSource; import org.apache.commons.dbutils.QueryRunner; import java.sql.Connection; import java.sql.SQLException; public class JdbcUtil { private static ComboPooledDataSource ds = new ComboPooledDataSource(); public static final QueryRunner qr = new QueryRunner(ds); static { try { ds.setDriverClass("com.mysql.jdbc.Driver"); ds.setJdbcUrl("jdbc:mysql://localhost:3306/library?useSSL=false&characterEncoding=utf8"); ds.setUser("root"); ds.setPassword("your_password"); // 修改为你的密码 } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return ds.getConnection(); } }DataUtil.java
字符串转日期工具。
package org.wenyuan.util; import java.text.SimpleDateFormat; import java.util.Date; public class DataUtil { public static Date testStringToDate(String src) { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd"); try { return format.parse(src); } catch (Exception e) { e.printStackTrace(); return null; } } }四、前端页面实现(JSP)
index.jsp
首页自动跳转至图书列表。
<jsp:forward page="BookServlet/getAll?key=&zuoze=&minPrice=&maxPrice=&pageNo=1&pageSize=5"/>denglu.jsp(登录页)
<form action="Deng"> <table> <tr><td>用户名:</td><td><input type="text" name="yhname"></td></tr> <tr><td>密 码:</td><td><input type="password" name="pwd"></td></tr> <tr><td><input type="submit" value="登录"></td></tr> </table> </form>all.jsp(主列表页)
集成搜索框、分页控件与数据表格。
<c:if test="${user.yhname==null}"> <a href="denglu.jsp">登录</a> </c:if> <c:otherwise> <span>欢迎你!${user.yhname}</span> <a href="m.jsp">添加</a> </c:otherwise> <input type="text" id="key" value="${books.key}"> <input type="text" id="zuoze" value="${books.zuoze}"> <input type="text" id="minPrice" value="${books.minprice eq 0 ? '' : books.minprice}"> <input type="text" id="maxPrice" value="${books.maxprice eq 9.17578e307 ? '' : books.maxprice}"> <a href="#" id="search">查询</a> <a href="#" id="firstPage">首页</a> <a href="#" id="prePage">上一页</a> <a href="#" id="nextPage">下一页</a> <a href="#" id="lastPage">末页</a> 共 ${books.pageNo}/${books.maxPagNo} 页 <table border="1"> <tr> <th>ID</th><th>书名</th><th>作者</th><th>价格</th><th>日期</th><th>操作</th> </tr> <c:forEach items="${books.books}" var="b"> <tr> <td>${b.id}</td> <td>${b.name}</td> <td>${b.zuoze}</td> <td>${b.price}</td> <td>${b.riqi}</td> <td> <a href="BookServlet/delete?id=${b.id}">删除</a> <a href="BookServlet/update?id=${b.id}">修改</a> <a href="Check?id=${b.id}">查看</a> </td> </tr> </c:forEach> </table>m.jsp(新增/修改页)
根据是否存在book对象判断当前是新增还是编辑状态。
<h1>${empty book ? '新增' : '修改'} 图书</h1> <form action="BookServlet/submit"> <c:if test="${not empty book}"> <input type="hidden" name="id" value="${book.id}"> </c:if> 书名:<input type="text" name="name" value="${book.name}"><br> 作者:<input type="text" name="zuoze" value="${book.zuoze}"><br> 价格:<input type="text" name="price" value="${book.price}"><br> 日期:<input type="date" name="date" value="${book.riqi}"><br> 验证码:<input type="text" name="vCode"><img src="vCode" onclick="this.src='vCode?'+new Date()" alt="验证码"><br> <input type="submit" value="提交"> </form>五、安全控制 —— 权限过滤器
通过Filter拦截所有对图书操作的请求,未登录用户将被重定向到登录页。
@WebFilter("/BookServlet/*") public class AllFilter implements javax.servlet.Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; User u = (User) request.getSession().getAttribute("user"); if (u == null) { ((HttpServletResponse) resp).sendRedirect(request.getContextPath() + "/denglu.jsp"); return; } chain.doFilter(req, resp); } }只需在类上添加注解即可启用,无需在 web.xml 中额外配置(Servlet 3.0+ 支持)。
这套图书管理系统虽然基于传统技术栈,但其分层清晰、结构规范,非常适合作为学习案例。它展示了如何在没有现代框架的情况下,手动组织代码实现模块化与可维护性。对于理解 Web 请求生命周期、会话管理、数据库连接优化等核心概念具有重要价值。
随着技术演进,我们可以在此基础上逐步引入 Spring MVC 替代原生 Servlet,用 MyBatis 或 JPA 替换 DBUtils,前端也可升级为 Ajax 异步加载,甚至整合 Vue/React 构建前后端分离应用。这种渐进式重构思维,正是工程师成长的真实路径。