JSP电子书城系统开发实战:从技术选型到购物车与部署避坑
发布时间:2026/9/28 1:38:08来源:尧图网络
简介基于JSP开发的完整电子书城系统源码包面向Java入门开发者与高校课程设计场景可用于学习传统Java Web分层架构与快速搭建图书电商原型。项目采用MVC设计模式后台以ServletJDBC为核心通过c3p0连接池访问MySQLservice层封装业务接口util包统一收纳可复用工具类前端使用JSPJSHTMLCSS通过EL表达式与c标签完成数据展示整体模块划分清晰。资源共587个文件含37个java源码、20个jsp页面、137个js脚本、48个css样式以及jpg/png图片素材、jar依赖包和sql数据库脚本压缩包仅10.84MB。配套Word版参考文档与可直接导入的数据库文件便于直接运行调试或参考学习。已有459人浏览学习适合完成课程设计、毕业设计或希望掌握JSP传统开发模式的读者。1. 基于JSP的电子书城系统课设流量王但能跑通闭环的人不多每年毕设选题季“基于JSP的电子书城系统”都是Java方向的高频选项。原因很直白电子书城听起来体面——有商品列表、购物车、订单功能能讲出故事截图放进论文里也像个“系统”技术栈又固定——JSP、Servlet、MySQL正好是学校《Java Web》课程里教过的那一套。但真正动手的人会发现这个题远比想象中更考验基本功分页怎么传参、购物车放Session还是Cookie、下单时库存和订单怎么保证一致任何一环拎出来都能让一个“会写Java”的新手卡上一整天。这篇文章不是给你讲一遍“增删改查”的教科书。我会按自己做过、也带别人做过这类单体Web项目的路线把电子书城从技术选型、建表、跑通首页到购物车下单、部署避坑的完整路径拆开顺带把JSP方向最常见的翻车点和参数细节讲透。适合正在做JSP课设/毕设的学生也适合想用一个小项目快速捡回Java Web基本功的开发者。你按章节顺序动手最后能拿到一个能演示、能答辩、能部署到一个Tomcat上跑起来的完整系统。2. JSP电子书城的技术选型为什么Servlet JSP MySQL仍是课设首选2.1 为什么要用JSP而不是Vue Spring Boot聊技术选型前先说个现象把“电子书城”换成“图书商城”“零食商城”这类题目的本质没变——都是典型的单体Web应用。JSP在1999年随J2EE一起出现到今天还被课设大量使用不是因为它新而是因为它正好卡在“学完Java基础”和“接触Spring Boot”之间的位置。JSP的核心机制是JSP文件在第一次被访问时转换成Servlet类再被编译执行。这意味着你写的是“带HTML标签的Java页面”但实际跑的是一个.do逻辑。它和前后端分离的工程化开发有一个本质差别——没有跨域、没有接口约定、没有前端构建链浏览器直接请求.jsp服务器直接把渲染好的HTML扔回去。对一个课设或者一次Java Web基本功训练来说少掉的那一多半复杂度全都省在了调试和演示上。我一般会建议如果你这个阶段的目的是把Java Web的会话状态、请求转发、JDBC事务这些底层机制搞明白而不是熟练掌握Vite和Axios那就选JSPServlet。这套组合把请求处理、页面渲染和数据库访问三类代码物理隔离在三层架构里面试时被问到“八股文”里的Servlet生命周期、Session原理你至少亲手用过不至于纯背。2.2 项目目录与三层分包每个包都对应一类职责JSP电子书城的项目结构我推荐按下面这个目录来组织。这不是唯一的做法但这是最能让答辩老师一眼看出你“懂分层”的做法src/main/java/ com.bookstore.entity/ User, Book, Order, OrderItem com.bookstore.dao/ UserDao, BookDao, OrderDao, OrderItemDao com.bookstore.service/ UserService, CartService, OrderService com.bookstore.servlet/ LoginServlet, RegisterServlet, ListBookServlet, AddCartServlet, OrderServlet src/main/webapp/ static/ css/, js/, images/ WEB-INF/web.xml JSP页面 index.jsp, login.jsp, register.jsp, cart.jsp, order.jsp实体、DAO、Service、Servlet四层每一层职责是单向的Servlet层只做参数接收和页面跳转Service层做业务判断库存够不够、用户是否已登录DAO层只和数据库打交道。JSP页面放在webapp根目录下静态资源放static目录web.xml做Servlet映射——如果用的是Servlet 3.0 的注解方式web.xml可以极简但保留它可以让项目从一个Tomcat 8容器迁到另一个时少踩兼容性坑。2.3 从0搭数据库book、user、order三张核心表数据库设计是整个系统里最先落地的部分后面所有代码都围着这三张表转。电子书城的表我建议这样设计用户表userid、username、password、email、nickname、create_time。密码字段存MD5或SHA-256摘要不要明文。注册和登录两页共用这一张表。书籍表bookid、title、author、publisher、price、stock、cover_url、description。cover_url存相对路径比如static/images/cover1.jpg不要把图片二进制写进数据库否则首屏加载会慢得让你怀疑人生。订单表orderid、order_no、user_id、total_amount、status、create_time。订单明细表order_itemid、order_id、book_id、book_title快照、price、quantity。Book单价以后可能改但订单明细里的价格必须留存下单那一刻的值所以明细表要冗余一个book_title和price字段。建表SQL里最需要注意的就是外键和索引。外键约束在课设里可以加但会增加删除数据的成本索引至少要在order表的user_id、order_item表的order_id上各建一个否则订单列表页随着数据量上涨会越来越慢。2.4 javax和jakarta引依赖时最容易绊倒人的版本问题写JSP项目时你会在pom.xml或手动拷贝的lib目录里引入Servlet和JSTL的依赖。这里藏着一个近年最常见的坑Tomcat 10开始Apache把Java EE的命名空间从javax.换成了jakarta.。也就是说你在网上找到的大部分教程里的依赖坐标是javax.servlet-api如果照搬到Tomcat 10上启动时大概率直接报ClassNotFoundException。容器版本servlet坐标前缀JSTL坐标前缀说明Tomcat 8/9javax.servletjavax.servlet.jsp.jstl老教程默认配Tomcat 9最稳Tomcat 10jakarta.servletjakarta.servlet.jsp.jstl新命名空间代码里import也要改Spring Boot内置Tomcat由spring-boot-starter-web统一管理不单独引JSTL走前后端分离路线时才常用如果你是课设我的建议是直接用Tomcat 9 javax坐标。不是jakarta不好而是你想查“为什么报错”时90%的社区问答和博客都建立在javax的基础上跟主流能少踩很多坑。项目代码里凡是import javax.servlet.http.HttpServlet的地方也要和容器版本一一对应。3. 用IDEA新建JSP项目并跑通首页书籍列表背后的完整链路3.1 IDEA新建JSP Web项目的正确姿势IntelliJ IDEA里新建JSP项目常见做法是不用Maven骨架直接创建一个普通的Java项目然后手动添加Web支持。具体步骤File → New → Project → Java选好JDK后不勾选模板建完空项目后右键项目根目录 → Add Framework Support → 勾选Web ApplicationIDEA会帮你生成webapp目录和WEB-INF/web.xml。再把Tomcat配置进去Run → Edit Configurations → 加一个Tomcat Server → Local在Deployment选项卡里把项目artifact部署进去Application context设置为/bookstore。这一步有玄学成分IDEA的版本不同菜单层级略有差别但核心动作就两个——给项目加上Web能力再告诉Tomcat“去哪个目录找项目”。很多人栽在这地方是因为Maven导入的超大骨架项目里带了多个web目录IDEA分不清该部署哪一个。用手动加Web支持的方式目录是死的杜绝了这层混乱。项目结构确认后先写一个最小的测试Servlet确认Tomcat和IDEA之间的关联打通启动后浏览器访问http://localhost:8080/bookstore/hello能输出字符串再开始写书城代码。这个步骤只需要一分钟却能让后面所有排查都有基准点——别直接上来就怼几十个文件出了问题谁也说不清是Tomcat没配好还是代码有bug。3.2 JDBC连接与数据源从DriverManager到连接池访问数据库是电子书城的根基JSP方向最常见的数据访问方式是JDBC。最原始的写法是每个DAO方法里都做一次Class.forName和DriverManager.getConnection缺点是每次连接都重新握手五百并发能把MySQL拖到卡死。我推荐在课设阶段直接用一个轻量连接池——DBCP2或者Druid都行配一个连接池工具类全项目共用。// JdbcUtils.java package com.bookstore.dao; import com.alibaba.druid.pool.DruidDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class JdbcUtils { private static DruidDataSource dataSource; static { dataSource new DruidDataSource(); dataSource.setDriverClassName(com.mysql.cj.jdbc.Driver); dataSource.setUrl(jdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai); dataSource.setUsername(root); dataSource.setPassword(你的密码); dataSource.setInitialSize(5); // 初始连接数 dataSource.setMaxActive(20); // 最大活跃连接数 } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }代码逻辑很简单静态块里初始化Druid数据源所有DAO通过JdbcUtils.getConnection拿连接。三个必改的参数是URL里的schema名bookstore、用户名密码、以及serverTimezone。URL加了characterEncodingutf8和useSSLfalse分别是解决中文乱码和MySQL 8默认SSL握手报错的关键参数。connection不再使用时调用close()Druid会把连接放回池里而不是真关掉。3.3 首页书籍列表先走通一条完整的数据链路首页是电子书城的门面典型需求是一进来就看到一屏书籍卡片每张卡片有封面、书名、价格和“加入购物车”按钮。用三层架构来走这条链路BookServlet接收请求调用BookService的listBooks()方法BookDAO执行SELECT再把返回的List 塞进request作用域最后forward到index.jsp渲染。-- BookDAO.listBooks() SELECT id, title, author, publisher, price, stock, cover_url, description FROM book ORDER BY id DESC;对应的JSP页面里用JSTL标签遍历集合是推荐做法。如果你用IDEA新建项目JSTL需要额外引入两个jar包别忘记。下面的页面骨架就是首页的核心渲染逻辑% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle电子书城/title/head body div classbook-grid c:forEach items${bookList} varbook div classbook-card img src${book.coverUrl} alt封面 width120 height160/ h3${book.title}/h3 p作者${book.author} 定价${book.price}/p a href/bookstore/cart/add?bookId${book.id}加入购物车/a /div /c:forEach /div /body /html c:forEach是这套渲染的核心标签items指向Servlet传进来的Listvar是每次循环的当前元素。所有在EL表达式里出现${book.title}的属性都对应Book实体类里名为title的字段。把链接href写成/bookstore/cart/add这个路径叫绝对根路径可以保证不管当前页面嵌在哪一层目录下都不会404。第一次跑通这条链路你的电子书城就有了“生命线”后面加个人中心、购物车、订单都是在这个通用路径上做加法。3.4 为什么我在JSP里尽量不写Java代码片段JSP页面里其实可以直接写 % ... % 这样的脚本代码从数据库取出数据在里面拼HTML也能跑而且很多老教程就是这么教的。但我给你的建议是不要在JSP里写Java逻辑用EL表达式和JSTL标签替代。原因有两个层面。第一是维护性。JSP文件每被修改一次容器都要重新编译如果在页面里写了大量Java控制语句改个样式都可能牵扯到Java语法错误前端模板的灵活性全没了。第二是职责边界。课设答辩时老师经常翻源码看分工是否清晰他看到JSP里塞了大段业务逻辑第一反应就是“你没搞懂MVC”。把数据准备工作放在Servlet里JSP只负责拿到数据后显示这个思路贯穿整个电子书城的开发过程。4. 购物车、分页与订单结算Servlet、Session和事务的完整链路4.1 分页查询一页十本书页码参数设计的套路电子书城的书籍数量超过五十本后一次性把所有记录查出来再丢给页面渲染就不是好方案了。分页是这里绕不开的交互每页显示10本底部一排页码点第3页就跳到第3页的书。分页的核心是页码规则的确定请求参数page默认值为1后端根据page和pageSize计算SQL里的LIMIT偏移量。// BookDAO.findPage(int page, int pageSize) // 假设 page3, pageSize10, 则 offset(3-1)*1020 public ListBook findPage(int page, int pageSize) { int offset (page - 1) * pageSize; String sql SELECT * FROM book ORDER BY id LIMIT ?, ?; try (Connection conn JdbcUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, offset); ps.setInt(2, pageSize); // ...执行并封装结果 } return null; // 这里返回封装后的List }LIMIT后面有两个占位符第一个是偏移量offset指跳过多少条记录第二个是pageSize指最多返回多少条。这个二元的参数配置是分页SQL的核心千万不要把LIMIT写死成LIMIT 0,10——一旦page不是1就翻不对页。PreparedStatement的?参数是JDBC预编译的特性既防SQL注入又让MySQL可以复用查询计划。页码条在JSP里用一个c:forEach循环生成总页数由总记录数除以pageSize后向上取整得出。4.2 购物车放Session还是Cookie我选Session的四个理由电子书城加购功能的实现很简单但容器选择总要纠结一下购物车数据放Session还是Cookie你可能也见过一些教程把购物车用JavaScript数组存在localStorage里那不是Java Web的课设该用的方案。我推荐放Session理由有四条第一Session是服务端存储不占客户端空间也不受Cookie 4KB大小限制放几十本书毫无压力第二Session里存对象比存字符串更直观Cart是一个MapBook, Integer书对象直接放在Map里当key就行第三课程考核点就是HttpSession的工作机制用它是往考点上凑第四Cookie方案需要自己处理序列化和过期策略对课设来说是纯成本。加购的Servlet写起来很短但关键点全在写法和边界处理上WebServlet(/cart/add) public class AddCartServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 从请求里拿bookId和数量 int bookId Integer.parseInt(req.getParameter(bookId)); int quantity 1; // 课设里先固定默认数量进阶可加参数 // 2. 从Session里取购物车拿不到就新建 HttpSession session req.getSession(); MapInteger, CartItem cart (MapInteger, CartItem) session.getAttribute(cart); if (cart null) { cart new HashMap(); session.setAttribute(cart, cart); } // 3. 已存在的书只加数量否则查库后放入购物车 CartItem item cart.get(bookId); if (item ! null) { item.setQuantity(item.getQuantity() 1); } else { item new CartItem(bookService.findById(bookId), quantity); cart.put(bookId, item); } // 4. 重定向回来源页面避免刷新重复加购 resp.sendRedirect(req.getContextPath() /cart.jsp); } }这段代码的逻辑链很清晰从请求参数中拿到加购目标在Session里维护一个持久的小型仓库。注意第四步用的是sendRedirect不是forward——如果forward到页面用户刷新浏览器时这个Servlet会再执行一次书就被加了两遍。这是“重定向防止表单重复提交”思路在加购场景的变体也是新手容易踩的一个坑。4.3 下单结算必须走事务从库存扣减到订单落库购物车有了下一步就是下单。下单的业务逻辑可以用一句话描述把购物车里所有条目整理成一条订单记录同时扣减每本书的库存。这两件事必须是一个原子操作——要么都成功要么都失败不能出现订单生成了但库存没扣、或者库存扣了但订单没生成的情况。这个“要么全做要么全不做”的保证就是数据库事务。public boolean createOrder(int userId, MapInteger, CartItem cart) { Connection conn null; try { conn JdbcUtils.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 // 1. 插入订单主表 String orderSql INSERT INTO order (order_no, user_id, total_amount, status) VALUES (?,?,?,?); PreparedStatement ps1 conn.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS); // ...填充参数得到自增订单id // 2. 逐商品扣库存并插入订单明细 String stockSql UPDATE book SET stock stock - ? WHERE id ? AND stock ?; PreparedStatement ps2 conn.prepareStatement(stockSql); // 如果受影响行数为0说明库存不足抛出异常回滚 String itemSql INSERT INTO order_item (order_id, book_id, book_title, price, quantity) VALUES (?,?,?,?,?); // 遍历cart逐个插入明细 conn.commit(); // 所有都成功提交 return true; } catch (Exception e) { if (conn ! null) try { conn.rollback(); } catch (SQLException ex) {} log.error(下单失败, e); return false; } finally { if (conn ! null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ex) {} } }事务的开关只有一行conn.setAutoCommit(false)。从这一行开始后面所有SQL都在同一个事务里执行直到commit或rollback。这里最值得学习的是库存扣减写成了UPDATE book SET stock stock - ? WHERE id ? AND stock ?——把“判断库存够不够”和“扣库存”合并成一条SQL数据库层面就挡掉了并发下单时库存被扣成负数的问题。如果先查询库存再说扣减并发时会读到过期数据这属于Java面试里常见的一致性考点在课设里能主动用上答辩会加分。4.4 表结构上的“订单-明细”一对多是怎么落地的订单表存总金额明细表存每一本书的数量和单价这个设计在第三章节的建表SQL里已经埋下伏笔。这里展开讲讲代码里是怎么维护这种一对多关联的。用户在结算页面看到的是订单总额这个total_amount不应该从前端传过来——前端传金额等于让用户自己报价格随便改个参数就能免费下单。正确做法是服务端遍历购物车把每个CartItem的单价乘以数量累加算出总额再写入订单表。明细表的book_title、price字段则在插入时从Book实体里取快照。这样做保证了订单历史记录不被后续商品改价影响也避免了Web应用层被篡改金额的风险。如果答辩时被问“为什么订单表不直接存商品快照”这个“历史数据不可变”的设计就是最好的答案。实际开发中哪怕用户买完书以后书下架了订单详情仍然要显示用户买过什么、付了多少钱这正是快照存在的原因。5. JSP项目部署与调试避坑5个高频翻车现场5.1 启动即报ClassNotFoundExceptionTomcat 10与javax包名不匹配现象项目在别人电脑上跑得好好的自己配了个Tomcat 10.x一启动控制台就抛java.lang.ClassNotFoundException: javax.servlet.http.HttpServlet或者在访问任意JSP页面时报500。原因Tomcat 10开始不再提供javax.servlet的类改成了jakarta.servlet。你代码里import的是javax容器里根本没有这些类。解决两个方向二选一。要么把Tomcat换成9.0.x要么把所有import javax.*改成jakarta.*并换用新依赖。课设推荐前者换来的是与90%网文教程的一致兼容性。改完容器版本后务必clean一下项目再重新部署IDEA有时缓存的编译产物会残留旧类名。5.2 MySQL 8连接失败driver class改名与时区校验现象程序启动后第一次查数据库报SQLNonTransientConnectionException或者控制台出现Establishing SSL connection without servers identity verification is not recommended这样的保错。原因MySQL 8.0的JDBC驱动类不再是com.mysql.jdbc.Driver而是com.mysql.cj.jdbc.Driver同时驱动会要求URL里带时区参数serverTimezone并默认尝试SSL握手。解决驱动类名改成新坐标URL追加useSSLfalse和serverTimezoneAsia/Shanghai。这个坑在新装MySQL 8的机器上百分之百会出现属于最常见的起步卡点。如果数据库密码里包含特殊字符比如或#还需要在URL里做URL编码——我见过有人把密码写进URL里面有个导致整条URL解析错乱连报错都看不懂。5.3 中文乱码请求、响应、数据库三个方向逐一排查现象页面标题中文显示正常但从表单提交进去的中文变成了问号或乱码数据库里存的也是乱码。原因乱码的根源是字符集在链路里不一致。JSP页面本身的pageEncoding是UTF-8但HTTP请求的默认编码是ISO-8859-1Tomcat接收POST参数时用了错误编码。另外MySQL连接URL里的characterEncoding如果不设置驱动会用数据库默认字符集。解决三个位置按顺序统一。第一JSP文件第一行写pageEncodingUTF-8模板里的meta charset也写UTF-8第二在Servlet里读取参数前加request.setCharacterEncoding(UTF-8)或在web.xml配CharacterEncodingFilter过滤所有请求第三连接URL追加useUnicodetruecharacterEncodingutf8。这三步全做齐基本能根除乱码。注意GET请求的query string不受setCharacterEncoding控制中文参数要用URLEncoder编码后再拼接或干脆把所有参数都走POST。5.4 nginx或Tomcat能访问首页但点开JSP页面404现象用nginx做反向代理后访问http://你的域名/能打开静态欢迎页但点击任何一个链接跳转到jsp页面就404或者浏览器直接弹下载框。原因nginx只处理静态文件转发并不具备解析JSP的能力——它拿到的是JSP源文件的请求找不到对应的物理文件或映射规则自然就404了。JSP文件的编译执行一定要在Servlet容器Tomcat里完成。解决让nginx只监听80端口把包含.jsp的请求通过proxy_pass全部转发给后端的Tomcat默认8080端口nginx自身不要尝试直接serve这个.jsp路径。配置核心是location拦截location /bookstore { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }在IDEA里调试时没碰到这个问题是因为请求直接打到Tomcat没经过nginx。部署到服务器引入nginx后才暴露这个边界nginx负责静态资源和反向代理JSP的活还得交给Tomcat。这个坑在传统JSP项目打包成war部署时特别常见。5.5 图片、CSS全部404静态资源路径被过滤器拦截现象首页HTML结构出来了但所有img标签裂图CSS样式完全没有打开F12一看全是404。原因两种可能。一是把静态资源放在了WEB-INF目录下面——WEB-INF是Servlet规范里的受保护目录浏览器永远无法直接访问它里面的任何文件二是Servlet的WebServlet映射用了“/*”拦截所有请求把静态文件请求也拦进了Servlet逻辑。解决静态资源统一放到src/main/webapp/static/下如果非要放在WEB-INF下必须在web.xml里显式配置静态资源映射。至于过滤器用“/*”拦截时要在过滤器中区分请求类型对以/static/开头的路径直接放行String path req.getRequestURI(); if (path.contains(/static/)) { // 放行不处理 chain.doFilter(req, resp); return; }这一条是安全性和资源加载的交汇点排查时先确认资源文件在磁盘上的实际路径再确认容器映射。6. 从课设到能演能用的书城验证方法与两个低成本进阶改造一个能拿去演示的电子书城至少要完整跑通七步链路注册新用户、登录、浏览首页、翻页、加入购物车、下单结算、在订单列表里看到这笔订单。每一步都对应一个Servlet和一张表我建议你每做完一个环节就往这个清单上打一个勾最后答辩前从头到尾走一遍全链路比临时背代码有效得多。第一个低成本改造是模糊搜索。把首页加一个搜索框Servlet读取keyword参数DAO层把SQL改成WHERE title LIKE ?占位符传% keyword %。第二个改造是销量排行。一条SQL就能从订单明细表里聚合出最受欢迎的十本书SELECT book_id, book_title, SUM(quantity) AS sales FROM order_item GROUP BY book_id ORDER BY sales DESC LIMIT 10;这两个改造一共只需要改两个文件但让系统从“有基本功能”升级成了“有真实可用感”答辩时是很加分的亮点。另外一个小建议把Tomcat的配置文件server.xml里的URIEncoding确认成UTF-8这是很多乱码问题的后悔药。这套方案的价值不在于技术新而在于用最小依赖把Java Web的核心链路完整走了一遍——我当年带新人时发现能独立讲清楚Session生命周期的人十个里也就两三个。希望这篇笔记能帮到正在为JSP电子书城熬夜的你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网