拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JavaEE订餐系统课程设计:Servlet+JSP事务与会话实战指南

JavaEE订餐系统课程设计:Servlet+JSP事务与会话实战指南

简介:本资源是一份完整的JavaEE课程设计报告书,面向高校计算机及相关专业学生,聚焦Web应用开发实践,解决从理论到工程落地的能力转化问题。报告以‘网上订餐系统’为载体,系统阐述了基于Struts+Spring+Hibernate框架的全栈开发流程,涵盖需求分析、多角色模块设计(用户/管理员/菜品/订单/反馈)、MySQL数据库建模(含菜品、用户、管理员、订单、分类等6张核心表)、系统流程图与功能框图、异常处理机制(自定义SystemException及全局配置)及登录界面等关键实现细节。资源为单文件PDF,大小985KB,内容结构完整,图文结合,含字段说明表、配置代码片段与界面示意图,便于理解技术选型逻辑与工程规范。目前已有739人学习下载,适合作为JavaEE课程设计参考范本、毕业设计前期素材或Web开发入门实战复盘资料。

1. 这不是写个“点餐页面”就交差的课程设计:JavaEE订餐系统必须跑通事务边界、会话管理、三层解耦,否则答辩当场被问住

你手里的《订餐系统JavaEE课程设计报告书.pdf》,大概率是某高校计算机专业大三下学期的期末硬仗——它表面是“做个网页点点菜”,实则是JavaEE技术栈的一次全链路压力测试:Servlet要扛住并发请求,JSP得干净分离展示逻辑,DAO层必须用JDBC或Hibernate管住数据库连接池,而最关键的,是用户登录态不能跨页丢失、下单扣库存和生成订单必须原子执行、管理员后台删菜品不能误删关联订单。我带过三届课程设计,80%的学生卡在“登录后跳转首页又变回未登录”“下单成功但库存没减”“管理员删了菜,用户历史订单里显示‘null’”这三类问题上。这不是代码写得丑,而是对JavaEE核心契约(如HTTP Session生命周期、JDBC事务隔离级别、Servlet容器线程模型)理解有断层。这篇笔记不讲PPT怎么排版、报告怎么凑字数,只拆解:从零搭起一个能过答辩、能现场演示、能解释清楚每一行为什么这么写的JavaEE订餐系统——用Tomcat 9 + MySQL 8 + 原生JDBC(不依赖Spring Boot简化干扰),所有代码可直接粘贴进Eclipse或VS Code运行,每一步都标清JDK版本兼容性、web.xml配置陷阱、以及老师最常揪着问的3个底层原理。


2. 用Servlet+JSP搭骨架:为什么不用Spring Boot?因为课程设计考的是容器本质

课程设计不是项目交付,考的是你对JavaEE规范的理解深度。Spring Boot自动装配会掩盖Servlet容器如何加载Filter、Listener、Servlet的顺序,而答辩时老师一句“说说web.xml里load-on-startup=1的作用”,就能筛掉一半人。所以本方案坚持原生JavaEE:Tomcat 9.0.83(JDK 11兼容)、MySQL 8.0.33、JDBC驱动用mysql-connector-java-8.0.33.jar。所有组件版本锁定,避免“本地能跑服务器报错”的玄学翻车。

2.1 创建标准Java Web项目结构:别让目录名毁掉整个部署

Eclipse或VS Code新建Dynamic Web Project时,Project name必须不含空格和中文(例:dining-system),Target runtime选Apache Tomcat v9.0。关键目录结构必须严格如下:

dining-system/ ├── src/ # Java源码 │ ├── servlet/ # 所有Servlet类 │ ├── dao/ # 数据访问对象(JDBC操作) │ ├── model/ # 实体类(User, Dish, Order等) │ └── util/ # 工具类(DBUtil, StringUtil) ├── WebContent/ # Web根目录(Tomcat部署时的ROOT) │ ├── WEB-INF/ │ │ ├── web.xml # 核心配置文件(必须!) │ │ └── lib/ # 放mysql-connector-java-8.0.33.jar │ ├── index.jsp # 首页(游客可见) │ ├── login.jsp # 登录页 │ ├── user/ # 用户功能页(需登录) │ │ ├── order.jsp # 下单页 │ │ └── history.jsp # 订单历史 │ └── admin/ # 管理员页(需权限校验) │ ├── dish-manage.jsp # 菜品管理 │ └── order-list.jsp # 订单列表 └── build.xml # Ant构建脚本(可选,但推荐)

提示:WebContent是Eclipse默认Web根目录名,VS Code中若用Tomcat插件部署,需在settings.json中显式指定"tomcat.server.webappDir": "./WebContent",否则静态资源404。

2.2 web.xml配置:三个必须声明的组件及其执行顺序

WEB-INF/web.xml是JavaEE的灵魂文件,漏配任意一项,系统必崩。以下是精简但完整的配置(Servlet 4.0规范):

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <!-- 1. 全局上下文参数:数据库连接信息 --> <context-param> <param-name>jdbcUrl</param-name> <param-value>jdbc:mysql://localhost:3306/dining?useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true</param-value> </context-param> <context-param> <param-name>jdbcUsername</param-name> <param-value>root</param-value> </context-param> <context-param> <param-name>jdbcPassword</param-name> <param-value>your_password</param-value> </context-param> <!-- 2. 监听器:应用启动时初始化数据库连接池 --> <listener> <listener-class>util.DBListener</listener-class> </listener> <!-- 3. 过滤器:统一处理中文乱码和登录校验 --> <filter> <filter-name>EncodingFilter</filter-name> <filter-class>util.EncodingFilter</filter-class> </filter> <filter-mapping> <filter-name>EncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter> <filter-name>LoginCheckFilter</filter-name> <filter-class>util.LoginCheckFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginCheckFilter</filter-name> <url-pattern>/user/*</url-pattern> <url-pattern>/admin/*</url-pattern> </filter-mapping> <!-- 4. Servlet映射:按业务模块分组 --> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping> <servlet> <servlet-name>OrderServlet</servlet-name> <servlet-class>servlet.OrderServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>OrderServlet</servlet-name> <url-pattern>/order</url-pattern> </servlet-mapping> <servlet> <servlet-name>DishAdminServlet</servlet-name> <servlet-class>servlet.DishAdminServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>DishAdminServlet</servlet-name> <url-pattern>/admin/dish</url-pattern> </servlet-mapping> </web-app>

参数说明与逻辑:

  • <context-param>提供全局配置,避免硬编码在Java类中;&amp;是XML实体,必须写成&amp;而非&,否则Tomcat启动报错。
  • <listener>绑定DBListener,其contextInitialized()方法会在Tomcat加载Web应用时执行,用于初始化DataSource(见2.3节)。
  • 两个<filter>按声明顺序执行:先EncodingFilter解决POST中文乱码,再LoginCheckFilter拦截未登录访问。注意<url-pattern>用/*匹配所有路径,但/user/*和/admin/*是精确前缀匹配,确保静态资源(如CSS/JS)不被误拦。
  • Servlet映射路径/login、/order等,是浏览器地址栏输入的URL,不是JSP文件路径。JSP页面通过RequestDispatcher.forward()跳转,不暴露真实路径。

2.3 DBListener:用Tomcat自带连接池,拒绝手动new Connection

手写DriverManager.getConnection()是课程设计大忌——每次请求都建新连接,10人并发就拖垮MySQL。必须用Tomcat JNDI数据源,由容器管理连接池。util.DBListener.java代码如下:

package util; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.sql.DataSource; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; import java.sql.Connection; import java.sql.SQLException; @WebListener public class DBListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { try { // 1. 初始化JNDI上下文 Context initCtx = new InitialContext(); Context envCtx = (Context) initCtx.lookup("java:comp/env"); // 2. 查找配置在context.xml中的数据源 DataSource ds = (DataSource) envCtx.lookup("jdbc/DiningDS"); // 3. 将数据源存入ServletContext,供其他Servlet获取 sce.getServletContext().setAttribute("dataSource", ds); // 4. 测试连接(可选,但强烈建议) Connection conn = ds.getConnection(); conn.close(); System.out.println("✅ DBListener: 数据源初始化成功,连接池已就绪"); } catch (NamingException | SQLException e) { e.printStackTrace(); throw new RuntimeException("❌ DBListener: 数据源初始化失败", e); } } @Override public void contextDestroyed(ServletContextEvent sce) { // 容器关闭时清理(通常无需手动操作) } }

关键点说明:

  • @WebListener注解替代web.xml中<listener>声明,更简洁(Servlet 3.0+支持)。
  • java:comp/env是JNDI标准命名空间,jdbc/DiningDS对应Tomcatconf/context.xml中配置的数据源名(见2.4节)。
  • sce.getServletContext().setAttribute("dataSource", ds)将数据源存入全局上下文,后续Servlet可通过getServletContext().getAttribute("dataSource")获取,避免重复查找JNDI,提升性能。
  • 测试连接conn.close()验证数据源可用性,防止部署后才发现数据库连不上。

2.4 Tomcat context.xml配置:把数据库密码从web.xml里摘出来

web.xml里明文写密码是严重安全漏洞,且不符合JavaEE最佳实践。正确做法是将数据源配置移到Tomcat的conf/context.xml(全局)或META-INF/context.xml(应用级)。推荐后者,便于项目迁移:

在WebContent/META-INF/context.xml中创建(若无此目录则新建):

<?xml version="1.0" encoding="UTF-8"?> <Context> <Resource name="jdbc/DiningDS" auth="Container" type="javax.sql.DataSource" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/dining?useSSL=false&amp;serverTimezone=Asia/Shanghai&amp;allowPublicKeyRetrieval=true" username="root" password="your_password" maxActive="20" minIdle="5" maxWait="10000" validationQuery="SELECT 1" testOnBorrow="true" removeAbandonedOnBorrow="true" removeAbandonedTimeout="60"/> </Context>

参数详解:

  • name="jdbc/DiningDS"必须与DBListener中envCtx.lookup("jdbc/DiningDS")完全一致。
  • maxActive="20":连接池最大连接数,课程设计设20足够(实际并发远低于此)。
  • validationQuery="SELECT 1":借连接时执行的检测SQL,确保连接有效;MySQL必须用SELECT 1,不能用SELECT * FROM DUAL。
  • testOnBorrow="true":每次从池中取连接时检测,牺牲一点性能换稳定性,课程设计必须开启,避免因MySQL超时断连导致下单失败。
  • removeAbandonedTimeout="60":连接被占用超60秒未归还,强制回收,防内存泄漏。

3. DAO层设计:用JDBC Template思想手写CRUD,拒绝SQL拼接

很多学生用String sql = "INSERT INTO user VALUES('"+name+"', '"+pwd+"')",这是SQL注入温床,也是答辩时老师必问“如何防止注入”的反面教材。本方案采用预编译+实体类映射,DAO层只做数据操作,不涉业务逻辑。

3.1 BaseDAO:封装通用JDBC操作,减少模板代码

dao/BaseDAO.java提供所有DAO继承的基础能力:

package dao; import util.DBUtil; import javax.sql.DataSource; import java.sql.*; import java.util.ArrayList; import java.util.List; public abstract class BaseDAO { protected DataSource dataSource; public BaseDAO() { // 从ServletContext获取数据源(需在Servlet中setDataSource) this.dataSource = (DataSource) DBUtil.getServletContext().getAttribute("dataSource"); } /** * 执行更新操作(INSERT/UPDATE/DELETE) * @param sql 预编译SQL * @param params 参数数组(按?顺序) * @return 影响行数 */ protected int executeUpdate(String sql, Object... params) { int rows = 0; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { // 设置参数 for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } rows = ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException("❌ 执行更新失败: " + sql, e); } return rows; } /** * 查询单条记录(如根据ID查用户) * @param sql 预编译SQL * @param rowMapper 结果集映射器 * @param params 参数数组 * @param <T> 返回实体类型 * @return 实体对象或null */ protected <T> T executeQuerySingle(String sql, RowMapper<T> rowMapper, Object... params) { T result = null; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { result = rowMapper.mapRow(rs); } } } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException("❌ 查询单条失败: " + sql, e); } return result; } /** * 查询多条记录(如查所有菜品) * @param sql 预编译SQL * @param rowMapper 结果集映射器 * @param params 参数数组 * @param <T> 返回实体列表类型 * @return 实体列表 */ protected <T> List<T> executeQueryList(String sql, RowMapper<T> rowMapper, Object... params) { List<T> list = new ArrayList<>(); try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { list.add(rowMapper.mapRow(rs)); } } } catch (SQLException e) { e.printStackTrace(); throw new RuntimeException("❌ 查询列表失败: " + sql, e); } return list; } // 回调接口,供子类实现结果映射 @FunctionalInterface public interface RowMapper<T> { T mapRow(ResultSet rs) throws SQLException; } }

为什么这样设计:

  • executeUpdate和executeQueryXXX方法封装了Connection、PreparedStatement、ResultSet的获取与关闭,避免学生忘记close导致连接泄露。try-with-resources确保资源自动释放。
  • RowMapper是函数式接口,让子类DAO用Lambda表达式定义映射逻辑,代码更简洁(见3.2节)。
  • 所有异常转为RuntimeException,上层Servlet统一捕获处理,DAO层不处理业务错误(如“用户名已存在”需在Service层判断)。

3.2 UserDAO:登录校验必须用PreparedStatement防注入

dao/UserDAO.java继承BaseDAO,实现用户相关操作:

package dao; import model.User; import java.sql.SQLException; public class UserDAO extends BaseDAO { /** * 根据用户名查用户(用于登录校验) * ✅ 使用预编译,防止SQL注入 * ❌ 错误写法: "SELECT * FROM user WHERE username = '" + username + "'" */ public User findByUsername(String username) { String sql = "SELECT id, username, password, role FROM user WHERE username = ?"; return executeQuerySingle(sql, rs -> { User user = new User(); user.setId(rs.getLong("id")); user.setUsername(rs.getString("username")); user.setPassword(rs.getString("password")); // 密码明文存储仅用于课程设计,实际需BCrypt加密 user.setRole(rs.getString("role")); // 'user' or 'admin' return user; }, username); } /** * 检查用户名是否已存在(注册时用) */ public boolean existsByUsername(String username) { String sql = "SELECT COUNT(*) FROM user WHERE username = ?"; Long count = executeQuerySingle(sql, rs -> rs.getLong(1), username); return count != null && count > 0; } /** * 创建新用户(注册) */ public int insert(User user) { String sql = "INSERT INTO user (username, password, role) VALUES (?, ?, ?)"; return executeUpdate(sql, user.getUsername(), user.getPassword(), user.getRole()); } }

血泪经验:

  • 登录校验findByUsername必须用?占位符,绝对禁止字符串拼接。曾有学生被老师现场输入' OR '1'='1,直接绕过登录进入管理员页,当场挂科。
  • existsByUsername返回Long而非boolean,因为COUNT(*)结果是数字,直接映射更安全。
  • 密码明文存储仅限课程设计演示,实际必须用BCryptPasswordEncoder.encode(password),但课程设计报告里写“此处应使用BCrypt加密”即可过关。

3.3 DishDAO:菜品管理需支持分页,避免一次查几千条撑爆内存

dao/DishDAO.java实现菜品增删改查,重点解决分页问题:

package dao; import model.Dish; import java.util.List; public class DishDAO extends BaseDAO { /** * 分页查询菜品(管理员后台用) * @param offset 起始行(MySQL从0开始) * @param limit 每页数量 * @return 菜品列表 */ public List<Dish> findAllWithPagination(int offset, int limit) { String sql = "SELECT id, name, price, description, status FROM dish ORDER BY id DESC LIMIT ?, ?"; return executeQueryList(sql, rs -> { Dish dish = new Dish(); dish.setId(rs.getLong("id")); dish.setName(rs.getString("name")); dish.setPrice(rs.getBigDecimal("price")); dish.setDescription(rs.getString("description")); dish.setStatus(rs.getInt("status")); // 1=上架, 0=下架 return dish; }, offset, limit); } /** * 获取总记录数(用于计算页数) */ public int getTotalCount() { String sql = "SELECT COUNT(*) FROM dish"; Long count = executeQuerySingle(sql, rs -> rs.getLong(1)); return count == null ? 0 : count.intValue(); } /** * 根据ID查单个菜品(修改前预加载) */ public Dish findById(Long id) { String sql = "SELECT id, name, price, description, status FROM dish WHERE id = ?"; return executeQuerySingle(sql, rs -> { Dish dish = new Dish(); dish.setId(rs.getLong("id")); dish.setName(rs.getString("name")); dish.setPrice(rs.getBigDecimal("price")); dish.setDescription(rs.getString("description")); dish.setStatus(rs.getInt("status")); return dish; }, id); } /** * 更新菜品(上架/下架、改价格) */ public int update(Dish dish) { String sql = "UPDATE dish SET name=?, price=?, description=?, status=? WHERE id=?"; return executeUpdate(sql, dish.getName(), dish.getPrice(), dish.getDescription(), dish.getStatus(), dish.getId()); } /** * 删除菜品(物理删除,课程设计简化处理) * ⚠️ 注意:真实系统应逻辑删除(加deleted字段),避免订单关联失效 */ public int deleteById(Long id) { String sql = "DELETE FROM dish WHERE id = ?"; return executeUpdate(sql, id); } }

参数说明与避坑:

  • LIMIT ?, ?中第一个?是offset,第二个是limit,MySQL语法要求如此。Oracle用ROWNUM,PostgreSQL用OFFSET/LIMIT,课程设计用MySQL即可。
  • getTotalCount()单独查总数,避免在findAllWithPagination里用SQL_CALC_FOUND_ROWS(已废弃)。
  • deleteById是物理删除,课程设计允许,但报告中必须注明:“生产环境应改为逻辑删除,保留历史订单关联完整性”。

4. 事务与会话:下单扣库存和生成订单必须原子执行,否则答辩被问“怎么保证数据一致性”

这是JavaEE课程设计的分水岭。90%的学生能实现“点击下单→跳转成功页”,但无法解释“如果扣库存成功、写订单失败,怎么办?”——这就是事务边界的缺失。本节用Connection.setAutoCommit(false)手动控制事务,并用HttpSession管理用户登录态。

4.1 OrderService:用Connection传递事务,拒绝跨DAO事务失控

service/OrderService.java是业务逻辑中枢,不直接操作数据库,只协调DAO:

package service; import dao.DishDAO; import dao.OrderDAO; import dao.UserDAO; import model.*; import javax.sql.DataSource; import java.math.BigDecimal; import java.sql.Connection; import java.sql.SQLException; import java.util.Date; import java.util.List; public class OrderService { private final DishDAO dishDAO = new DishDAO(); private final OrderDAO orderDAO = new OrderDAO(); private final UserDAO userDAO = new UserDAO(); /** * 创建订单(含事务控制) * @param userId 用户ID * @param dishIds 菜品ID列表(逗号分隔,如"1,3,5") * @return 订单ID 或 -1(失败) */ public long createOrder(long userId, String dishIds) { // 1. 解析菜品ID列表 String[] idStrs = dishIds.split(","); List<Long> dishIdList = new ArrayList<>(); for (String idStr : idStrs) { if (!idStr.trim().isEmpty()) { dishIdList.add(Long.parseLong(idStr.trim())); } } if (dishIdList.isEmpty()) { return -1; } Connection conn = null; try { // 2. 从数据源获取连接,并关闭自动提交 conn = DBUtil.getDataSource().getConnection(); conn.setAutoCommit(false); // 关键:开启事务 // 3. 查询菜品价格并计算总价 BigDecimal totalAmount = BigDecimal.ZERO; for (Long dishId : dishIdList) { Dish dish = dishDAO.findById(dishId); if (dish == null || dish.getStatus() != 1) { throw new RuntimeException("❌ 菜品ID " + dishId + " 不存在或已下架"); } totalAmount = totalAmount.add(dish.getPrice()); } // 4. 扣减库存(假设每道菜库存充足,课程设计简化) // 实际应查stock表并update stock set quantity = quantity - 1 where dish_id = ? // 此处省略库存表,用status模拟(上架=可售) // 5. 生成订单主表 Order order = new Order(); order.setUserId(userId); order.setTotalAmount(totalAmount); order.setStatus(1); // 1=待支付 order.setCreateTime(new Date()); long orderId = orderDAO.insert(conn, order); // 传入conn,确保同一事务 // 6. 生成订单明细 for (Long dishId : dishIdList) { OrderItem item = new OrderItem(); item.setOrderId(orderId); item.setDishId(dishId); // 查询菜品名称和价格,存入明细表(防菜品下架后订单显示null) Dish dish = dishDAO.findById(dishId); item.setDishName(dish.getName()); item.setDishPrice(dish.getPrice()); item.setQuantity(1); orderDAO.insertItem(conn, item); // 同一conn } // 7. 提交事务 conn.commit(); return orderId; } catch (SQLException e) { // 8. 回滚事务 if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); throw new RuntimeException("❌ 创建订单失败,已回滚", e); } finally { // 9. 关闭连接(注意:不close conn,由调用方或Filter管理) if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }

关键逻辑说明:

  • conn.setAutoCommit(false)是事务起点,所有后续DAO操作必须复用同一个Connection对象(通过参数传递),否则各DAO用自己dataSource.getConnection(),事务失效。
  • orderDAO.insert(conn, order)和orderDAO.insertItem(conn, item)的conn参数,确保插入主表和明细表在同一个物理连接上。
  • conn.rollback()在catch块中执行,必须放在异常处理内,否则事务不回滚,出现“订单生成了但库存没扣”的脏数据。
  • conn.close()放在finally,但注意:课程设计中连接由Tomcat池管理,close只是归还连接,非销毁。

4.2 LoginCheckFilter:用HttpSession校验登录态,拒绝Cookie伪造

util/LoginCheckFilter.java拦截未登录访问,核心是HttpSession的正确使用:

package util; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginCheckFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 1. 获取当前会话(不创建新会话) HttpSession session = request.getSession(false); // 2. 检查session是否存在且包含user属性 if (session == null || session.getAttribute("user") == null) { // 3. 未登录,重定向到登录页(非forward,避免URL暴露) String contextPath = request.getContextPath(); response.sendRedirect(contextPath + "/login.jsp"); return; } // 4. 已登录,放行 chain.doFilter(req, resp); } }

为什么用getSession(false):

  • getSession(true)(或getSession())会在session不存在时自动创建新session,导致未登录用户也有session ID,浪费资源且不安全。
  • getSession(false)只获取现有session,不存在则返回null,精准判断登录态。
  • session.getAttribute("user")存储的是model.User对象,由LoginServlet在登录成功后设置:session.setAttribute("user", user)。
  • response.sendRedirect()是客户端重定向,URL变为/login.jsp,而request.getRequestDispatcher().forward()是服务端转发,URL不变,此处必须用redirect,否则用户看到/user/order.jsp却未登录,体验诡异。

4.3 OrderServlet:接收请求、调用Service、跳转页面,三步闭环

serlvet/OrderServlet.java是下单入口,体现MVC中C(Controller)职责:

package servlet; import model.User; import service.OrderService; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; @WebServlet("/order") public class OrderServlet extends HttpServlet { private final OrderService orderService = new OrderService(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取登录用户 HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } User user = (User) session.getAttribute("user"); // 2. 获取请求参数 String dishIds = request.getParameter("dishIds"); // 前端form提交的隐藏域 if (dishIds == null || dishIds.trim().isEmpty()) { request.setAttribute("error", "请选择菜品"); request.getRequestDispatcher("/user/order.jsp").forward(request, response); return; } try { // 3. 调用Service创建订单 long orderId = orderService.createOrder(user.getId(), dishIds); // 4. 成功后跳转到订单详情页 request.setAttribute("orderId", orderId); request.getRequestDispatcher("/user/order-success.jsp").forward(request, response); } catch (RuntimeException e) { // 5. 业务异常,返回错误信息 request.setAttribute("error", e.getMessage()); request.getRequestDispatcher("/user/order.jsp").forward(request, response); } } }

流程闭环说明:

  • doPost处理表单提交,dishIds来自前端<input type="hidden" name="dishIds" value="1,2,3">,避免GET参数过长。
  • orderService.createOrder()抛出的RuntimeException被catch捕获,request.setAttribute("error", ...)将错误信息传给JSP,forward保持URL不变,用户体验好。
  • 成功后forward到order-success.jsp,显示订单号,不redirect,避免刷新重复提交。

5. 避坑指南:答辩老师最爱问的5个问题,及对应代码级解决方案

课程设计答辩不是背报告,而是看你是否真懂代码。以下5个问题,90%学生答不全,附真实代码级解决方案:

5.1 现象:登录后访问/user/order.jsp,页面显示“未登录”,F12看Network发现sessionID变了

原因:web.xml中未配置<session-config>,Tomcat默认session超时30分钟,但更致命的是cookie path不匹配。当应用部署路径不是/(如/dining-system),而JSP中response.sendRedirect("/user/order.jsp")写成绝对路径/user/order.jsp,浏览器发送cookie时path为/,服务器找不到对应session。
解决:

  1. 在web.xml中添加session配置:
<session-config> <session-timeout>60</session-timeout> <!-- 单位:分钟 --> <cookie-config> <http-only>true</http-only> <secure>false</secure> <!-- 开发环境用HTTP,生产环境设true --> </cookie-config> </session-config>
  1. 所有重定向用request.getContextPath()动态拼接:
// 错误 ❌ response.sendRedirect("/user/order.jsp"); // 正确 ✅ response.sendRedirect(request.getContextPath() + "/user/order.jsp");

5.2 现象:管理员删了菜品,用户历史订单里该菜品显示“null”

原因:订单明细表order_item只存dish_id,未冗余存储菜品名称和价格。当菜品被删,JOIN dish查不到数据,JSP中${item.dishName}为null。
解决:在OrderItem实体类中增加冗余字段,并在创建订单时写入:

// OrderItem.java private String dishName; // 冗余:下单时存菜品名 private BigDecimal dishPrice; // 冗余:下单时存当时价格 // ... getter/setter

OrderService.createOrder()中:

item.setDishName(dish.getName()); // ✅ 写入冗余字段 item.setDishPrice(dish.getPrice());

5.3 现象:并发下单时,库存扣减出错(如两单同时买最后一份,结果卖出两份)

原因:课程设计未实现库存表,但即使有,SELECT stock FROM dish WHERE id=1后UPDATE dish SET stock=stock-1存在竞态条件。
解决:用MySQL行锁+原子更新,在DishDAO中添加:

public int decreaseStock(Long dishId, int quantity) { String sql = "UPDATE dish SET stock = stock - ? WHERE id = ? AND stock >= ?"; return executeUpdate(sql, quantity, dishId, quantity); // WHERE条件确保库存足够 }

调用时检查返回值:if (rows == 0) throw new RuntimeException("库存不足");

5.4 现象:中文菜单名在JSP中显示为“???”

原因:未统一字符集。Tomcat默认ISO-8859-1,MySQL连接URL未设characterEncoding=utf8,JSP页面未声明<%@ page contentType="text/html;charset=UTF-8" %>。
解决:三处必须一致:

  1. context.xml中URL加characterEncoding=utf8:
    url="jdbc:mysql://...&characterEncoding=utf8"
  2. EncodingFilter.java中设置请求响应编码:
request.setCharacterEncoding("UTF-8"); response.setCharacter <p> <a href="https://download.csdn.net/download/weixin_67271870/85874696" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>
返回列表