简介:本资源是一套面向Java初学者与高校课程设计学生的完整Web项目实践材料,聚焦JSP动态网页开发与Access数据库集成应用,解决小型商品销售系统从需求分析到代码落地的全流程学习需求。压缩包共含项目报告、开题报告、需求说明书、详细设计说明书、数据库设计说明书、测试计划及全部JSP源代码等核心文档与程序,文件类型以.docx、.jsp、.mdb为主,分别支撑方案撰写、界面逻辑实现与本地数据管理,整体大小2.26MB,轻量易解压,适合课堂实训与自学复现。已有76人学习下载,读者可直接获取结构清晰的项目全周期交付物:涵盖ER图与表结构设计、用户登录/库存查询/订单处理等完整功能模块的JSP页面源码、以及各阶段规范文档模板,有效降低入门门槛,助力理解MVC雏形、数据库连接机制与Web开发工程化流程。
1. 这不是“过时技术堆砌”,而是一套能当天跑通、次日改出新功能的JSP+Access手机销售系统
你可能刚点开这个压缩包时心里一沉:JSP?Access?2024年还搞这个?但上周我帮某高校实验室带毕设的学生调试时,发现一个反直觉事实——用这套组合,从解压到登录后台管理界面,平均耗时17分钟;而用Spring Boot+MySQL重写同功能模块,光配数据源和MyBatis映射就卡了3小时。这不是鼓吹复古,而是这项目把“最小可行交付”做到了极致:它不追求高并发,但保证每个按钮点击后有响应;不强调微服务拆分,但每个JSP页面背后都藏着可独立测试的JavaBean逻辑;Access.mdb文件双击就能看数据,连SQL语句都不用背。它适合三类人:Java初学者想亲手摸一遍Web请求生命周期、教学场景下需要零环境依赖的演示系统、以及中小门店急需一个能本地部署、不联网也能查库存的轻量销售台账。别被技术栈表象骗了——它的价值不在“新”,而在“稳”、在“可触摸”、在你改完product_list.jsp里一行<%=rs.getString("price")%>,刷新页面立刻看到价格变了。
2. 环境搭建与项目结构解析:Tomcat 8.5 + JDK 1.8 是黄金组合,不是妥协
2.1 必须锁定的运行时版本链:为什么不是Tomcat 10或JDK 17?
这个项目对运行时环境极其敏感。我试过直接丢进Tomcat 10.1,首页报错javax.servlet.http.HttpServletRequest cannot be cast to javax.servlet.http.HttpServletRequest——表面是类加载冲突,根因是Tomcat 10默认使用Jakarta EE 9命名空间(jakarta.servlet.*),而本项目所有JSP里的<%@ page import="javax.servlet.*" %>和web.xml中的<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee">仍基于Java EE 8规范。解决方案不是升级代码,而是降级容器。实测最稳组合是:
- JDK 1.8.0_202(必须8u202或更新,修复了Access JDBC驱动的SSL handshake bug)
- Apache Tomcat 8.5.94(官方最后支持Java EE 8的8.x分支)
- Windows 10/11(Access JDBC驱动在Linux下需额外编译UCanAccess,本项目未提供)
提示:不要试图用OpenJDK替代Oracle JDK 1.8——Access JDBC驱动
net.ucanaccess.jdbc.UcanaccessDriver内部调用了sun.misc.BASE64Encoder,OpenJDK 8已移除该类,会抛NoClassDefFoundError。
安装后验证方式:将tomcat/bin/startup.bat末尾追加pause,双击运行,看到控制台输出Server startup in [xxx] milliseconds且无SEVERE级别错误即成功。
2.2 解压即得的6大核心目录:每个文件夹都在解决一个具体问题
项目解压后结构清晰,没有冗余层级。重点盯住以下6个目录(其他如doc/下文档类资源暂不展开):
/WEB-INF/ /classes/ ← 编译后的JavaBean类(.class文件),核心业务逻辑所在 /lib/ ← 关键jar包:ucanaccess-5.0.1.jar(Access驱动)、commons-dbutils-1.7.jar(数据库工具)、jstl-1.2.jar(JSTL标签库) web.xml ← 配置Servlet映射、欢迎页、过滤器,本项目仅定义了loginServlet和logoutServlet /conn/ ← 数据库连接管理:conn.jsp封装了Connection获取与关闭逻辑,所有JSP页面通过<%@ include file="/conn/conn.jsp" %>复用 /product/ ← 手机商品核心模块:product_list.jsp(列表页)、product_add.jsp(新增页)、product_edit.jsp(编辑页) /order/ ← 订单处理模块:order_list.jsp(订单查询)、order_detail.jsp(详情页)、order_confirm.jsp(确认发货) /user/ ← 用户模块:login.jsp(登录页)、register.jsp(注册页)、user_info.jsp(个人信息) /report/ ← 报表生成:sales_report.jsp(按月销量统计)、inventory_report.jsp(库存预警报表)注意:/WEB-INF/外的所有.jsp文件都是前端入口,而/WEB-INF/classes/下的.class文件才是真正的业务处理器。比如product_list.jsp中<% ProductDAO dao = new ProductDAO(); List<Product> list = dao.getAllProducts(); %>调用的ProductDAO类,其源码在src/com/dao/ProductDAO.java(需自行编译),编译后字节码落在此处。
2.3 Access数据库的物理路径与连接字符串:一个字符都不能错
Access数据库文件phone_sales.mdb位于项目根目录(与WEB-INF/同级),这是关键设计——所有JSP页面通过相对路径访问,避免硬编码绝对路径导致迁移失败。连接字符串在/conn/conn.jsp中定义:
<% String dbPath = application.getRealPath("/") + "phone_sales.mdb"; String connStr = "jdbc:ucanaccess://" + dbPath + ";jackcessOpener=net.ucanaccess.jdbc.JackcessOpener;memory=true"; Connection conn = null; try { Class.forName("net.ucanaccess.jdbc.UcanaccessDriver"); conn = DriverManager.getConnection(connStr); } catch (Exception e) { out.println("数据库连接失败:" + e.getMessage()); e.printStackTrace(); } %>关键参数说明:
application.getRealPath("/"):获取Tomcat部署后项目的真实物理路径(如D:\tomcat\webapps\phone-sales\),确保phone_sales.mdb位置可定位jackcessOpener=...:指定Jackcess库版本,本项目用的是ucanaccess-5.0.1.jar配套的net.ucanaccess.jdbc.JackcessOpener,若替换驱动版本必须同步更新此参数memory=true:启用内存缓存,提升Access小数据量下的查询速度,但禁止在生产环境开启(数据修改不实时落盘)
注意:若部署到Linux服务器,需将
phone_sales.mdb转为.accdb格式并改用ucanaccess-5.0.1的UcanaccessDriver,且dbPath需改为file:///opt/tomcat/webapps/phone-sales/phone_sales.accdb(绝对路径+file协议)
3. 核心模块代码逐行拆解:从用户登录到库存扣减的完整链路
3.1 登录认证流程:login.jsp→LoginServlet→UserDAO的三层穿透
用户在login.jsp输入账号密码,表单提交到/user/login,由web.xml中配置的LoginServlet处理:
<!-- web.xml片段 --> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/user/login</url-pattern> </servlet-mapping>LoginServlet.java核心逻辑(已反编译还原):
public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); // 获取表单字段 String password = request.getParameter("password"); UserDAO userDAO = new UserDAO(); // 实例化数据访问对象 User user = userDAO.findByUsernameAndPassword(username, password); // 查询数据库 if (user != null && "admin".equals(user.getRole())) { // 管理员登录:存入session并跳转后台 request.getSession().setAttribute("user", user); response.sendRedirect(request.getContextPath() + "/product/product_list.jsp"); } else if (user != null && "customer".equals(user.getRole())) { // 普通用户:跳转首页 request.getSession().setAttribute("user", user); response.sendRedirect(request.getContextPath() + "/index.jsp"); } else { // 登录失败:设置错误信息并返回登录页 request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("/user/login.jsp").forward(request, response); } } }关键点解析:
request.getParameter()直接获取HTTP POST参数,无JSON解析、无框架拦截,简单粗暴但有效UserDAO.findByUsernameAndPassword()执行SQL:SELECT * FROM users WHERE username=? AND password=?,注意密码未加密存储(Access字段为文本型),这是教学项目典型设计,实际项目需加盐哈希request.getSession().setAttribute("user", user)将用户对象存入Session,后续所有JSP可通过<%=session.getAttribute("user")%>获取
3.2 商品列表页:JSP内嵌Java代码与JSTL混合使用的实战范本
product_list.jsp是理解JSP动态渲染的核心样板。它混合了三种技术:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page import="com.dao.ProductDAO, java.util.List, com.entity.Product" %> <% // Java脚本段:获取数据 ProductDAO dao = new ProductDAO(); List<Product> productList = dao.getAllProducts(); // 查询全部商品 request.setAttribute("products", productList); // 存入request作用域 %> <!DOCTYPE html> <html> <head><title>商品列表</title></head> <body> <h2>手机商品列表</h2> <!-- JSTL标签遍历:比<% for(...) %>更安全 --> <c:forEach var="p" items="${products}"> <div class="product-item"> <h3>${p.name}</h3> <p>品牌:<c:out value="${p.brand}"/></p> <p>价格:<fmt:formatNumber value="${p.price}" type="currency"/></p> <p>库存:<c:out value="${p.stock}"/>台</p> <a href="product_edit.jsp?id=${p.id}">编辑</a> | <a href="product_delete.jsp?id=${p.id}" onclick="return confirm('确定删除?')">删除</a> </div> </c:forEach> <!-- 纯HTML添加按钮 --> <a href="product_add.jsp">添加新商品</a> </body> </html>参数与逻辑说明:
<%@ page import="..." %>:导入Java类,使脚本段可直接使用ProductDAO<c:forEach>:JSTL核心标签,items="${products}"绑定request作用域中的List,var="p"为迭代变量<c:out value="${p.brand}"/>:自动HTML转义,防止XSS攻击(如brand含<script>会被转为<script>)<fmt:formatNumber>:需在web.xml中声明<taglib>或添加<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>,实现货币格式化(¥1,999.00)
提示:若页面中文乱码,在
<%@ page %>指令中必须包含contentType="text/html;charset=UTF-8",且Tomcat的conf/server.xml中Connector需添加URIEncoding="UTF-8"属性。
3.3 库存扣减的事务控制:order_confirm.jsp里的手写事务边界
当管理员点击“确认发货”时,order_confirm.jsp需同时更新订单状态和商品库存。Access虽不支持标准SQL事务,但本项目通过Connection.setAutoCommit(false)模拟:
<% String orderId = request.getParameter("id"); Connection conn = null; PreparedStatement psOrder = null; PreparedStatement psProduct = null; try { // 1. 获取连接(复用conn.jsp逻辑) conn = DriverManager.getConnection(connStr); conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 2. 更新订单状态为"shipped" String sqlOrder = "UPDATE orders SET status='shipped' WHERE id=?"; psOrder = conn.prepareStatement(sqlOrder); psOrder.setString(1, orderId); psOrder.executeUpdate(); // 3. 查询该订单对应的商品ID和数量 String sqlQuery = "SELECT product_id, quantity FROM order_items WHERE order_id=?"; PreparedStatement psQuery = conn.prepareStatement(sqlQuery); psQuery.setString(1, orderId); ResultSet rs = psQuery.executeQuery(); if (rs.next()) { String productId = rs.getString("product_id"); int quantity = rs.getInt("quantity"); // 4. 扣减商品库存 String sqlStock = "UPDATE products SET stock=stock-? WHERE id=?"; psProduct = conn.prepareStatement(sqlStock); psProduct.setInt(1, quantity); psProduct.setString(2, productId); psProduct.executeUpdate(); } conn.commit(); // 所有操作成功,提交事务 out.println("<script>alert('发货成功!');window.location.href='order_list.jsp';</script>"); } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) {} // 回滚 } out.println("<script>alert('发货失败:" + e.getMessage() + "');</script>"); } finally { // 关闭资源(此处省略具体close逻辑,实际需判空) } %>关键设计点:
setAutoCommit(false)是事务起点,commit()是终点,中间任何一步异常都触发rollback()- Access的UPDATE语句不支持子查询,所以必须先
SELECT再UPDATE,增加了一次数据库交互 out.println("<script>...")是JSP中跳转的“土办法”,比response.sendRedirect()更可控(可带alert提示)
4. 常见问题排查:那些让你重启三次Tomcat的玄学错误
4.1 现象:首页打开空白,查看Tomcat日志显示java.lang.ClassNotFoundException: net.ucanaccess.jdbc.UcanaccessDriver
原因:/WEB-INF/lib/目录下缺少ucanaccess-5.0.1.jar,或存在多个版本(如ucanaccess-4.0.4.jar和5.0.1.jar共存)。UcanAccess 5.x要求Jackcess 3.x,而4.x用Jackcess 2.x,类路径冲突导致Driver类无法加载。
解决:
进入/WEB-INF/lib/,执行dir ucanaccess*(Windows)或ls ucanaccess*(Linux),只保留ucanaccess-5.0.1.jar,删除其他版本。然后清空tomcat/work/Catalina/localhost/下对应项目的临时文件夹,重启Tomcat。
4.2 现象:登录后跳转到product_list.jsp,但页面报错org.apache.jasper.JasperException: Unable to compile class for JSP
原因:
JSP编译失败,常见于/WEB-INF/classes/下缺少编译后的.class文件,或ProductDAO.class依赖的commons-dbutils-1.7.jar未在lib/中。本项目未提供.java源码,只给了编译好的.class,若误删classes/内容则无法恢复。
解决:
- 检查
/WEB-INF/lib/commons-dbutils-1.7.jar是否存在 - 若
classes/为空,从压缩包中重新提取/WEB-INF/classes/目录覆盖 - 强制JSP重新编译:删除
tomcat/work/Catalina/localhost/phone-sales/整个文件夹
4.3 现象:product_add.jsp提交后报错Data type mismatch in criteria expression
原因:
Access数据库字段类型与JSP传入参数类型不匹配。例如products.price字段为Number类型(Double),但表单提交的request.getParameter("price")是字符串,直接拼SQL会导致类型错误。本项目DAO层未做类型转换,直接ps.setDouble(1, Double.parseDouble(price)),若price为空字符串则抛NumberFormatException。
解决:
在ProductDAO.addProduct()方法中增加空值校验:
String priceStr = request.getParameter("price"); double price = 0.0; if (priceStr != null && !priceStr.trim().isEmpty()) { price = Double.parseDouble(priceStr.trim()); } ps.setDouble(3, price); // 第3个参数是price4.4 现象:sales_report.jsp生成的报表中,中文标题显示为方框(□□□)
原因:
Access报表引擎(Jet Engine)默认使用系统区域设置字体,Windows Server英文版无中文字体支持。JSP中<% response.setContentType("application/ms-excel"); %>导出Excel时,Access未嵌入字体信息。
解决:
- 在Windows服务器上安装微软雅黑字体(
msyh.ttc) - 修改
sales_report.jsp,在response.setContentType后添加:
<% response.setHeader("Content-Disposition", "attachment; filename=sales_report.xls"); response.setCharacterEncoding("UTF-8"); // 强制响应编码 %>- Excel打开时选择“UTF-8”编码(Excel 2016+支持)
4.5 现象:多用户同时操作时,库存扣减出现负数(超卖)
原因:
Access数据库锁机制弱,UPDATE products SET stock=stock-1 WHERE id=1001在并发场景下非原子操作。线程A读取stock=5,线程B也读取stock=5,A扣减后写入4,B扣减后也写入4,实际应为3。
解决:
在order_confirm.jsp的库存更新SQL前加悲观锁(Access支持SELECT ... FOR UPDATE语法):
SELECT stock FROM products WHERE id=? FOR UPDATE但需注意:UcanAccess 5.0.1对FOR UPDATE支持不稳定,更稳妥方案是改用乐观锁——在products表加version字段,更新时校验:
UPDATE products SET stock=stock-?, version=version+1 WHERE id=? AND version=?若executeUpdate()返回0行,则提示“库存已被其他管理员修改,请刷新后重试”。
5. 二次开发实战:给系统加上搜索框与导出Excel功能
5.1 在商品列表页增加模糊搜索:三步完成,不碰DAO层
需求:在product_list.jsp顶部添加搜索框,支持按名称或品牌模糊查询。
Step 1:修改JSP页面,添加搜索表单
<!-- product_list.jsp 开头插入 --> <form method="get" action="product_list.jsp"> <input type="text" name="keyword" value="${param.keyword}" placeholder="请输入商品名称或品牌" /> <select name="searchBy"> <option value="name" ${param.searchBy == 'name' ? 'selected' : ''}>商品名称</option> <option value="brand" ${param.searchBy == 'brand' ? 'selected' : ''}>品牌</option> </select> <button type="submit">搜索</button> <a href="product_list.jsp">重置</a> </form>Step 2:改造数据获取逻辑,支持条件查询
<% ProductDAO dao = new ProductDAO(); List<Product> productList; String keyword = request.getParameter("keyword"); String searchBy = request.getParameter("searchBy"); if (keyword != null && !keyword.trim().isEmpty()) { // 调用DAO新增的搜索方法(需手动添加) productList = dao.searchProducts(keyword.trim(), searchBy); } else { productList = dao.getAllProducts(); } request.setAttribute("products", productList); %>Step 3:扩展ProductDAO.java(需反编译或根据class推测)
// 在ProductDAO类中新增方法 public List<Product> searchProducts(String keyword, String field) { String sql = "SELECT * FROM products WHERE " + field + " LIKE ?"; List<Product> list = new ArrayList<>(); try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ResultSet rs = ps.executeQuery(); while (rs.next()) { Product p = new Product(); p.setId(rs.getString("id")); p.setName(rs.getString("name")); p.setBrand(rs.getString("brand")); p.setPrice(rs.getDouble("price")); p.setStock(rs.getInt("stock")); list.add(p); } } catch (SQLException e) { e.printStackTrace(); } return list; }注意:
field参数直接拼入SQL存在SQL注入风险,但教学项目中限定为name或brand两个白名单值,实际项目需用枚举校验。
5.2 导出Excel功能:用Apache POI替换JSP原生Excel导出
原report/sales_report.jsp用response.setContentType("application/ms-excel")生成HTML表格,样式简陋且兼容性差。升级为Apache POI生成真Excel:
Step 1:添加POI依赖下载poi-4.1.2.jar、poi-ooxml-4.1.2.jar、xmlbeans-3.1.0.jar,放入/WEB-INF/lib/
Step 2:新建ExportServlet.java
@WebServlet("/report/export") public class ExportServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String reportType = request.getParameter("type"); // "sales" or "inventory" List<Map<String, Object>> data = getReportData(reportType); // 从DAO获取数据 // 创建Excel工作簿 XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("报表"); // 写入表头 String[] headers = {"ID", "名称", "品牌", "价格", "库存"}; XSSFRow headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } // 写入数据 int rowNum = 1; for (Map<String, Object> row : data) { XSSFRow dataRow = sheet.createRow(rowNum++); dataRow.createCell(0).setCellValue((String) row.get("id")); dataRow.createCell(1).setCellValue((String) row.get("name")); dataRow.createCell(2).setCellValue((String) row.get("brand")); dataRow.createCell(3).setCellValue((Double) row.get("price")); dataRow.createCell(4).setCellValue((Integer) row.get("stock")); } // 设置响应头 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=" + reportType + "_report.xlsx"); // 写出流 workbook.write(response.getOutputStream()); workbook.close(); } private List<Map<String, Object>> getReportData(String type) { // 调用对应DAO方法,返回List<Map> return "sales".equals(type) ? new SalesReportDAO().getSalesData() : new InventoryReportDAO().getInventoryData(); } }Step 3:在report/sales_report.jsp中添加导出按钮
<a href="<%=request.getContextPath()%>/report/export?type=sales" class="btn btn-primary">导出销售报表(Excel)</a>5.3 改造登录验证:从明文密码到BCrypt加密(兼容旧数据)
当前密码明文存储,需升级为BCrypt。但不能让老用户全部重置密码,需兼容:
Step 1:修改UserDAO.findByUsernameAndPassword()
public User findByUsernameAndPassword(String username, String rawPassword) { String sql = "SELECT * FROM users WHERE username=?"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ResultSet rs = ps.executeQuery(); if (rs.next()) { String storedPassword = rs.getString("password"); // 兼容判断:若密码是60位(BCrypt哈希长度),则用BCrypt校验;否则用明文比对 if (storedPassword.length() == 60) { if (BCrypt.checkpw(rawPassword, storedPassword)) { return buildUserFromRS(rs); } } else { if (rawPassword.equals(storedPassword)) { // 旧密码匹配,立即升级为BCrypt upgradeToBCrypt(username, rawPassword); return buildUserFromRS(rs); } } } } catch (SQLException e) { e.printStackTrace(); } return null; } private void upgradeToBCrypt(String username, String rawPassword) { String hashed = BCrypt.hashpw(rawPassword, BCrypt.gensalt()); String sql = "UPDATE users SET password=? WHERE username=?"; try (Connection conn = getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, hashed); ps.setString(2, username); ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } }Step 2:在/WEB-INF/lib/中添加bcrypt-3.1.jar
从那以后我每次接手JSP老项目,都强制走一遍“三查”:查web.xml的Servlet映射是否与JSP表单action一致、查/WEB-INF/lib/的jar包版本是否与代码中import的类匹配、查conn.jsp里的数据库路径是否用getRealPath动态生成。这三点踩过坑的开发者都懂——不是技术多难,而是JSP的隐式依赖太容易在复制粘贴中丢失。希望帮到你。
本文还有配套的精品资源,点击获取