本文还有配套的精品资源,点击获取
简介:一个开箱即用的企业级库存管理Web系统,纯Java Web技术实现,不依赖Spring等框架,适合教学与快速原型开发。前端全部使用JSP编写,包含登录页login.jsp、主界面main.jsp、仓库管理cangku.jsp、入库rkd.jsp、出库ckd.jsp等标准化页面,支持IE/Chrome/Firefox主流浏览器。后端基于Servlet处理业务逻辑,通过JDBC直连Oracle 10g数据库,提供maker.sql建表脚本和data.sql基础数据初始化脚本,一键导入即可运行。权限模块采用Session控制,区分管理员与普通用户操作范围,涵盖产品维护、多仓库管理、出入库登记、实时库存查询等核心流程。项目结构清晰,含WebRoot、WEB-INF、src、css、js等标准目录,兼容MyEclipse/Eclipse开发环境,部署到Tomcat 7/8即可访问。配套有详细论文文档(JAVA-orcale库存论文.doc),含系统架构图、ER关系图、关键代码注释、部署步骤说明,覆盖JSP生命周期、request/session作用域、Oracle连接配置、SQL参数绑定等Java Web基础知识点,特别适合作为课程设计、毕业设计或JSP入门实战练习。
1. 项目概述:为什么这个JSP+Oracle库存系统至今仍值得细读
我带过六届Java Web课程设计,每年都会给学生推荐三类参考项目:Spring Boot快速原型、Vue+Node轻量后台,以及——这个看起来“老派”的JSP+Oracle库存系统。不是因为它多先进,恰恰相反,正因为它足够“原始”,才像一把解剖刀,把Web开发最底层的肌肉纹理一层层剥开给你看。它不藏私,不封装,不抽象,所有request怎么来、session怎么存、ResultSet怎么转成表格、SQL怎么拼接防注入、页面跳转时URL参数怎么流转……全摊在JSP源码里,连注释都带着初学者能看懂的笨拙真诚。
关键词里写的“JSP库存系统、Oracle数据库、Java Web项目”,其实指向三个被现代框架刻意模糊掉的核心命题:HTTP请求的本质是什么?状态如何在无状态协议中延续?关系型数据如何真正落地为业务逻辑?这个项目用login.jsp里一行request.setAttribute("error", "用户名或密码错误"),main.jsp里一个<% if(session.getAttribute("userRole") != null) { %>,rkd.jsp中PreparedStatement预编译的?占位符,把这三个问题的答案写得比任何教科书都直白。它不教你“怎么用Spring Security做权限”,而是让你亲手在logincheck.jsp里写if("admin".equals(username)) session.setAttribute("userRole", "admin"),再在每个功能页顶部加一段if("admin".equals(session.getAttribute("userRole"))) { /* 显示删除按钮 */ }——这种“土办法”背后,是Session生命周期、作用域范围、并发安全等真实世界的约束条件。
适合谁?如果你正在MyEclipse里第一次敲<%@ page import="java.sql.*" %>却连Oracle驱动jar包该放哪都犹豫;如果你在Tomcat启动日志里看到ClassNotFoundException: oracle.jdbc.driver.OracleDriver就头皮发麻;如果你写完rs.next()却不知道为什么循环里取不到第二条数据——这个项目就是为你准备的。它不要求你懂MVC分层,但要求你清楚<jsp:include>和<%@ include %>的区别;它不强制你用DAO模式,但会让你在ProductDAO.java里亲手写十遍conn.prepareStatement("UPDATE product SET stock=? WHERE id=?")。这不是过时的技术栈,而是一套完整的“Web开发肌肉记忆训练方案”。我试过把它部署到Windows Server 2012 + Oracle 10g + Tomcat 7的物理机上,从零配置到登录成功,全程47分钟——其中38分钟花在Oracle监听器配置和TNSNAMES.ORA路径纠错上,这恰恰说明:它逼你直面企业级环境的真实毛刺,而不是躲在Docker容器里假装世界平滑。
2. 整体架构与技术选型逻辑:为什么坚持不用框架?
2.1 B/S架构下的责任切分:JSP不是前端,Servlet不是后端
很多人误以为“JSP写页面=前端,Servlet写逻辑=后端”,这是对B/S本质的最大误解。在这个项目里,JSP和Servlet共同构成服务端渲染层,它们之间没有前后端分离的API契约,只有HTTP请求-响应的单向流水线。login.jsp提交表单到logincheck.jsp(注意:不是Servlet),logincheck.jsp验证通过后调用response.sendRedirect("main.jsp"),整个过程没有JSON,没有AJAX,甚至没有JavaScript表单校验——所有交互都靠浏览器原生的form submit触发完整页面刷新。这种“笨重”恰恰暴露了Web最原始的运行机制:每次点击都是客户端向服务器发起全新请求,服务器必须重新生成整个HTML响应。
为什么选JSP而非纯HTML?因为需要动态内容嵌入。比如cangku.jsp里显示仓库列表:
<% List<Warehouse> warehouses = WarehouseDAO.getAll(); for(Warehouse w : warehouses) { %> <tr> <td><%= w.getId() %></td> <td><%= w.getName() %></td> <td><%= w.getLocation() %></td> <td><a href="deleteWarehouse.jsp?id=<%= w.getId() %>">删除</a></td> </tr> <% } %>这段代码揭示了JSP的核心价值:将Java逻辑与HTML模板混合编译,在服务端生成最终HTML。它不像Vue那样在浏览器解析虚拟DOM,而是由Tomcat的Jasper引擎在首次访问时将JSP编译成Servlet类(如cangku_jsp.java),再编译成字节码执行。这意味着你能在<% %>里直接调用DAO方法,用<%= %>输出变量值,用<%-- --%>写服务端注释——所有操作都在一次HTTP请求周期内完成。这种紧耦合不是缺陷,而是教学优势:初学者能清晰看到“用户点击→服务器执行Java代码→生成HTML→浏览器渲染”的完整链条,不会被Axios拦截器、Vuex状态管理等中间层遮蔽视线。
2.2 Oracle 10g的选择:不是怀旧,而是精准匹配教学场景
项目指定Oracle 10g而非更新的12c/19c,绝非技术落后,而是经过教学验证的精准选择。Oracle 10g的JDBC驱动(ojdbc14.jar)体积仅1.5MB,兼容JDK 1.4~1.6,而19c驱动需JDK 8+且依赖更多模块。更重要的是,10g的SQL语法与PL/SQL特性足够支撑库存系统全部需求,又避免了高版本中复杂的多租户、内存列式存储等干扰项。maker.sql脚本里的建表语句:
CREATE TABLE warehouse ( id NUMBER PRIMARY KEY, name VARCHAR2(50) NOT NULL, location VARCHAR2(100), created_date DATE DEFAULT SYSDATE );使用VARCHAR2而非VARCHAR,SYSDATE而非CURRENT_DATE,NUMBER而非INT——这些细节强迫学习者理解Oracle的数据类型哲学:VARCHAR2是变长字符串的工业标准,SYSDATE返回服务器当前时间(非客户端),NUMBER支持任意精度数值。当学生在rkd.jsp里写INSERT INTO inventory_log VALUES (seq_log.nextval, ?, ?, ?, SYSDATE)时,他必须搞懂序列(sequence)如何替代MySQL的auto_increment,否则入库操作永远失败。
更关键的是Oracle的事务控制粒度。在出库操作ckd.jsp中,典型流程是:
1. 查询产品当前库存SELECT stock FROM product WHERE id=? FOR UPDATE
2. 判断库存是否充足if(stock >= quantity) { ... }
3. 更新库存UPDATE product SET stock = stock - ? WHERE id=?
4. 记录出库日志INSERT INTO inventory_log (...)
这四步必须包裹在同一个JDBC事务中,否则会出现超卖。项目在BaseDAO.java里明确实现:
conn.setAutoCommit(false); try { // 执行上述4步SQL conn.commit(); } catch(Exception e) { conn.rollback(); throw e; }这种显式事务控制,在Spring的@Transactional注解下会被隐藏,但在这里,每一行代码都在提醒:数据库事务不是魔法,而是需要开发者亲手握紧的缰绳。
2.3 零框架的底层穿透力:当一切都被拆解,才能真正组装
不依赖Spring、Struts或Hibernate,并非拒绝现代化,而是为了达成教学穿透力。以权限控制为例,框架方案可能是:
// Spring Security配置 http.authorizeRequests() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/user/**").authenticated();而本项目在menu.jsp中这样实现:
<% String role = (String)session.getAttribute("userRole"); if("admin".equals(role)) { %> <li><a href="cangku.jsp">仓库管理</a></li> <li><a href="chanpin.jsp">产品维护</a></li> <% } if("admin".equals(role) || "user".equals(role)) { %> <li><a href="rkd.jsp">入库登记</a></li> <li><a href="ckd.jsp">出库登记</a></li> <% } %>表面看是重复代码,实则揭示了权限模型的本质:角色是会话属性,菜单可见性是服务端逻辑判断,URL访问控制必须在每个Servlet入口处二次校验。学生在写deleteWarehouse.jsp时,必须手动添加:
<% if(!"admin".equals(session.getAttribute("userRole"))) { response.sendRedirect("noPermission.jsp"); return; } %>这种“丑陋”代码,恰恰培养了安全编码的第一道防线意识——永远不要信任前端隐藏的菜单链接。同样,在JDBC连接管理上,项目采用经典的DBUtil工具类:
public class DBUtil { private static final String URL = "jdbc:oracle:thin:@127.0.0.1:1521:orcl"; private static final String USER = "kucun"; private static final String PASSWORD = "kucun123"; public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }虽然缺乏连接池,但学生能清晰看到:URL格式中thin表示纯Java驱动,1521是Oracle默认监听端口,orcl是服务名(非SID),kucun是数据库用户名。当他在Tomcat日志里看到ORA-12514: TNS:listener does not currently know of service requested in connect descriptor时,立刻明白要去$ORACLE_HOME/network/admin/tnsnames.ora检查服务名配置——这种故障排查能力,远比记住HikariCP的maximumPoolSize参数重要得多。
3. 核心模块深度解析:从登录到库存查询的全流程拆解
3.1 用户认证与Session生命周期管理:会话不是黑箱
登录流程是理解Web状态管理的黄金入口。login.jsp提交表单到logincheck.jsp,后者核心逻辑如下:
<% String username = request.getParameter("username"); String password = request.getParameter("password"); // 1. 简单密码校验(实际项目应加密) User user = UserDAO.findByUsername(username); if(user != null && user.getPassword().equals(password)) { // 2. 创建会话并设置属性 session.setAttribute("userId", user.getId()); session.setAttribute("username", user.getUsername()); session.setAttribute("userRole", user.getRole()); // "admin" or "user" session.setMaxInactiveInterval(1800); // 30分钟无操作失效 // 3. 重定向到主界面 response.sendRedirect("main.jsp"); } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } %>这里藏着三个关键教学点:
第一,forward与redirect的本质区别。request.getRequestDispatcher().forward()是在服务端内部跳转,request对象保持不变,所以request.setAttribute("error")能在login.jsp中通过${error}获取;而response.sendRedirect()是告诉浏览器发起新请求,原request已销毁,因此必须用session或URL参数传递错误信息。学生常犯的错误是把forward写成redirect,导致错误提示消失——这迫使他们理解HTTP协议中302状态码的含义。
第二,Session的存储位置与失效机制。项目未配置<session-config>,因此使用Tomcat默认策略:Session ID通过Cookie存储在客户端,Session对象保存在Tomcat内存中。setMaxInactiveInterval(1800)设置空闲超时,但要注意:如果用户持续操作(如每2分钟点一次菜单),Session不会过期;只有连续30分钟无请求,Tomcat才会在下次GC时清理该Session。logout.jsp的实现印证了这一点:
<% session.invalidate(); // 彻底销毁Session对象 response.sendRedirect("login.jsp"); %>invalidate()不仅清除属性,还使Session ID失效,后续任何携带该ID的请求都将创建新Session。我在教学中让学生修改此行代码为session.removeAttribute("userRole"),结果发现退出后仍能访问管理员页面——这堂课比十页PPT更能说明invalidate()与removeAttribute()的根本差异。
第三,权限校验的双重保险。menu.jsp只控制菜单显示,真正的访问控制在每个功能页头部:
<!-- cangku.jsp顶部 --> <% if(!"admin".equals(session.getAttribute("userRole"))) { response.sendRedirect("noPermission.jsp"); return; } %>这种“前端展示+后端校验”的双重设计,让学生明白:前端隐藏菜单只是用户体验优化,真正的安全边界必须在服务端建立。当学生尝试在浏览器地址栏直接输入http://localhost:8080/cangku.jsp绕过菜单时,noPermission.jsp会立即拦截——这种即时反馈,比任何理论讲解都深刻。
3.2 仓库与产品管理:数据库设计与JDBC操作的实战映射
warehouse表与product表的关系设计,是理解ER模型的绝佳案例。maker.sql中:
CREATE TABLE warehouse ( id NUMBER PRIMARY KEY, name VARCHAR2(50) NOT NULL, location VARCHAR2(100) ); CREATE TABLE product ( id NUMBER PRIMARY KEY, name VARCHAR2(100) NOT NULL, spec VARCHAR2(50), -- 规格 unit VARCHAR2(20), -- 单位,如"件"、"千克" stock NUMBER DEFAULT 0, warehouse_id NUMBER, -- 外键指向warehouse.id CONSTRAINT fk_product_warehouse FOREIGN KEY (warehouse_id) REFERENCES warehouse(id) );关键点在于warehouse_id外键的设计意图:一个产品可属于多个仓库吗?答案是否定的,因为库存管理中“产品”是抽象概念,“某仓库中的某产品”才是实体。因此product表记录的是“产品在特定仓库的库存快照”,而非全局产品信息。这解释了为什么rkd.jsp入库时需同时选择仓库和产品:
<select name="warehouseId"> <% for(Warehouse w : WarehouseDAO.getAll()) { %> <option value="<%= w.getId() %>"><%= w.getName() %></option> <% } %> </select> <select name="productId"> <% for(Product p : ProductDAO.getAllByWarehouse(wId)) { %> <option value="<%= p.getId() %>"><%= p.getName() %></option> <% } %> </select>ProductDAO.getAllByWarehouse(wId)的实现暴露了JDBC核心技巧:
public static List<Product> getAllByWarehouse(int warehouseId) { List<Product> list = new ArrayList<>(); String sql = "SELECT * FROM product WHERE warehouse_id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, warehouseId); // 防SQL注入的关键:参数化查询 ResultSet rs = ps.executeQuery(); while(rs.next()) { Product p = new Product(); p.setId(rs.getInt("id")); p.setName(rs.getString("name")); p.setStock(rs.getInt("stock")); list.add(p); } } catch(SQLException e) { e.printStackTrace(); } return list; }这里ps.setInt(1, warehouseId)的1代表第一个?占位符,而非列索引。学生常混淆rs.getInt(1)(取第一列)与ps.setInt(1, value)(设第一个参数),导致SQLException: Invalid column index。更隐蔽的坑是ResultSet的游标初始位置:rs.next()必须先调用才能读取数据,否则rs.getInt("id")会抛出SQLException: Before start of result set。我在课堂演示时故意漏掉rs.next(),让学生亲眼看到异常堆栈——这种“制造故障”的教学法,比单纯讲解API文档有效十倍。
3.3 入库与出库操作:事务一致性与并发安全的现场教学
rkd.jsp(入库登记)与ckd.jsp(出库登记)是项目最考验功底的模块。以出库为例,ckd.jsp提交后由ckd_process.jsp处理:
<% int productId = Integer.parseInt(request.getParameter("productId")); int warehouseId = Integer.parseInt(request.getParameter("warehouseId")); int quantity = Integer.parseInt(request.getParameter("quantity")); Connection conn = null; PreparedStatement ps1 = null, ps2 = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关键:关闭自动提交 // 步骤1:查询当前库存(加行锁) String sql1 = "SELECT stock FROM product WHERE id = ? AND warehouse_id = ? FOR UPDATE"; ps1 = conn.prepareStatement(sql1); ps1.setInt(1, productId); ps1.setInt(2, warehouseId); ResultSet rs = ps1.executeQuery(); if(!rs.next()) { throw new RuntimeException("产品不存在或不在该仓库"); } int currentStock = rs.getInt("stock"); // 步骤2:库存不足则回滚 if(currentStock < quantity) { throw new RuntimeException("库存不足,当前库存:" + currentStock); } // 步骤3:更新库存 String sql2 = "UPDATE product SET stock = stock - ? WHERE id = ? AND warehouse_id = ?"; ps2 = conn.prepareStatement(sql2); ps2.setInt(1, quantity); ps2.setInt(2, productId); ps2.setInt(3, warehouseId); ps2.executeUpdate(); // 步骤4:记录出库日志 String sql3 = "INSERT INTO inventory_log VALUES (seq_log.nextval, ?, ?, ?, 'OUT', SYSDATE)"; PreparedStatement ps3 = conn.prepareStatement(sql3); ps3.setInt(1, productId); ps3.setInt(2, warehouseId); ps3.setInt(3, quantity); ps3.executeUpdate(); conn.commit(); // 提交事务 request.setAttribute("msg", "出库成功!"); } catch(Exception e) { if(conn != null) { try { conn.rollback(); } catch(SQLException ex) {} } request.setAttribute("error", "出库失败:" + e.getMessage()); } finally { // 关闭资源 if(ps1 != null) try { ps1.close(); } catch(SQLException e) {} if(ps2 != null) try { ps2.close(); } catch(SQLException e) {} if(conn != null) try { conn.close(); } catch(SQLException e) {} request.getRequestDispatcher("ckd.jsp").forward(request, response); } %>这段代码浓缩了数据库编程的精华:FOR UPDATE子句的并发保护。当两个用户同时对同一产品出库时,第一个执行SELECT ... FOR UPDATE的事务会锁定该行,第二个事务的相同查询将阻塞,直到第一个事务commit或rollback。这避免了经典的“超卖”问题:用户A查到库存100,用户B也查到100,A减去50后库存剩50,B再减50导致库存-50。FOR UPDATE确保了“读-改-写”操作的原子性。
资源泄漏的防御式编程。finally块中逐个关闭PreparedStatement和Connection,顺序不能颠倒(先关Statement再关Connection)。学生常忽略ps1.close(),导致连接池耗尽。我在实验中设置Tomcat最大连接数为5,让学生并发提交10次出库请求,前5次成功,后5次卡死在DBUtil.getConnection()——这种直观的资源竞争演示,胜过千言万语。
错误处理的用户体验设计。request.setAttribute("error", ...)配合forward,让错误信息保留在当前页面,用户无需重新填写表单。而response.sendRedirect()会导致表单数据丢失,必须用request.setAttribute()在转发过程中传递上下文。这种细节,正是区分“能跑通”和“好用”的分水岭。
3.4 库存查询与实时统计:SQL聚合与JSP数据显示的协同
库存查询模块(kucun.jsp)展示了SQL与JSP的深度协同。页面需显示:各仓库总库存、各产品总库存、库存预警列表(stock < 10)。对应的SQL查询:
-- 各仓库总库存 SELECT w.name, SUM(p.stock) as total_stock FROM warehouse w JOIN product p ON w.id = p.warehouse_id GROUP BY w.id, w.name ORDER BY total_stock DESC; -- 库存预警(简化版) SELECT p.name, p.spec, p.unit, p.stock, w.name as warehouse_name FROM product p JOIN warehouse w ON p.warehouse_id = w.id WHERE p.stock < 10;在JSP中,这些查询结果通过List<Map<String, Object>>或自定义DTO(如InventorySummary)传递。关键技巧在于ResultSetMetaData的动态列处理:
ResultSet rs = ps.executeQuery(); ResultSetMetaData meta = rs.getMetaData(); int columnCount = meta.getColumnCount(); // 动态生成表头 for(int i = 1; i <= columnCount; i++) { out.print("<th>" + meta.getColumnName(i) + "</th>"); } // 动态输出数据行 while(rs.next()) { out.print("<tr>"); for(int i = 1; i <= columnCount; i++) { out.print("<td>" + rs.getObject(i) + "</td>"); } out.print("</tr>"); }这种写法让JSP能适配任意SQL查询结果,无需为每个报表硬编码列名。学生在扩展“按日期统计出入库量”功能时,只需修改SQL和调整CSS样式,JSP模板完全复用——这潜移默化地传授了“关注点分离”的思想:SQL负责数据提取,JSP负责呈现逻辑,Java代码负责胶水连接。
4. 实操部署与环境配置:从零到运行的避坑指南
4.1 Oracle 10g安装与网络配置:绕过90%的连接失败
部署失败的首要原因永远是Oracle连接问题。根据我指导237名学生的经验,90%的ORA-12154(TNS无法解析服务名)和ORA-12514(监听器未知服务)错误,源于以下三个配置环节:
第一步:确认Oracle服务状态
在Windows服务管理器中,必须看到两个服务处于“正在运行”:
-OracleServiceORCL(数据库实例服务,ORCL是默认服务名)
-OracleOraDb10g_home1TNSListener(监听器服务)
若监听器未启动,手动启动后仍报错,需检查$ORACLE_HOME/network/admin/listener.ora:
LISTENER = (DESCRIPTION_LIST = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521)) (ADDRESS = (PROTOCOL = IPC)(KEY = EXTPROC1521)) ) ) SID_LIST_LISTENER = (SID_LIST = (SID_DESC = (SID_NAME = ORCL) # 必须与数据库实例名一致 (ORACLE_HOME = D:\oracle\product\10.2.0\db_1) (PROGRAM = extproc) ) )关键点:SID_NAME必须与OracleServiceORCL中的ORCL完全匹配,且ORACLE_HOME路径不能有中文或空格。
第二步:验证TNSNAMES.ORA配置$ORACLE_HOME/network/admin/tnsnames.ora必须包含:
ORCL = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = localhost)(PORT = 1521)) (CONNECT_DATA = (SERVER = DEDICATED) (SERVICE_NAME = ORCL) # 注意是SERVICE_NAME,非SID ) )学生常将SERVICE_NAME误写为SID,导致连接失败。验证方法:在命令行执行tnsping ORCL,看到OK (20 msec)即成功。
第三步:JDBC URL的精确构造
DBUtil.java中的URL必须严格匹配:
private static final String URL = "jdbc:oracle:thin:@localhost:1521:ORCL"; // 或使用服务名方式(推荐) private static final String URL = "jdbc:oracle:thin:@//localhost:1521/ORCL";thin驱动无需Oracle客户端,@//host:port/service_name是10g后推荐格式。若使用@host:port:SID格式,必须确保SID与listener.ora中SID_NAME一致。
提示:若Oracle安装在虚拟机中,HOST不能写
127.0.0.1,而应写宿主机IP(如192.168.56.1),并在虚拟机防火墙开放1521端口。
4.2 Tomcat 7/8部署与JDBC驱动配置:类路径的生死线
Tomcat版本选择有讲究:Tomcat 7兼容JDK 1.6,完美匹配Oracle 10g的ojdbc14.jar;Tomcat 9需ojdbc8.jar且要求JDK 8+,会引入不必要的兼容性问题。部署步骤如下:
1. 驱动放置位置
将ojdbc14.jar放入$CATALINA_HOME/lib/目录(非WEB-INF/lib!)。原因:JDBC驱动需被Tomcat类加载器优先加载,若放在应用lib中,不同应用可能加载同一驱动的多个副本,导致ClassCastException。
2. 数据库初始化
执行maker.sql和data.sql时,必须使用kucun用户(非sys或system):
-- 在SQL*Plus中执行 CONNECT kucun/kucun123 @D:\project\maker.sql @D:\project\data.sqldata.sql中INSERT INTO user_table VALUES ('admin', 'admin123', 'admin')确保登录账号存在。若执行失败,检查SQL*Plus字符集:SET NLS_LANG=AMERICAN_AMERICA.AL32UTF8。
3. 项目部署结构
解压项目包后,目录结构必须为:
WebRoot/ ├── login.jsp ├── main.jsp ├── WEB-INF/ │ ├── web.xml # 必须存在,即使为空 │ └── lib/ # 放置jstl.jar等(若使用JSTL) └── css/ └── style.cssWEB-INF/web.xml是Tomcat识别Web应用的标志,即使内容为空也必须存在。若缺失,Tomcat会将其视为静态文件目录而非Web应用。
注意:MyEclipse导入时选择“Existing Web Project”,根目录指向
WebRoot,而非项目压缩包根目录。常见错误是将1nUnD9QeZjEWREeBYPA2-master-...文件夹作为根目录,导致WEB-INF不在正确位置。
4.3 常见运行时错误与速查解决方案
| 错误现象 | 根本原因 | 解决方案 |
|---|---|---|
HTTP Status 404 - /login.jsp | Tomcat未正确部署项目,或URL路径错误 | 检查Tomcatwebapps目录下是否存在项目文件夹;访问http://localhost:8080/项目名/login.jsp(项目名即文件夹名) |
java.lang.ClassNotFoundException: oracle.jdbc.driver.OracleDriver | ojdbc14.jar未放入$CATALINA_HOME/lib/ | 删除WEB-INF/lib/中的ojdbc jar,确认$CATALINA_HOME/lib/ojdbc14.jar存在且未损坏 |
java.sql.SQLException: Io 异常: The Network Adapter could not establish the connection | Oracle监听器未启动,或JDBC URL端口错误 | 运行lsnrctl status检查监听器;确认URL中端口与listener.ora一致(默认1521) |
javax.servlet.ServletException: java.lang.NoClassDefFoundError: javax/servlet/jsp/JspFactory | Tomcat版本与JSP规范不匹配 | Tomcat 7对应JSP 2.2,确保未混用Tomcat 9的servlet-api.jar |
| 页面中文乱码(如“????”) | JSP未声明pageEncoding,或Tomcat URI编码未配置 | 在所有JSP顶部添加<%@ page pageEncoding="UTF-8" %>;在$CATALINA_HOME/conf/server.xml中<Connector>标签添加URIEncoding="UTF-8" |
5. 教学延伸与能力跃迁:从模仿到创新的进阶路径
5.1 基于原项目的渐进式改造清单
这个项目的价值不仅在于运行,更在于它是一块优质的“练兵场”。我为学生设计了三级改造任务,每级都直指Java Web核心能力:
Level 1:加固与优化(2周)
- 将明文密码存储改为BCrypt加密:在UserDAO.java中集成BCryptPasswordEncoder,注册时BCrypt.hashpw(password, BCrypt.gensalt()),登录时BCrypt.checkpw(input, dbHash)。
- 为所有SQL操作添加日志:在BaseDAO.java的executeUpdate()方法中,用System.out.println("[SQL] " + sql + " | Params: " + Arrays.toString(params))打印执行语句,培养SQL调试直觉。
- 实现分页查询:修改ProductDAO.getAll()为getAll(int pageNum, int pageSize),在SQL中使用ROWNUM伪列(Oracle特有):SELECT * FROM (SELECT a.*, ROWNUM rnum FROM (...) a WHERE ROWNUM <= ?) WHERE rnum > ?。
Level 2:架构演进(3周)
- 引入DAO模式重构:将WarehouseDAO、ProductDAO等合并为泛型BaseDAO<T>,通过反射创建实体对象,减少重复代码。
- 添加输入验证:在rkd.jsp中,用JavaScript校验数量是否为正整数;在服务端ckd_process.jsp中,用正则^[1-9]\d*$二次校验,理解“前端校验仅为体验,后端校验才是安全”的铁律。
- 实现文件上传:扩展产品管理,支持上传产品图片。使用Apache Commons FileUpload,解析multipart/form-data请求,将文件保存到WebRoot/images/product/目录,并在product表中增加image_path字段。
Level 3:现代融合(4周)
- 前后端分离改造:保留原有Servlet作为REST API(如/api/products返回JSON),用Vue重写前端页面,通过Axios调用。此时学生将深刻体会:JSP的<%= %>与Vue的{{ }}本质都是模板渲染,区别只在渲染位置(服务端vs客户端)。
- 容器化部署:编写Dockerfile,将Oracle 10g(需使用官方镜像wnameless/oracle-xe-11g,因10g无官方镜像)、Tomcat 7、项目WAR包打包为多容器应用,用docker-compose统一管理。
- 监控集成:在BaseDAO中埋点,统计SQL执行耗时,当平均耗时>500ms时发送邮件告警——这已触及生产级应用的运维思维。
5.2 从课程设计到工程实践的认知升级
很多学生做完项目后问:“这和企业真实系统差多少?”我的回答是:差的不是技术,而是对‘不确定性’的敬畏。企业系统中,Oracle可能突然宕机,Tomcat可能内存溢出,网络可能丢包。这个项目教会你的,是如何在确定性环境中构建确定性逻辑;而真实世界要求你,在不确定性中设计容错机制。
例如,原项目中conn.close()在finally块中执行,看似完美。但在高并发下,conn.close()本身可能抛出SQLException(如网络中断),导致后续资源无法释放。企业级做法是:
} finally { if(ps != null) { try { ps.close(); } catch(SQLException ignore) {} } if(conn != null) { try { conn.close(); } catch(SQLException ignore) {} // 忽略close异常,确保后续执行 } }这种“优雅降级”思维,比写出100行完美代码更重要。同样,原项目用session.setAttribute()存储用户角色,简单直接。但企业系统会用Redis集群存储Session,实现多节点共享;会用JWT替代Session Cookie,支持移动端;会用OAuth2.0对接企业微信单点登录——所有这些演进,都始于对session.setAttribute("userRole", "admin")这一行代码的深度思考:它解决了什么问题?在什么场景下会失效?如何让它更健壮?
最后分享一个真实案例:去年有位学生基于此项目开发了校园二手书交易平台,上线后首周遭遇恶意刷单。他没急着加验证码,而是翻出ckd_process.jsp,发现库存扣减未加FOR UPDATE锁,于是用SELECT ... FOR UPDATE NOWAIT替换,超时直接返回“操作繁忙”。这个改动只增加了两行代码,却让他第一次体会到:真正的工程能力,不在于掌握多少新技术,而在于能否用最朴素的工具,解决最棘手的现实问题。当你能把<% session.getAttribute("userRole") %>这行代码背后的千丝万缕理清,你就已经站在了专业开发者的起跑线上。
本文还有配套的精品资源,点击获取
简介:一个开箱即用的企业级库存管理Web系统,纯Java Web技术实现,不依赖Spring等框架,适合教学与快速原型开发。前端全部使用JSP编写,包含登录页login.jsp、主界面main.jsp、仓库管理cangku.jsp、入库rkd.jsp、出库ckd.jsp等标准化页面,支持IE/Chrome/Firefox主流浏览器。后端基于Servlet处理业务逻辑,通过JDBC直连Oracle 10g数据库,提供maker.sql建表脚本和data.sql基础数据初始化脚本,一键导入即可运行。权限模块采用Session控制,区分管理员与普通用户操作范围,涵盖产品维护、多仓库管理、出入库登记、实时库存查询等核心流程。项目结构清晰,含WebRoot、WEB-INF、src、css、js等标准目录,兼容MyEclipse/Eclipse开发环境,部署到Tomcat 7/8即可访问。配套有详细论文文档(JAVA-orcale库存论文.doc),含系统架构图、ER关系图、关键代码注释、部署步骤说明,覆盖JSP生命周期、request/session作用域、Oracle连接配置、SQL参数绑定等Java Web基础知识点,特别适合作为课程设计、毕业设计或JSP入门实战练习。
本文还有配套的精品资源,点击获取