简介:这份资源是基于JavaEE与Oracle构建的仓库管理系统完整项目包,面向计算机相关专业的毕业设计、课程设计及工程实训人群,也适合希望从零掌握Web开发全流程的进阶学习者。系统围绕仓库出入库业务展开,涵盖入库新商品与已有商品、出库操作、库存商品查看、用户注册以及个人信息管理等核心模块,结构清晰,便于理解企业级应用的典型分层设计。压缩包共191个文件,约60.22MB,包含31个java源文件与对应class文件、24个jsp页面、12个xml配置、5个jar依赖,以及gif、png、jpg等界面素材和css、js等前端资源,另附数据库sql脚本、论文文档与mp4演示视频,形成从代码到文档的完整闭环。目前已有173人学习下载。读者可借此获得可直接运行的源码工程、Oracle建库脚本、配套论文与操作视频,既能作为毕设参考模板,也能用于拆解Servlet与JSP协作机制、数据库连接配置及前后端交互逻辑,快速积累项目实战经验。
1. 从一份能跑起来的仓库管理系统源码说起:JavaEE+Oracle 到底解决了什么问题
很多做课程设计或者接私活的同学,拿到「基于JavaEE+Oracle实现的仓库管理系统」这个题目时,第一反应是去搜一套能直接跑的源码,把论文和视频一起打包交差。但真正做过企业级 WMS 的人知道,仓库管理系统的核心难点从来不是界面好不好看,而是库存数量在并发出入库时能不能对得上、Oracle 的序列和触发器怎么配合 JavaEE 的事务边界、以及数据库 SQL 在数据量涨到几十万行之后还能不能撑住。这套技术栈选型放在今天看不算新潮,但它在中小型仓储场景里依然是稳的:JavaEE 提供容器级事务和连接池,Oracle 提供强一致性和成熟的存储过程能力,两者配合能把「入库扣减、出库锁定、盘点调账」这条链路做扎实。这篇文章面向的是需要交付一套完整可运行系统的开发者,也面向想把 JavaEE+Oracle 这套组合真正用对的人。我会从环境搭建讲到核心表结构设计,再到事务与锁的落地细节,最后给出几个只有踩过才知道的排查技巧。你不需要先看完论文才能动手,跟着章节走就能把系统跑起来。
2. JavaEE+Oracle 仓库管理系统的环境搭建与工程结构
2.1 JDK、Tomcat、Oracle 三件套的版本匹配
这套系统最常见的翻车点不是代码写错,而是版本对不上。JavaEE 项目通常跑在 Tomcat 上,而 Tomcat 的版本又和 JDK 版本强绑定。我一般会选 JDK 8 或 JDK 11 这两个长期支持版本,Tomcat 用 8.5 或 9.0,Oracle 用 11g 或 19c 单实例。注意 Oracle 19c 之后 JDBC 驱动分成了 ojdbc8 和 ojdbc10,JDK 8 配 ojdbc8,JDK 11 以上可以配 ojdbc10,选错了会在启动时报NoClassDefFoundError或者连接直接超时。
下面是我常用的环境检查命令,先确认本机 Java 和数据库客户端都能通:
# 检查 JDK 版本,必须是 1.8 或 11 java -version # 检查 Oracle 监听是否正常,1521 是默认端口 lsnrctl status # 用 sqlplus 登录,确认账号密码和 service name 正确 sqlplus warehouse/warehouse123@//localhost:1521/ORCLPDB1逻辑说明:java -version确认 JDK 大版本,避免用 JDK 17 去跑老 Tomcat 8.5 导致反射报错。lsnrctl status看监听器是否 READY,如果实例没注册上去,后面 JDBC 一定连不上。sqlplus这一步是排除网络和认证问题的最快方式,能登进去说明数据库侧没问题,连不上就先去查tnsnames.ora和listener.ora。
参数说明:warehouse/warehouse123是业务账号,不要用 sys 或 system 直接跑应用,权限太大容易出安全事故。ORCLPDB1是 19c 的 PDB 服务名,11g 里换成ORCL即可。端口如果不是 1521,在 JDBC URL 里同步改。
2.2 用 Maven 搭出可部署的 JavaEE 工程骨架
JavaEE 项目现在很少手写web.xml了,但仓库管理系统这种课程设计类项目,用 Maven 的 war 打包方式最省事。下面是我常用的pom.xml关键片段,只保留和 Oracle、Servlet、连接池相关的依赖:
<dependencies> <!-- Servlet API,scope 必须是 provided,否则和 Tomcat 冲突 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- Oracle JDBC 驱动,JDK 8 用 ojdbc8 --> <dependency> <groupId>com.oracle.database.jdbc</groupId> <artifactId>ojdbc8</artifactId> <version>19.3.0.0</version> </dependency> <!-- HikariCP 连接池,比 DBCP 快且稳定 --> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>4.0.3</version> </dependency> <!-- JSTL 用于页面标签,仓库列表页会用到 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>逻辑说明:Servlet API 的 scope 写成 provided 是关键,Tomcat 自带 servlet 容器,如果打进 war 包会出现类加载冲突,表现为启动时报ClassCastException或者过滤器不生效。ojdbc8 的 groupId 在 19c 之后变了,老教程里写的com.oracle:ojdbc6已经过时,用新坐标能避免手动下载 jar 包。HikariCP 的配置后面在context.xml或 Java 配置类里写,比传统 DBCP 少很多连接泄漏问题。
参数说明:ojdbc8的版本号 19.3.0.0 对应 Oracle 19c,如果数据库是 11g,降到 12.2.0.1 也能用。HikariCP 4.x 要求 JDK 8 以上,JDK 11 可以用 5.x。JSTL 1.2 是 javax 命名空间,如果以后迁到 Jakarta EE 要换成jakarta.servlet.jsp.jstl。
2.3 连接池与数据源在 Tomcat 里的配置方式
连接池不配好,系统跑一会儿就报Connection reset或者Too many connections。我一般把数据源配在 Tomcat 的context.xml里,让容器管理连接生命周期,应用代码只负责取用。
<Context> <Resource name="jdbc/warehouseDB" auth="Container" type="javax.sql.DataSource" driverClassName="oracle.jdbc.OracleDriver" url="jdbc:oracle:thin:@//localhost:1521/ORCLPDB1" username="warehouse" password="warehouse123" maxTotal="20" maxIdle="10" minIdle="5" maxWaitMillis="10000" validationQuery="SELECT 1 FROM DUAL" testOnBorrow="true" /> </Context>逻辑说明:validationQuery用SELECT 1 FROM DUAL是 Oracle 的标准探活语句,比SELECT 1更稳妥,因为 Oracle 不允许无 FROM 的查询。testOnBorrow开启后每次取连接都会校验,会稍微增加开销,但能避免拿到已经被数据库断开的死连接。maxWaitMillis设 10 秒,超过就抛异常,防止请求线程无限等待拖垮 Tomcat。
参数说明:maxTotal设 20 是中小型系统的经验值,仓库管理系统并发不会特别高,设太大反而增加 Oracle 的会话压力。minIdle保持 5 个热连接,避免每次请求都新建物理连接。如果 Oracle 是 RAC 或者有防火墙空闲断连策略,把validationQuery和testWhileIdle一起配上。
3. 仓库管理系统核心表结构与 Oracle SQL 落地
3.1 物料、库存、出入库单三张主表的字段设计
仓库管理系统的表设计绕不开三个核心实体:物料主数据、库存余额、出入库流水。很多课程设计源码把这三者混在一张表里,导致后期查库存要全表扫描,盘点对不上账。我一般拆成t_material、t_stock、t_stock_record三张表,用外键和唯一索引保证一致性。
-- 物料主数据表 CREATE TABLE t_material ( material_id NUMBER(10) PRIMARY KEY, material_code VARCHAR2(32) NOT NULL, material_name VARCHAR2(128) NOT NULL, unit VARCHAR2(16), create_time DATE DEFAULT SYSDATE, CONSTRAINT uk_material_code UNIQUE (material_code) ); -- 库存余额表,一个物料一条记录 CREATE TABLE t_stock ( stock_id NUMBER(10) PRIMARY KEY, material_id NUMBER(10) NOT NULL, quantity NUMBER(12,2) DEFAULT 0 NOT NULL, update_time DATE DEFAULT SYSDATE, CONSTRAINT fk_stock_material FOREIGN KEY (material_id) REFERENCES t_material (material_id), CONSTRAINT uk_stock_material UNIQUE (material_id) ); -- 出入库流水表,只追加不修改 CREATE TABLE t_stock_record ( record_id NUMBER(10) PRIMARY KEY, material_id NUMBER(10) NOT NULL, record_type CHAR(1) NOT NULL, -- I 入库,O 出库 quantity NUMBER(12,2) NOT NULL, operator VARCHAR2(32), record_time DATE DEFAULT SYSDATE, CONSTRAINT fk_record_material FOREIGN KEY (material_id) REFERENCES t_material (material_id) );逻辑说明:t_stock上对material_id建唯一约束,保证一个物料只有一条库存记录,避免并发插入产生重复行。t_stock_record只追加不更新,所有出入库都往这里写流水,库存余额通过触发器或应用层事务同步更新。record_type用 CHAR(1) 存 I/O,比存中文「入库/出库」省空间且索引效率高。
参数说明:NUMBER(12,2)支持小数点后两位,适合按重量或体积计量的物料。如果物料数量必须是整数,改成NUMBER(12)。material_code加唯一索引,防止重复录入同一物料。create_time和record_time默认SYSDATE,应用层不传时间也能自动记录。
3.2 用序列和触发器生成主键的两种做法
Oracle 没有 MySQL 的AUTO_INCREMENT,主键生成要么用序列加触发器,要么在应用层用sequence.nextval取号。我一般推荐应用层取号,因为触发器在批量插入时会有性能损耗,而且调试时不容易看到主键是怎么来的。
-- 创建序列,每个表一个 CREATE SEQUENCE seq_material START WITH 1 INCREMENT BY 1 NOCACHE; CREATE SEQUENCE seq_stock START WITH 1 INCREMENT BY 1 NOCACHE; CREATE SEQUENCE seq_record START WITH 1 INCREMENT BY 1 NOCACHE; -- 如果一定要用触发器,写法如下 CREATE OR REPLACE TRIGGER trg_material_id BEFORE INSERT ON t_material FOR EACH ROW BEGIN IF :NEW.material_id IS NULL THEN SELECT seq_material.NEXTVAL INTO :NEW.material_id FROM DUAL; END IF; END; /逻辑说明:NOCACHE是为了避免序列跳号,课程设计里经常要求主键连续,CACHE 20虽然快但会在数据库重启后丢一段号。触发器里加IF :NEW.material_id IS NULL判断,是为了兼容应用层已经手动赋值的场景,避免覆盖。应用层取号的写法是SELECT seq_material.NEXTVAL FROM DUAL,在 Java 里用PreparedStatement执行后取第一列。
参数说明:START WITH 1从 1 开始,INCREMENT BY 1每次加 1。如果预计数据量很大,可以设CACHE 100提升性能,但接受跳号。触发器方式适合不想改应用代码的场景,但批量插入 1000 条时逐行触发会明显变慢。
3.3 出入库事务在 JavaEE 里怎么保证库存不超卖
库存超卖是仓库管理系统最典型的并发问题:两个出库请求同时读到库存 10,各自扣 8,最后库存变成 -6。解决方式有两种,一种是在 SQL 层面用UPDATE ... WHERE quantity >= ?做乐观锁,另一种是在 JavaEE 里用容器管理事务加行锁。
// 出库核心逻辑,使用悲观锁锁定库存行 public boolean outbound(Long materialId, BigDecimal qty, String operator) { Connection conn = null; PreparedStatement ps = null; try { conn = dataSource.getConnection(); conn.setAutoCommit(false); // 先锁定库存行,FOR UPDATE 会阻塞其他事务 String lockSql = "SELECT quantity FROM t_stock WHERE material_id = ? FOR UPDATE"; ps = conn.prepareStatement(lockSql); ps.setLong(1, materialId); ResultSet rs = ps.executeQuery(); if (!rs.next()) { conn.rollback(); return false; } BigDecimal current = rs.getBigDecimal("quantity"); if (current.compareTo(qty) < 0) { conn.rollback(); return false; // 库存不足 } // 扣减库存 String updateSql = "UPDATE t_stock SET quantity = quantity - ?, update_time = SYSDATE WHERE material_id = ?"; ps = conn.prepareStatement(updateSql); ps.setBigDecimal(1, qty); ps.setLong(2, materialId); ps.executeUpdate(); // 写流水 String recordSql = "INSERT INTO t_stock_record(record_id, material_id, record_type, quantity, operator) " + "VALUES (seq_record.NEXTVAL, ?, 'O', ?, ?)"; ps = conn.prepareStatement(recordSql); ps.setLong(1, materialId); ps.setBigDecimal(2, qty); ps.setString(3, operator); ps.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { /* 记录日志 */ } } return false; } finally { // 关闭资源,连接池会自动回收 } }逻辑说明:SELECT ... FOR UPDATE在 Oracle 里会对满足条件的行加行级排他锁,第二个并发事务会阻塞在查询上,直到第一个事务提交或回滚。这样读到的库存一定是最新值,扣减不会超卖。setAutoCommit(false)开启手动事务,扣库存和写流水在同一个事务里,要么都成功要么都回滚。seq_record.NEXTVAL直接在 INSERT 里取序列,减少一次数据库往返。
参数说明:FOR UPDATE不要加NOWAIT,否则并发时会直接抛异常而不是等待。如果等待时间过长,可以在 Tomcat 或数据库层面设innodb_lock_wait_timeout类似的参数,Oracle 对应的是DISTRIBUTED_LOCK_TIMEOUT,但一般默认值够用。BigDecimal用于金额和数量,避免double精度丢失。
4. 避坑与排查:JavaEE+Oracle 仓库系统最常见的 5 个翻车现场
4.1 中文乱码:从 JDBC 到 Tomcat 到 Oracle 字符集
现象:入库单里的物料名称显示成问号或者方块,导出报表全是乱码。原因通常是三层字符集不一致:Tomcat 的URIEncoding、JDBC 连接的NLS_LANG、Oracle 数据库的NLS_CHARACTERSET。解决方式是先查数据库字符集,再统一应用侧编码。
-- 查 Oracle 字符集,必须是 AL32UTF8 或 ZHS16GBK SELECT parameter, value FROM nls_database_parameters WHERE parameter IN ('NLS_CHARACTERSET', 'NLS_NCHAR_CHARACTERSET');如果数据库是ZHS16GBK,JDBC URL 里加?useUnicode=true&characterEncoding=GBK;如果是AL32UTF8,用characterEncoding=UTF-8。Tomcat 的server.xml里 Connector 加URIEncoding="UTF-8"。三个地方对齐后乱码基本消失。
4.2 连接池耗尽:Too many connections 的真实原因
现象:系统跑一段时间后报ORA-00020: maximum number of processes exceeded或者 HikariCP 报Connection is not available。原因不一定是连接池设小了,更常见的是代码里取了连接没关,或者事务没提交导致连接一直被占用。排查方式是查 Oracle 的会话数:
-- 查当前会话数和活跃会话 SELECT COUNT(*) FROM v$session WHERE username = 'WAREHOUSE'; SELECT sid, serial#, status, machine, program FROM v$session WHERE username = 'WAREHOUSE' AND status = 'ACTIVE';如果 ACTIVE 会话数远大于连接池的maxTotal,说明有连接泄漏。检查所有getConnection()的地方是否在 finally 里关闭,或者用 try-with-resources。另外setAutoCommit(false)后忘记 commit 也会让连接一直持有锁。
4.3 序列跳号与主键冲突:NOCACHE 和 CACHE 的取舍
现象:主键不连续,或者批量插入时报ORA-00001: unique constraint violated。原因通常是序列用了CACHE但数据库重启,或者应用层和触发器同时取号导致重复。解决方式是统一取号入口,要么全用触发器,要么全用应用层nextval,不要混用。如果业务要求主键连续,用NOCACHE加ORDER,但性能会下降。
4.4 日期格式:Oracle 的 DATE 和 Java 的 java.util.Date 对不上
现象:入库时间存进去变成 0001-01-01 或者时分秒丢失。原因是 Oracle 的DATE包含年月日时分秒,但 JDBC 用setDate只传日期部分。解决方式是统一用java.sql.Timestamp或者setObject传java.util.Date,在 SQL 里用SYSDATE或TO_DATE显式转换。
// 正确写法,保留时分秒 ps.setTimestamp(1, new Timestamp(new java.util.Date().getTime()));4.5 分页查询在 Oracle 里的正确写法
现象:用 MySQL 的LIMIT语法在 Oracle 里报错。Oracle 分页要用ROWNUM或者 12c 以上的OFFSET ... FETCH。我一般用嵌套子查询加ROWNUM,兼容性最好:
SELECT * FROM ( SELECT a.*, ROWNUM rn FROM ( SELECT * FROM t_stock_record ORDER BY record_time DESC ) a WHERE ROWNUM <= 20 ) WHERE rn > 10;逻辑说明:内层排序,中层限制最大行号,外层取区间。ROWNUM是在结果集生成时分配的,所以必须嵌套两层才能正确分页。12c 以上可以直接用OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY,但 11g 不支持。
5. 从能跑到好用:库存对账与性能验证的两个进阶技巧
系统能跑起来只是第一步,真正交付前我会做两件事:库存对账和慢 SQL 排查。库存对账是验证t_stock的余额和t_stock_record的流水汇总是否一致,这是仓库管理系统的命根子。我一般写一个对账 SQL,定期跑或者手动触发:
-- 对账:库存余额 vs 流水汇总 SELECT s.material_id, m.material_name, s.quantity AS stock_qty, NVL(r.in_qty, 0) - NVL(r.out_qty, 0) AS record_qty FROM t_stock s JOIN t_material m ON s.material_id = m.material_id LEFT JOIN ( SELECT material_id, SUM(CASE WHEN record_type = 'I' THEN quantity ELSE 0 END) AS in_qty, SUM(CASE WHEN record_type = 'O' THEN quantity ELSE 0 END) AS out_qty FROM t_stock_record GROUP BY material_id ) r ON s.material_id = r.material_id WHERE s.quantity <> NVL(r.in_qty, 0) - NVL(r.out_qty, 0);逻辑说明:这条 SQL 找出库存余额和流水汇总不一致的物料。正常情况下结果为空,如果有记录,说明某次出入库事务只更新了一边,或者有人直接改过t_stock表。NVL处理没有流水的物料,避免 NULL 导致比较失败。CASE WHEN分别汇总入库和出库数量,比用两个子查询快。
参数说明:record_type = 'I'和'O'要和表设计里的定义一致。如果流水表数据量很大,在material_id和record_type上建联合索引,对账 SQL 能从全表扫描降到索引范围扫描。
第二个技巧是慢 SQL 定位。Oracle 的AWR报告对课程设计来说太重,我一般直接查v$sql找执行时间长的语句:
SELECT sql_id, sql_text, elapsed_time / 1000000 AS elapsed_sec, executions, buffer_gets FROM v$sql WHERE parsing_schema_name = 'WAREHOUSE' ORDER BY elapsed_time DESC FETCH FIRST 10 ROWS ONLY;逻辑说明:elapsed_time单位是微秒,除以 100 万换成秒。executions是执行次数,如果次数少但耗时高,说明单次查询慢;如果次数多且总耗时高,说明调用太频繁。buffer_gets高说明逻辑读多,通常意味着缺索引或者走了全表扫描。找到sql_id后用SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY_CURSOR('sql_id'))看执行计划。
参数说明:parsing_schema_name填业务账号,不要查 sys 的 SQL。FETCH FIRST 10 ROWS ONLY是 12c 以上语法,11g 用ROWNUM <= 10嵌套。如果v$sql查不到,可能是 SQL 已经被刷出共享池,需要开AWR或者用DBMS_SQLTUNE做 SQL 监控。
最后说一个我自己的习惯:每次改完库存相关的代码,先跑一遍对账 SQL,再手动模拟两个并发出库请求,看库存会不会变成负数。这个习惯帮我拦住了至少三次上线前的超卖 bug。仓库管理系统不怕功能少,就怕账对不上,账对不上,论文写得再漂亮也白搭。希望帮到你。
本文还有配套的精品资源,点击获取