资讯动态

JSP+Bean+Servlet实战:手把手搭建网上购书系统

发布时间:2026/9/15 3:52:34 来源:尧图企业网站定制
简介面向Java Web课程设计与初学者的轻量级实战项目基于JspBeanServlet经典三层架构实现一个简单的网上购书系统功能覆盖用户注册、登录、图书信息查询与浏览适合用较短周期理解B/S结构下请求处理与业务分层。资源包共55个文件包含15个java源码与15个class编译文件、6个jsp页面另有sql数据库脚本、xml/properties配置以及PDF版课程综合训练文档压缩包仅1.21MB部署与阅读门槛低。项目按标准Web工程组织src存放JavaBean和ServletWebRoot下为JSP视图并附带项目配置文件与数据库脚本便于直接导入IDE后对照学习。资料已有170人学习适合需要快速完成课程设计或希望掌握ServletJSP协作机制的开发者。通过源码可梳理登录校验、流程跳转、数据查询等典型写法配套PDF和README还可提供整体设计思路与运行说明。1. 为什么课程设计都爱用 JspBeanServlet课程设计周被分到“网上购书系统”这种题目的同学大概率会在 Spring Boot 和纯 JSP 之间纠结。Spring Boot 初始化一个项目就要下载一堆依赖答辩时一句“为什么多了一层”就能把你问住纯 Servlet 写页面又回到了 PrintWriter 拼接 HTML 的原始社会。JspBeanServlet 这个三位一体组合恰好卡在中间JSP 负责把书单、个人信息渲染成页面Servlet 负责接收注册、登录、查询请求并决定跳转JavaBean 负责把用户、图书数据封装成内存对象。这套系统实现了用户注册、登录、信息查询、信息浏览四个功能不依赖任何重量级框架既能让课程设计的 B/S 结构成立又保留了往后迁移到 SSM 或 Spring Boot 时的分层直觉。适合刚学完 Java Web 的初学者也适合想快速理清动态请求链路的人。2. 模型层与数据链路Bean 设计、JDBC 连接与三层结构划分2.1 项目结构里 Bean 和 Servlet 怎么分目录拿到源码包解压后看到的目录结构是典型的 Java Web 工程。WebRoot 是 Web 根目录里面放着 JSP 和 WEB-INF/web.xmlsrc 下按类的职责拆包。常见做法是分为 bean、dao、servlet、util 四个包实体类和数据访问分开控制器独立。下面是一个课程设计里常用的骨架src ├── com/example/bean # 实体类User、Book ├── com/example/dao # 数据访问层操作用户表和图书表 ├── com/example/servlet # 控制器注册、登录、图书列表 ├── com/example/util # 工具类DBUtil、EncodingFilter WebRoot ├── login.jsp ├── register.jsp ├── bookList.jsp ├── userInfo.jsp └── WEB-INF └── web.xml这样分的目的不是凑目录而是让每条请求的走向一眼能看出来。浏览器把表单提交给 ServletServlet 调 DAO 里的方法DAO 通过 DBUtil 拿到 Connection再把结果封装成 Bean最后 JSP 用 EL 表达式把 Bean 的属性读出来。任何一个环节出了问题只需要打开对应包里的类不需要在几百行的页面里翻 Java 代码。DAO 层在不少课程设计里会被直接省掉把 JDBC 代码写在 Servlet 里这样也能跑。但后续如果要从 MySQL 换成 SQL Server或者加一个 Redis 缓存所有 Servlet 都要跟着改。我一般会保留这一层哪怕只是做一个多余的封装。2.2 数据库表user 和 book 的字段设计网上购书系统的核心数据就是用户和图书最少需要两张表。以 MySQL 5.7 为例建表 SQL 如下CREATE DATABASE bookshop DEFAULT CHARSET utf8mb4; USE bookshop; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, email VARCHAR(100), phone VARCHAR(20), regdate DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, bookname VARCHAR(100) NOT NULL, author VARCHAR(50), price DECIMAL(10,2), publisher VARCHAR(100), stock INT DEFAULT 0, description TEXT ) ENGINEInnoDB;字段名称不要直接用name、password虽然没问题但desc、group这类是 MySQL 保留字容易在查询时报语法错误。这里把书名定为bookname而不是name是为了避免后续写select name from book时和函数名混淆。password字段设成 VARCHAR(64) 是预留散列宽度课程设计阶段通常直接存明文但至少留下改造空间。regdate用 DATETIME 存储注册时间DEFAULT CURRENT_TIMESTAMP 可以在插入时省掉一个参数。表与 Bean 的对应关系如下表名Bean主键常用查询字段userUseridusername, emailbookBookidbookname, author, price2.3 User Bean为什么必须有 setter/getterJavaBean 的规范是提供无参构造和属性对应的 getter/setterServlet 里new User()然后setXxxJSP 里${user.username}调的就是getUsername()。如果少了 setter反射会把数据塞不进去这是初学者最容易遇到的 500 错误。package com.example.bean; import java.util.Date; public class User { private int id; private String username; private String password; private String email; private String phone; private Date regdate; public User() { } public int getId() { return id; } public void setId(int id) { this.id id; } public String getUsername() { return username; } public void setUsername(String username) { this.username username; } public String getPassword() { return password; } public void setPassword(String password) { this.password password; } public String getEmail() { return email; } public void setEmail(String email) { this.email email; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone phone; } public Date getRegdate() { return regdate; } public void setRegdate(Date regdate) { this.regdate regdate; } }这里没有让 User 实现 Serializable因为只在 Servlet 和 JSP 之间传递不涉及远程调用。但如果你把 User 放进了 HttpSession并且后面要集群部署就需要实现 Serializable否则序列化时会抛异常。这也是很多人提到的 bean 生命周期问题的一个侧面在纯 JspBeanServlet 里Bean 由 JVM 直接管理和回收不存在 Spring 容器里的 init/destroy 回调理解这一点对排查空指针有帮助。2.4 JDBC 工具类与 Connection 参数无论注册还是查询第一步都是拿到数据库连接。我习惯写一个 DBUtil 静态工具类把 URL、用户名、密码集中管理默认不对外开放。package com.example.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/bookshop?useUnicodetruecharacterEncodingutf8useSSLfalse; private static final String USERNAME root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }URL 里的characterEncodingutf8解决中文乱码传输的问题useSSLfalse是避免本机开发时出现 SSL 警告刷屏。驱动类名在 MySQL 5.x 之后是com.mysql.cj.jdbc.Driver如果你还在用老版本驱动要写com.mysql.jdbc.Driver否则会报ClassNotFoundException。Class.forName放在静态块里保证类第一次被加载时只执行一次这是 Java Web 项目里初始化驱动最常见的写法不会把驱动注册多次。2.5 UserDao用 PreparedStatement 操作数据表DAO 层把 SQL 和业务逻辑隔离开。注册功能对应 insert登录对应 select。下面是一个注册方法package com.example.dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import com.example.bean.User; import com.example.util.DBUtil; public class UserDao { public boolean insertUser(User user) { String sql INSERT INTO user(username, password, email, phone) VALUES(?,?,?,?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getEmail()); ps.setString(4, user.getPhone()); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } } public User findByUsernameAndPassword(String username, String password) { String sql SELECT * FROM user WHERE username? AND password?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setEmail(rs.getString(email)); user.setPhone(rs.getString(phone)); // 不把 password 放回 user避免在页面上暴露 return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }这里选PreparedStatement而不是Statement理由有两个一是 SQL 语句可以先用?占位参数通过 setString 绑定可读性更好二是能有效避免经典的单引号拼接注入比如用户名传 or 11时不会拼接进 WHERE 条件。注意 try-with-resources 写在 JDK 7 以后才行Eclipse 里如果编译报错先检查项目的 Java 版本是否调到 1.8 以上。插入成功返回executeUpdate()受影响的条数注册时还需要在 Servlet 里对用户名做唯一性校验否则数据库会抛 DuplicateKeyException。3. Servlet 控制器注册、登录、查询与浏览的请求流转3.1 请求映射从 form 到 ServletJSP 表单的action指向的是 Servlet 的映射路径。下面这张表是这套系统里最常见的四组映射请求路径Servlet功能常用方法/registerRegServlet用户注册POST/loginLoginServlet用户登录POST/book/listBookListServlet浏览全部图书GET/book/searchBookSearchServlet按关键字查询图书GET映射方式有两种一是给 Servlet 加WebServlet(/login)注解二是在web.xml里写servlet和servlet-mapping。课程设计里如果项目带有 config 和 web.xml优先用 web.xml 方式因为 Tomcat 7 或更早版本对注解支持不完整而且答辩时可以直接翻配置文件讲路由。后面代码示例采用注解方便看清路径实际使用时把注解换成 web.xml 配置即可。3.2 RegServlet注册流程的 doPost 写法注册页提交的数据是 username、password、email、phoneServlet 要做三件事接收参数、调用 DAO、根据结果跳转。package com.example.servlet; 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 com.example.bean.User; import com.example.dao.UserDao; WebServlet(/register) public class RegServlet extends HttpServlet { Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); String username request.getParameter(username); String password request.getParameter(password); String email request.getParameter(email); String phone request.getParameter(phone); if (username null || username.trim().isEmpty()) { request.setAttribute(error, 用户名不能为空); request.getRequestDispatcher(/register.jsp).forward(request, response); return; } User user new User(); user.setUsername(username.trim()); user.setPassword(password); user.setEmail(email); user.setPhone(phone); UserDao dao new UserDao(); boolean ok dao.insertUser(user); if (ok) { response.sendRedirect(request.getContextPath() /login.jsp); } else { request.setAttribute(error, 注册失败用户名可能已存在); request.getRequestDispatcher(/register.jsp).forward(request, response); } } }第一行的request.setCharacterEncoding(UTF-8)必须在读取参数之前调用否则 POST 过来的中文会变成乱码。response.setContentType设置响应给浏览器时的编码两个编码不一致是 JSP 页面出现“???”最常见的原因。注册失败时用forward回到注册页并带上 error 属性成功时用sendRedirect跳到登录页。为什么失败用 forward成功用重定向因为 forward 是服务端内部跳转浏览器地址栏不变刷新页面会再次提交表单容易出现重复注册重定向让浏览器重新发起一个 GET 请求到 login.jsp刷新不会重复提交。3.3 LoginServletSession 保存登录状态登录成功后必须把用户信息放到 Session后续 JSP 页面才能识别“当前是谁”。LoginServlet 的核心位置WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); User user new UserDao().findByUsernameAndPassword(username, password); if (user null) { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } HttpSession session request.getSession(); session.setAttribute(user, user); response.sendRedirect(request.getContextPath() /book/list); } }request.getSession()不传参数表示“没有 Session 就创建”适合登录成功场景。session.setAttribute(user, user)把 User 对象按引用存进 Session 容器Tomcat 默认用内存存储所以对象不需要序列化但如果你把session.setAttribute的 bean 放到 Spring 容器里管理生命周期会变得复杂这里是显式 set/get不存在 Spring 容器那种作用域问题。查到user为空时说明登录名或密码不对业务上不要提示“用户不存在”避免被扫出有效账号。3.4 BookListServlet图书浏览与关键字查询图书列表需要支持全部浏览和按书名查询两个入口。一个 Servlet 可以同时处理通过keyword参数区分package com.example.servlet; import java.io.IOException; import java.util.List; 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 com.example.bean.Book; import com.example.dao.BookDao; WebServlet(/book/list) public class BookListServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String keyword request.getParameter(keyword); BookDao dao new BookDao(); // 参数为空则查全部否则按书名关键字模糊查询 ListBook books (keyword null || keyword.trim().isEmpty()) ? dao.findAll() : dao.search(keyword.trim()); request.setAttribute(books, books); request.getRequestDispatcher(/bookList.jsp).forward(request, response); } }列表页用doGet处理因为图书浏览是只读操作用 GET 还可以让用户方便地把页面收藏或分享。keyword null || keyword.trim().isEmpty()这个判断是必须的只访问/book/list时不带参数如果直接调dao.search(null)SQL 里的LIKE就会传入一个 null导致结果为空或被数据库拒绝。查询完把books放进 request 作用域再用forward跳到 JSPJSP 里用${books}就能拿到这个集合。3.5 web.xml 映射当注解不生效时有些版本的 Tomcat 不扫描注解或者项目配置了metadata-completetrue会导致WebServlet被忽略页面提交后变成 404。这是 Tomcat 部署 Web 项目时最常见的坑之一。建议在 web.xml 里这样显式声明servlet servlet-nameloginServlet/servlet-name servlet-classcom.example.servlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameloginServlet/servlet-name url-pattern/login/url-pattern /servlet-mappingservlet-name只是个唯一标识可以任意取跟类名无关。url-pattern就是表单 action 里写的路径。一个 Servlet 可以配置多个 url-pattern比如图书列表和图书搜索可以都指向BookListServlet然后通过参数区分。配置完 web.xml 后要重新部署项目只刷新页面不会重新加载配置文件。4. JSP 表现层与表单会话把 Bean 渲染成可见页面4.1 JSP 页面最终也是 ServletJSP 不是浏览器执行的它由 Tomcat 先翻译成.java再编译成.class本质上是一个以页面模板形式存在的 Servlet。很多同学会问“jsp编译的class文件保存在哪里”其实跑过 Tomcat 后去看这个目录就能找到tomcat/work/Catalina/localhost/你的项目名/org/apache/jsp/里面会有login_jsp.java和login_jsp.class。理解了这一点就能解释为什么 JSP 页面上可以直接使用request、response、session、out、application这些内置对象因为它们正是_jspService方法签名里预先声明的局部变量。页面顶部的page指令决定了 JSP 内容的类型% page contentTypetext/html;charsetUTF-8 languagejava %contentType影响响应头的Content-TypecharsetUTF-8通知浏览器按 UTF-8 解码。如果 JSP 文件和这里声明的编码不一致页面上所有中文都会显示成乱码这是 JSP 个人信息展示页面、学生信息管理系统这些动态页面最容易出现的问题。4.2 注册页面的表单绑定register.jsp 是最典型的表单页action 指向 Servlet 映射method 用 post避免密码出现在地址栏。配合后端返回的 error 属性显示错误信息% page contentTypetext/html;charsetUTF-8 % html headtitle用户注册/title/head body h3注册新用户/h3 % if (request.getAttribute(error) ! null) { % p stylecolor:red% request.getAttribute(error) %/p % } % form action% request.getContextPath() %/register methodpost 用户名input typetext nameusername required/br/ 密码input typepassword namepassword required/br/ 邮箱input typetext nameemail/br/ 电话input typetext namephone/br/ input typesubmit value注册/ /form /body /htmlaction% request.getContextPath() %/register中的getContextPath()会动态补上项目上下文路径这样部署到任意名字的项目下都不会 404。required属性是 HTML5 原生校验能拦截空提交但服务端仍然要再校验一次因为浏览器校验可以被人为绕过。JSP 小脚本% %只用于简单的错误展示复杂的循环和条件应尽量交给 EL 和 JSTL。4.3 图书列表JSTL 遍历 Bean 集合图书列表页是这套系统里信息量最大的页面建议用 JSTL 的c:forEach遍历 request 里的 books 集合。JSP 内置对象常见有内置对象类型说明requestHttpServletRequest一次请求内有效存查询结果responseHttpServletResponse给浏览器返回响应sessionHttpSession同一用户多次请求有效存登录信息applicationServletContext整个应用全局共享慎用bookList.jsp 的列表部分% page contentTypetext/html;charsetUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html body h3图书列表/h3 form action% request.getContextPath() %/book/list methodget input typetext namekeyword placeholder输入书名关键字/ input typesubmit value查询/ /form table border1 tr th书名/thth作者/thth价格/thth出版社/th /tr c:forEach items${books} varbook tr td${book.bookname}/td td${book.author}/td td${book.price}/td td${book.publisher}/td /tr /c:forEach c:if test${empty books} trtd colspan4没有找到任何图书/td/tr /c:if /table /body /html${books}会调用request.getAttribute(books)实际返回的是 List items属性接收任何 Collection 类型。${book.bookname}会调用getBookname()所以 Bean 的属性名必须和数据库字段映射好否则 500 或者输出空。JSTL 标签库需要在 Web 工程的WEB-INF/lib下放入jstl-1.2.jar如果缺失页面会报Unable to find taglib错误。4.4 个人信息展示Session 中取 Bean登录成功后userInfo.jsp 需要判断 Session 里有没有 user 对象有就展示个人信息没有就跳转登录页。这是“jsp个人信息展示页面”的标准结构c:choose c:when test${sessionScope.user ! null} p欢迎${sessionScope.user.username}/p p邮箱${sessionScope.user.email}/p p电话${sessionScope.user.phone}/p /c:when c:otherwise p请先 a href% request.getContextPath() %/login.jsp登录/a/p /c:otherwise /c:choose${sessionScope.user}明确指定从 session 作用域取值而不是用${user}让容器在 page/request/session/application 四个作用域里依次查找。后者在同一个页面里如果 request 也存了同名属性就会取到错误的值。Session 里存的 User 如果在登录后又被修改由于是引用原对象也会变所以不要轻易在跨页面流程里直接改sessionScope.user的字段。4.5 全局编码 Filter一个过滤器解决所有页面乱码JSP 页面和 Servlet 各写一遍编码设置很容易漏通常用一个 Filter 把全局请求和响应编码统一处理package com.example.util; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.annotation.WebFilter; 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); } }WebFilter(/*)表示拦截所有请求路径doFilter里设置编码后调用chain.doFilter放行这样每个 Servlet 和 JSP 都不需要再单独设置 request 编码。response.setCharacterEncoding(UTF-8)只影响字符流如果页面要返回 HTML最好在 JSP 的 page 指令里再显式写出 content type。Filter 的注册顺序只影响多个过滤器之间的顺序单过滤器场景下不需要关心。5. 部署、排错与防注入技巧从能跑到跑稳5.1 Eclipse Tomcat 三步部署拿到源码后最常见的问题是找不到怎么运行。在 Eclipse 里导入这个 Web 项目File → Import → Existing Projects into Workspace选择项目根目录。确保项目上右键 Properties → Targeted Runtimes 勾选了 Tomcat 版本。右键项目 → Run As → Run on Server选择已配置的 Tomcat 实例。启动后访问http://localhost:8080/项目名/login.jsp。如果直接用Run as Java Application当然会报“找不到或无法加载主类”因为这本来就不是一个普通 Java 工程必须交给 Tomcat 运行。部署完毕后再去查tomcat/work/Catalina/localhost/项目名/org/apache/jsp/这时能看到 jsp 翻译后的.java和.class文件这就是 JSP 编译产物的实际存储位置。5.2 高频异常排查表现象可能原因处理方向404表单 action 或 url-pattern 不一致检查 web.xml 或 WebServlet 路径500Bean 属性不存在、DAO 返回 null看 Tomcat 日志里的 Caused byClassNotFoundException缺 mysql-connector-java.jar把 jar 放到 WEB-INF/lib中文乱码Filter 未拦截、JSP 编码不一致统一 UTF-8页面和数据库字符集对齐端口占用Tomcat 8080 被占用修改 conf/server.xml 端口或杀掉占用进程排查 500 时不要只看页面上那几行直接打开tomcat/logs/localhost.yyyy-MM-dd.log几乎所有异常原因都写在 Caused by 里。最常见的org.apache.jasper.JasperException往往是 JSP 引用了不存在的 getter或者 JSTL 标签没有引入而导致解析失败。5.3 用 PreparedStatement 封死 SQL 注入这个网上购书系统如果用到关键字查询最容易写出这样的拼接 SQL// 错误示例直接把参数拼进 SQL String sql SELECT * FROM book WHERE bookname LIKE % keyword %; Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql);当 keyword 传入 OR 11时SQL 会变成WHERE bookname LIKE %% OR 11%条件恒成立绕过查询边界。改成占位符就可以解决String sql SELECT * FROM book WHERE bookname LIKE ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, % keyword %); ResultSet rs ps.executeQuery();LIKE的模糊匹配是在占位符参数值里手动拼上%SQL 本身不包含用户输入。我一般会再写一个可复用的search方法对keyword做两次转义一次用trim()去掉首尾空格一次把%和_这两个通配符转义掉避免用户输入%时把全表查出来。这个改动不增加多少代码但对于公开部署的 Web 项目是必须守住的一层底线。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价