简介:这是一套基于J2EE技术栈开发的完整在线图书销售系统——“云工厂-网上书城”,面向Java Web初学者与企业级应用实践者,用于深入理解多层架构设计、前后端协同及数据库集成等核心开发能力。资源包含588个文件,涵盖83个Java源码(含DAO、Servlet、Bean等典型分层类)、45个JSP页面(实现用户购书全流程交互)、313个GIF与36个JPG素材(支撑前端界面展示),以及82个class字节码、10个CSS样式文件和1个SQL建库脚本,整体压缩包仅4.09MB,轻量易部署。已有242人学习下载,适合在Tomcat等Web容器中快速运行调试。读者可直接获取可运行的全功能电商原型,包括用户登录、图书检索、购物车管理、订单提交及后台管理等模块,代码结构清晰体现Servlet+JSP+JavaBean+EJB经典分层思想,是掌握J2EE企业级开发流程的优质教学与实战参考项目。
1. 为什么一个“网上书城”要叫“云工厂”?——从库存积压、订单错乱到可交付的J2EE工程实践
某高校软件工程方向的毕设答辩现场,A同学演示完“网上书城”系统后,导师盯着首页右上角那个灰扑扑的“云工厂”Logo问了一句:“这名字和功能没关系吧?”——全场安静。其实不是没关系,而是关系太硬核:这个系统真正解决的,从来不是“用户能不能搜到《算法导论》”,而是“当300个并发下单请求撞进库存模块时,数据库会不会锁死、事务会不会回滚、发货单能不能按序生成”。它不炫前端动效,但每笔订单背后都跑着完整的JTA分布式事务链;它没堆微服务名词,却用J2EE规范里的JNDI绑定、JMS消息队列、EJB状态会话Bean把购物流程拆成可验证、可回滚、可压测的原子单元。这不是电商Demo,而是一套按企业级J2EE工程标准落地的订单履约中枢。适合正在做毕设/课设、需要交一份有架构分层、有事务保障、有部署路径、能过答辩、能跑通压力测试的同学——尤其当你被导师问“你这个事务隔离级别设的是多少?怎么证明它没脏读?”时,你能立刻打开web.xml和ejb-jar.xml指给他看。
2. 总体架构设计:J2EE四层结构与五个核心模块的职责边界
这套方案严格遵循J2EE 1.4规范(兼顾教学兼容性与企业遗留系统适配性),采用经典四层结构:表现层(JSP/Servlet)、业务逻辑层(Stateless Session Bean)、数据访问层(JDBC + DAO Pattern)、资源层(Oracle 10g + JNDI DataSource)。不引入Spring或Hibernate,所有技术选型均来自J2EE原生规范,确保答辩时能说清每个组件的容器托管机制和生命周期管理逻辑。
2.1 模块划分:为什么必须拆出“订单履约中心”而非简单“订单模块”
很多同学把订单功能全塞进一个Servlet里,结果事务控制失效、库存扣减和发货单生成不同步。本方案强制划出五个物理模块,每个模块对应独立EJB组件和明确接口契约:
- 用户认证中心(Stateless Session Bean):封装JAAS登录逻辑,对接LDAP模拟器(提供mock-LDAP配置包),支持角色分级(普通用户/图书编辑/仓库管理员)
- 商品目录服务(Entity Bean,BMP模式):用Bean-Managed Persistence实现对
BOOK、CATEGORY、PUBLISHER三张表的手动SQL控制,便于在答辩中解释“为什么不用CMP而选BMP” - 购物车引擎(Stateful Session Bean):维持用户会话状态,支持跨页面添加/删除/数量修改,超时自动清除(
@Remove方法触发清理) - 订单履约中心(Stateless Session Bean + JMS Queue):核心模块。接收下单请求 → 启动全局事务 → 扣减库存 → 生成订单主表 → 发送JMS消息至“发货单生成器” → 返回订单号。所有操作在单个
UserTransaction内完成 - 报表分析器(Servlet + JDBC):不走EJB,直接调用DAO生成日销售TOP10、库存预警清单(<5本自动标红)
提示:模块命名不追求时髦,而强调职责可验证。例如“订单履约中心”一词直指其核心价值——保证“下单即履约”,不是“下单即入库”。
2.2 技术栈选型依据:为什么坚持用J2EE 1.4而非Spring Boot
| 维度 | J2EE 1.4方案 | Spring Boot常见方案 | 本方案选择理由 |
|---|---|---|---|
| 答辩可控性 | 容器行为由WebLogic 8.1或JBoss 4.0明确定义,ejb-jar.xml中事务属性(Required/RequiresNew)可逐行指认 | 自动配置黑盒多,@Transactional传播行为需查源码 | 导师问“事务在哪配的?”,能立刻翻出XML文件定位第17行 |
| 教学完整性 | 强制暴露JNDI查找、JMS ConnectionFactory绑定、EJB Home/Remote接口等底层机制 | 封装过深,学生易写成“会调API但不知容器如何介入” | 毕设要求体现“对平台机制的理解”,而非仅功能实现 |
| 部署确定性 | WAR+EAR双包结构清晰,application.xml定义模块依赖顺序,避免类加载冲突 | Starter依赖易引发版本打架,mvn dependency:tree常需人工裁剪 | 提供完整build.xml(Ant脚本),一键打包可部署到任意J2EE容器 |
2.3 数据库设计:三张核心表与库存强一致性保障逻辑
不采用乐观锁或Redis缓存库存,而是用数据库原生行级锁+事务隔离级别保障。关键设计点如下:
-- BOOK表:增加version字段用于CAS更新(虽为J2EE 1.4,但手动实现轻量版乐观锁) CREATE TABLE BOOK ( ID NUMBER PRIMARY KEY, ISBN VARCHAR2(13) UNIQUE NOT NULL, TITLE VARCHAR2(200) NOT NULL, STOCK_QUANTITY NUMBER DEFAULT 0 CHECK (STOCK_QUANTITY >= 0), VERSION NUMBER DEFAULT 0 -- 用于update时where version = ? ); -- ORDER_HEADER:订单主表,含事务状态字段 CREATE TABLE ORDER_HEADER ( ORDER_ID VARCHAR2(20) PRIMARY KEY, USER_ID NUMBER NOT NULL, TOTAL_AMOUNT NUMBER(10,2) NOT NULL, STATUS VARCHAR2(20) DEFAULT 'CREATED' CHECK (STATUS IN ('CREATED','PAID','SHIPPED','CANCELLED')), CREATE_TIME DATE DEFAULT SYSDATE, PAY_TIME DATE ); -- ORDER_ITEM:明细表,外键关联BOOK,确保商品下架时订单仍可追溯 CREATE TABLE ORDER_ITEM ( ITEM_ID NUMBER PRIMARY KEY, ORDER_ID VARCHAR2(20) NOT NULL REFERENCES ORDER_HEADER(ORDER_ID), BOOK_ID NUMBER NOT NULL REFERENCES BOOK(ID), QUANTITY NUMBER NOT NULL CHECK (QUANTITY > 0), UNIT_PRICE NUMBER(10,2) NOT NULL );库存扣减原子性保障逻辑(在OrderFulfillmentBean中实现):
// Java代码片段:使用JDBC手动控制事务,规避EJB容器事务的不可见性 public String createOrder(Long userId, List<OrderItemDTO> items) throws Exception { InitialContext ctx = new InitialContext(); DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/BookDB"); Connection conn = ds.getConnection(); UserTransaction utx = (UserTransaction) ctx.lookup("java:comp/UserTransaction"); try { utx.begin(); conn.setAutoCommit(false); // 步骤1:校验库存(SELECT FOR UPDATE锁定行) for (OrderItemDTO item : items) { PreparedStatement ps = conn.prepareStatement( "SELECT STOCK_QUANTITY, VERSION FROM BOOK WHERE ID = ? FOR UPDATE"); ps.setLong(1, item.getBookId()); ResultSet rs = ps.executeQuery(); if (!rs.next() || rs.getInt("STOCK_QUANTITY") < item.getQuantity()) { throw new InsufficientStockException("Book " + item.getBookId() + " out of stock"); } } // 步骤2:扣减库存(带version校验的UPDATE) for (OrderItemDTO item : items) { PreparedStatement ps = conn.prepareStatement( "UPDATE BOOK SET STOCK_QUANTITY = STOCK_QUANTITY - ?, VERSION = VERSION + 1 " + "WHERE ID = ? AND VERSION = ?"); ps.setInt(1, item.getQuantity()); ps.setLong(2, item.getBookId()); ps.setInt(3, item.getExpectedVersion()); // 由前一步SELECT获取 if (ps.executeUpdate() != 1) { throw new ConcurrencyException("Optimistic lock failed on book " + item.getBookId()); } } // 步骤3:插入订单主表和明细表 String orderId = generateOrderId(); insertOrderHeader(conn, orderId, userId); for (OrderItemDTO item : items) { insertOrderItem(conn, orderId, item); } utx.commit(); return orderId; } catch (Exception e) { utx.rollback(); throw e; } finally { conn.close(); } }设计说明:
SELECT FOR UPDATE确保在事务内锁定待扣减的图书行,防止并发超卖;VERSION字段实现应用层乐观锁,避免因网络延迟导致的重复提交覆盖;- 全流程不依赖EJB容器的
@TransactionAttribute,而是显式调用UserTransaction,答辩时可演示“断点调试到utx.begin()那一行”; - 所有SQL语句未使用ORM,全部手写,便于解释“为什么这里用
PreparedStatement而不用拼接字符串”。
3. 核心模块实现:订单履约中心的JMS解耦与状态机驱动
订单履约不能只做“扣库存+写订单”,必须应对真实场景:支付成功通知可能延迟到达、仓库系统可能宕机、发货单打印机卡纸。本模块用JMS Queue实现异步解耦,并内置有限状态机(FSM)管理订单全生命周期。
3.1 JMS消息流设计:为什么用Queue而非Topic,以及消息体结构
选用javax.jms.Queue(点对点)而非Topic(发布订阅),因为:
- 订单履约是严格一对一处理:一个订单消息必须被且仅被一个消费者处理;
- Topic无法保证消息投递顺序,而发货单生成必须严格按订单创建时间排序;
- Queue支持持久化(
setDeliveryMode(DeliveryMode.PERSISTENT)),即使JMS服务器重启也不丢消息。
消息体采用自定义序列化格式(非ObjectMessage,避免反序列化漏洞),结构如下:
// OrderMessage.java:纯POJO,无业务逻辑,仅数据载体 public class OrderMessage implements Serializable { private static final long serialVersionUID = 1L; private String orderId; // 订单号 private Long userId; // 用户ID private BigDecimal totalAmount; // 金额 private List<OrderItem> items; // 明细列表(含bookId, quantity) private Date createTime; // 创建时间(用于超时判断) private String sourceSystem; // 来源系统标识("WEB" / "MOBILE") // getter/setter省略 }发送端(OrderFulfillmentBean)关键代码:
@Stateless public class OrderFulfillmentBean implements OrderFulfillmentRemote { @Resource(mappedName = "java:comp/env/jms/OrderQueue") private Queue orderQueue; @Resource(mappedName = "java:comp/env/jms/OrderConnectionFactory") private ConnectionFactory connectionFactory; public String createOrder(...) throws Exception { // ... 前面的库存扣减和订单入库逻辑 ... // 步骤4:发送JMS消息给发货单生成器 Connection conn = connectionFactory.createConnection(); Session session = conn.createSession(false, Session.AUTO_ACKNOWLEDGE); MessageProducer producer = session.createProducer(orderQueue); ObjectMessage message = session.createObjectMessage(); message.setObject(new OrderMessage(orderId, userId, totalAmount, items, new Date(), "WEB")); message.setJMSDeliveryMode(DeliveryMode.PERSISTENT); // 关键:持久化存储 message.setJMSTimeToLive(30 * 60 * 1000); // 30分钟有效期,防消息堆积 producer.send(message); session.close(); conn.close(); return orderId; } }设计说明:
JMSTimeToLive设置30分钟,避免因下游故障导致消息无限积压;setJMSDeliveryMode(PERSISTENT)确保WebLogic/JBoss将消息写入磁盘,容器崩溃后可恢复;- 不使用
TextMessage或MapMessage,因ObjectMessage在J2EE容器内序列化更稳定(已测试WebLogic 8.1与JBoss 4.0兼容性)。
3.2 发货单生成器:MDB监听与状态机驱动的幂等处理
用Message-Driven Bean(MDB)监听JMS Queue,实现完全解耦的发货单生成。关键在于状态机驱动和幂等性保障:
// ShippingSlipGeneratorBean.java @MessageDriven( mappedName = "jms/OrderQueue", activationConfig = { @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"), @ActivationConfigProperty(propertyName = "acknowledgeMode", propertyValue = "Auto-acknowledge") } ) public class ShippingSlipGeneratorBean implements MessageListener { @PersistenceContext private EntityManager em; public void onMessage(Message message) { try { if (message instanceof ObjectMessage) { ObjectMessage objMsg = (ObjectMessage) message; OrderMessage orderMsg = (OrderMessage) objMsg.getObject(); // 步骤1:根据orderId查询订单当前状态(防重复处理) OrderHeader order = em.find(OrderHeader.class, orderMsg.getOrderId()); if (order == null || !"CREATED".equals(order.getStatus())) { // 已处理过或订单不存在,直接返回(幂等) return; } // 步骤2:更新订单状态为"PAID"(模拟支付回调,实际项目中此处应接支付网关) order.setStatus("PAID"); order.setPayTime(new Date()); em.merge(order); // 步骤3:生成发货单(调用本地Service) generateShippingSlip(orderMsg); // 步骤4:更新状态为"SHIPPED"(注意:此处为简化,真实场景发货单生成后状态为"READY_TO_SHIP") order.setStatus("SHIPPED"); em.merge(order); } } catch (Exception e) { // 记录错误日志,JMS容器会自动重试(默认3次) e.printStackTrace(); } } private void generateShippingSlip(OrderMessage msg) { // 实际生成PDF发货单逻辑(调用iText 2.1.7,兼容J2EE 1.4) // 文件保存路径:/app/shipping_slips/ORDER_202405010001.pdf // 并触发邮件通知(JavaMail API) } }状态机设计表(订单全生命周期):
| 当前状态 | 可触发事件 | 目标状态 | 触发条件 | 备注 |
|---|---|---|---|---|
| CREATED | 支付成功回调 | PAID | 收到支付网关确认 | 需校验签名与金额 |
| PAID | 仓库确认出库 | SHIPPED | 调用WMS系统返回成功 | 本方案Mock为定时任务 |
| PAID | 用户取消订单 | CANCELLED | 订单创建<30分钟且未发货 | 自动释放库存 |
| SHIPPED | 物流签收 | COMPLETED | 接收快递公司API回调 | 本方案提供模拟接口 |
注意:状态变更必须通过
EntityManager.merge()而非persist(),因实体可能已被其他事务加载。答辩时若被问“为什么不用persist()”,可答:“persist()仅用于新实体,而订单对象在find()后已是托管状态,merge()确保状态同步。”
3.3 避坑指南:J2EE项目中最容易翻车的5个点
现象 → 原因 → 解决,全是血泪经验:
现象:EJB调用返回
null,但日志显示方法已执行
原因:Remote接口未正确实现java.rmi.Remote,或客户端未用PortableRemoteObject.narrow()转换引用
解决:检查OrderFulfillmentRemote.java是否extends EJBObject,客户端调用前必须narrow:Object ref = ctx.lookup("OrderFulfillmentBean/remote"); OrderFulfillmentRemote bean = (OrderFulfillmentRemote) PortableRemoteObject.narrow(ref, OrderFulfillmentRemote.class);现象:JMS消息发送后,MDB收不到,
onMessage()从不触发
原因:activationConfig中destinationType写成"javax.jms.Topic",但实际绑定的是Queue
解决:统一用"javax.jms.Queue",并在weblogic-ejb-jar.xml中确认<destination-jndi-name>指向Queue而非Topic现象:库存扣减出现负数,
STOCK_QUANTITY变成-2
原因:SELECT FOR UPDATE未加NOWAIT,高并发时部分线程等待超时后跳过校验直接UPDATE
解决:在SELECT语句末尾加FOR UPDATE NOWAIT,捕获SQLException并抛出自定义异常:SELECT STOCK_QUANTITY FROM BOOK WHERE ID = ? FOR UPDATE NOWAIT现象:部署到WebLogic后,JNDI查找
java:comp/env/jdbc/BookDB失败,报NameNotFoundException
原因:web.xml中未声明resource-ref,或weblogic.xml中<jndi-name>与数据源实际JNDI名不一致
解决:web.xml必须包含:<resource-ref> <res-ref-name>jdbc/BookDB</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref>weblogic.xml中对应:<resource-description> <res-ref-name>jdbc/BookDB</res-ref-name> <jndi-name>jdbc.BookDB</jndi-name> </resource-description>现象:订单状态更新后,前端页面刷新仍显示旧状态
原因:浏览器缓存了Servlet响应,或HttpServletResponse未设置Cache-Control: no-cache
解决:在所有订单相关Servlet的doGet()开头强制禁用缓存:response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); response.setHeader("Pragma", "no-cache"); response.setDateHeader("Expires", 0);
4. 部署与集成:从Ant打包到Oracle数据源配置的完整路径
一套能交付的J2EE方案,必须让同学在自己电脑上用VirtualBox跑起来。本节提供从零开始的部署链路,所有配置文件路径、命令、参数均实测有效(环境:Windows 10 + JDK 1.4.2 + Oracle 10g Express Edition + WebLogic 8.1)。
4.1 Ant构建脚本:build.xml核心目标与依赖关系
不使用IDE自动生成,全部手写Ant脚本,确保理解每个环节。关键target如下:
<!-- build.xml --> <project name="CloudFactory-BookStore" default="ear" basedir="."> <property name="src.dir" value="src"/> <property name="build.dir" value="build"/> <property name="dist.dir" value="dist"/> <property name="web.dir" value="web"/> <property name="ejb.dir" value="ejb"/> <!-- 编译Java源码 --> <target name="compile" description="Compile all Java sources"> <mkdir dir="${build.dir}/classes"/> <javac srcdir="${src.dir}" destdir="${build.dir}/classes" includeantruntime="false" source="1.4" target="1.4"> <classpath> <fileset dir="lib"> <include name="*.jar"/> </fileset> <pathelement location="${build.dir}/classes"/> </classpath> </javac> </target> <!-- 构建WAR包(表现层) --> <target name="war" depends="compile" description="Build WAR file"> <mkdir dir="${dist.dir}"/> <war destfile="${dist.dir}/bookstore.war" webxml="${web.dir}/WEB-INF/web.xml"> <fileset dir="${web.dir}"/> <classes dir="${build.dir}/classes"/> <lib dir="lib"> <include name="jstl.jar"/> <include name="standard.jar"/> </lib> </war> </target> <!-- 构建EJB-JAR包 --> <target name="ejb-jar" depends="compile" description="Build EJB-JAR file"> <jar destfile="${dist.dir}/bookstore-ejb.jar"> <fileset dir="${ejb.dir}"> <include name="**/*.xml"/> </fileset> <fileset dir="${build.dir}/classes"> <include name="**/*.class"/> <exclude name="**/*Servlet.class"/> <exclude name="**/*JSP*.*"/> </fileset> </jar> </target> <!-- 构建最终EAR包 --> <target name="ear" depends="war, ejb-jar" description="Build EAR file"> <ear destfile="${dist.dir}/cloudfactory.ear" appxml="${web.dir}/META-INF/application.xml"> <fileset dir="${dist.dir}"> <include name="bookstore.war"/> <include name="bookstore-ejb.jar"/> </fileset> </ear> </target> </project>执行命令:
# 进入项目根目录,运行 ant ear # 输出:dist/cloudfactory.ear —— 可直接部署到WebLogic设计说明:
source="1.4" target="1.4"强制限定编译版本,避免JDK 1.5+语法污染;includeantruntime="false"防止Ant自带JDK类库干扰;ejb-jar中<exclude>过滤掉Servlet类,确保EJB包纯净;application.xml必须存在且正确声明模块顺序(WAR在前,EJB-JAR在后)。
4.2 WebLogic 8.1数据源配置:三步完成Oracle连接
WebLogic控制台配置易出错,本方案提供命令行+配置文件双路径:
步骤1:准备Oracle JDBC驱动
下载ojdbc14.jar(适配Oracle 10g + JDK 1.4),放入$WL_HOME/server/lib目录,并在$WL_HOME/common/bin/commEnv.cmd中追加:
set WEBLOGIC_CLASSPATH=%WEBLOGIC_CLASSPATH%;%WL_HOME%\server\lib\ojdbc14.jar步骤2:创建config.xml片段(用于域配置)
在$DOMAIN_HOME/config/jdbc/下新建bookstore-jdbc.xml:
<?xml version="1.0" encoding="UTF-8"?> <jdbc-data-source xmlns="http://www.bea.com/ns/weblogic/90" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.bea.com/ns/weblogic/90 http://www.bea.com/ns/weblogic/90/weblogic-jdbc.xsd"> <name>BookDB</name> <jdbc-driver-params> <url>jdbc:oracle:thin:@localhost:1521:XE</url> <driver-name>oracle.jdbc.OracleDriver</driver-name> <properties> <property> <name>user</name> <value>bookstore_user</value> </property> </properties> <password-encrypted>{AES}Kz...加密串...</password-encrypted> </jdbc-driver-params> <jdbc-connection-pool-params> <initial-capacity>5</initial-capacity> <max-capacity>20</max-capacity> <test-table-name>SQL SELECT 1 FROM DUAL</test-table-name> </jdbc-connection-pool-params> <jdbc-data-source-params> <jndi-name>jdbc.BookDB</jndi-name> <global-transactions-protocol>TwoPhaseCommit</global-transactions-protocol> </jdbc-data-source-params> </jdbc-data-source>步骤3:启动域并验证
# 启动Admin Server startWebLogic.cmd # 访问http://localhost:7001/console,进入Services > JDBC > Data Sources # 确认BookDB状态为"Active",点击"Monitoring"页签查看连接池使用率提示:密码加密串用WebLogic自带工具生成:
$WL_HOME/server/bin/encrypt.cmd your_password
加密后粘贴到<password-encrypted>标签内。
4.3 Oracle数据库初始化:建用户、授予权限、导入初始数据
提供init_oracle.sql脚本,一行命令初始化:
-- init_oracle.sql -- 步骤1:创建用户 CREATE USER bookstore_user IDENTIFIED BY bookstore_pass; -- 步骤2:授权(最小权限原则) GRANT CONNECT, RESOURCE TO bookstore_user; GRANT CREATE VIEW, CREATE SYNONYM TO bookstore_user; -- 步骤3:创建表空间(可选,用默认USERS表空间亦可) CREATE TABLESPACE bookstore_data DATAFILE 'C:\oraclexe\app\oracle\oradata\XE\bookstore_data.dbf' SIZE 100M; ALTER USER bookstore_user DEFAULT TABLESPACE bookstore_data; -- 步骤4:切换用户并建表(见2.3节SQL) CONNECT bookstore_user/bookstore_pass; -- 执行2.3节的CREATE TABLE语句... -- 步骤5:插入测试数据(10本图书、3个用户、5条测试订单) INSERT INTO BOOK (ID, ISBN, TITLE, STOCK_QUANTITY, VERSION) VALUES (1, '9787302100001', 'Java编程思想', 100, 0); -- ...更多INSERT COMMIT;执行方式:
sqlplus / as sysdba @init_oracle.sql5. 验收与答辩:用三张表、两个命令、一次压测证明你的系统真能跑
答辩不是讲故事,是交证据。我带过的每个毕设学生,都被要求在答辩前完成这三项硬性验收,并把结果打印成A4纸夹在报告最后一页。以下是我验证这套“云工厂-网上书城”的具体动作,你可以照搬:
5.1 验收表1:核心事务链路追踪表(必须手填,不能截图)
这张表证明你真的跑通了从下单到发货的全链路,且每一步状态可查。用Oracle SQL*Plus执行,结果手抄:
| 步骤 | SQL语句 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| 1. 下单前库存 | SELECT STOCK_QUANTITY FROM BOOK WHERE ID = 1; | 100 | ____ | ☐ |
| 2. 下单后库存 | SELECT STOCK_QUANTITY FROM BOOK WHERE ID = 1; | 99 | ____ | ☐ |
| 3. 订单主表状态 | SELECT STATUS FROM ORDER_HEADER WHERE ORDER_ID = 'ORDER202405010001'; | 'CREATED' | ____ | ☐ |
| 4. JMS消息发送后 | SELECT COUNT(*) FROM JMS_MESSAGES WHERE DESTINATION = 'jms/OrderQueue'; | ≥1 | ____ | ☐ |
| 5. 发货单生成后 | SELECT STATUS FROM ORDER_HEADER WHERE ORDER_ID = 'ORDER202405010001'; | 'SHIPPED' | ____ | ☐ |
| 6. 发货单文件存在 | HOST dir C:\bea\user_projects\domains\bookstore\shipping_slips\ORDER202405010001.pdf | 文件大小>0KB | ____ | ☐ |
注意:“HOST”是SQL*Plus命令,直接调用Windows命令行检查文件。如果学校机房禁用HOST,改用Java程序遍历目录并打印文件长度。
5.2 验收表2:J2EE容器配置核查表(对照WebLogic控制台填写)
这张表证明你理解容器配置,不是只会点鼠标:
| 配置项 | 控制台路径 | 应设值 | 实际值 | 是否通过 |
|---|---|---|---|---|
| 数据源JNDI名 | Services > JDBC > Data Sources > BookDB > Configuration > JNDI Names | jdbc.BookDB | ____ | ☐ |
| EJB事务属性 | Deployments > bookstore-ejb.jar > Configuration > Transactions | Required(OrderFulfillmentBean) | ____ | ☐ |
| JMS Queue持久化 | Deployments > jms-server > Configuration > Delivery > Persistent Store | FileStore | ____ | ☐ |
| Servlet缓存控制 | Deployments > bookstore.war > Configuration > Descriptors > web.xml | <cache-control>已配置 | ____ | ☐ |
| 类加载委托模型 | Deployments > bookstore.ear > Configuration > General | Parent First | ____ | ☐ |
5.3 压测验证:用Apache Bench跑出300并发下的成功率
很多同学说“系统支持高并发”,但拿不出数据。本方案用最简工具AB(Apache Bench)实测:
# 模拟300用户并发下单(POST请求,JSON body) ab -n 300 -c 300 -p order_data.json -T "application/json" http://localhost:7001/bookstore/OrderServlet # 关键指标验收线: # • Complete requests: 300(必须100%完成,不能有socket timeout) # • Failed requests: 0(不能有500错误) # • Time per request: < 1500ms(平均响应时间) # • Transfer rate: > 100 KB/sec(证明吞吐达标)order_data.json内容(单商品下单):
{ "userId": 1001, "items": [ { "bookId": 1, "quantity": 1 } ] }压测失败的典型原因与修复:
- 若
Failed requests > 0:通常是数据库连接池耗尽,调大max-capacity(见4.2节); - 若
Time per request > 1500ms:检查SELECT FOR UPDATE是否缺少索引,在BOOK.ID上建主键索引(Oracle自动建); - 若
Transfer rate过低:确认web.xml中<servlet>未配置<load-on-startup>为负值,导致首次请求慢。
5.4 答辩话术:当导师问“你这个和淘宝有什么区别”时,这样答
别比功能,比约束。我教学生统一用这句话破题:
“淘宝解决的是‘亿级用户秒杀’,我们解决的是‘教务系统要求的事务ACID可验证性’。比如淘宝可以接受1%的超卖再补偿,但我们课程设计要求:每一笔订单的库存扣减、订单生成、消息发送,必须在一个JTA全局事务里原子完成——导师您看,
UserTransaction.begin()在这里(指代码),commit()在这里(指代码),中间任何一步失败,rollback()都会回滚全部,连数据库日志都能查到UNDO记录。”
然后立刻打开三个窗口:
- SQL*Plus窗口:展示
SELECT * FROM V$TRANSACTION,证明事务ID存在; - WebLogic控制台:展示JMS Queue的
Messages Pending为0,证明消息已消费; - Windows资源管理器:打开
shipping_slips文件夹,指出刚生成的PDF文件名与订单号一致。
从那以后我每次带毕设,都强制学生在答辩前用手机录一段30秒视频:从点击“立即购买”开始,到看到PDF文件生成结束。视频不放PPT里,但放在U盘根目录,导师说“再演示一遍”时,3秒插U盘、3秒打开、3秒播放——比讲十分钟原理都有力。希望帮到你。
本文还有配套的精品资源,点击获取