原生Servlet+JDBC点餐系统:从请求路由到事务处理的完整实战解析

发布时间:2026/9/28 20:33:53
原生Servlet+JDBC点餐系统:从请求路由到事务处理的完整实战解析 简介基于MVC开发模式的原生Servlet与JDBC点餐系统完整项目面向Java Web学习者、毕业设计与课程设计人群可用于理解经典三层协作在真实业务中的落地方式。压缩包共139个文件包含21个jsp页面、6个java源码、6个class编译文件、7个jar依赖库、1个sql数据库脚本以及多个jpg截图、html页面和文档说明覆盖前端展示、后端控制、数据库交互与项目配置等完整环节包体仅3.76MB便于快速下载和部署。资源围绕Servlet生命周期、MVC分层、HttpSession会话管理、用户认证、JDBC持久化操作、异常处理等关键知识点展开菜单管理、点餐下单、订单处理等业务模块清晰适合对照源码逐层拆解也可作为毕业设计或课设改造基础。已有61人学习下载对于想通过完整项目提升Java Web实战能力的开发者来说是一份可以直接运行并二次开发的高性价比参考资料。1. 点餐系统与原生ServletJDBC为什么还要自己造轮子点餐系统这类CRUD项目现在用Spring Boot加个MyBatis-Plus半天就能搭出个能跑的后台。但如果你还在学Java Web或者正准备课设答辩基于MVC开发模式开发原生ServletJDBC服务器的点餐系统才是真正把「请求怎么进来、数据怎么查出来、页面怎么拼回去」这三件事串起来的项目。它没有框架帮你包办一切路由自己配、连接自己管、事务自己提交回滚每一行代码都落在你可见的范围里。说这个标题在讲什么一个典型的MVC三层点餐系统控制层用Servlet接收请求模型层用JDBC操作MySQL视图层用JSP渲染菜品列表、购物车和订单页。你不需要依赖Spring容器不需要ORM映射整个请求生命周期从Tomcat启动到ResultSet关闭你都能亲手控制。这套东西看起来老但Servlet的生命周期、JDBC的Connection管理、MVC的职责划分恰恰是后面理解Spring MVC和连接池的地基。这篇笔记适合三类人正在做Java Web课程设计的在校生想补Servlet和JDBC底层细节的初级开发以及需要给团队出教学demo的技术组长。我会顺着「启动入口到路由分发」「三层架构怎么切」「核心业务代码怎么写」「JDBC为什么总出幺蛾子」「跑通后怎么验证和进阶」的顺序把整个系统的落地路径一次讲透。2. 从web.xml到DispatcherServlet请求路由是MVC的入口2.1 为什么入口不直接挂一堆Servlet点餐系统的功能点不算少用户登录、菜品列表、菜品详情、加入购物车、提交订单、历史订单查询。如果每个功能配一个Servletweb.xml里会膨胀出十几个servlet和servlet-mapping配置类之间的跳转逻辑也会散落在各个Servlet里。常见做法是做一个DispatcherServlet作为前端控制器把「请求要做什么」和「谁来执行」解耦开。前端控制器不是Spring MVC的专利原生Servlet项目一样可以这样玩。你在web.xml里只注册一个Servlet让它拦截*.do或者/api/*这类统一后缀的请求然后在Servlet内部按request参数或路径去分发。好处是所有的权限校验、编码设置、异常兜底都有了一个统一入口。这个项目的web.xml核心配置大概长这样?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 display-nameOrderingSystem/display-name filter filter-nameEncodingFilter/filter-name filter-classcom.ordering.web.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping servlet servlet-nameDispatcherServlet/servlet-name servlet-classcom.ordering.web.servlet.DispatcherServlet/servlet-class load-on-startup1/load-on-startup /servlet servlet-mapping servlet-nameDispatcherServlet/servlet-name url-pattern*.do/url-pattern /servlet-mapping welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list /web-app这里把编码过滤器放在最前面拦截所有URL解决了POST请求中文参数乱码的根问题——POST参数在请求体里必须调用request.setCharacterEncoding()才能正确解码而且要在读第一个参数之前调用。load-on-startup设成1保证Tomcat启动时DispatcherServlet就被实例化而不是等第一个请求来了才创建。原生Servlet的默认实例化时机是懒加载如果你在init方法里做了数据源初始化懒加载会让第一个请求特别慢。2.2 DispatcherServlet里怎么分发请求Servlet的service()方法会自动根据请求方法调用doGet()或doPost()。这里有个细节业务分发不应该按GET和POST分而是按「操作类型」分。点餐系统里同样的/order.do带上actionadd是加购带上actionsubmit就是提交订单。DispatcherServlet的核心逻辑就是解析action参数找到对应的Handler类执行完再转发到对应的JSP视图。WebServlet(name DispatcherServlet, urlPatterns *.do) public class DispatcherServlet extends HttpServlet { private MapString, Action actionMap new HashMap(); Override public void init() { // 一个key对应一种业务动作 actionMap.put(login, new LoginAction()); actionMap.put(listDish, new ListDishAction()); actionMap.put(addCart, new AddCartAction()); actionMap.put(submitOrder, new SubmitOrderAction()); actionMap.put(listOrders, new ListOrdersAction()); // 处理静态资源或未知action actionMap.put(404, new NotFoundAction()); } Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { handle(req, resp); } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) { handle(req, resp); } private void handle(HttpServletRequest req, HttpServletResponse resp) { try { req.setCharacterEncoding(UTF-8); String action req.getParameter(action); if (action null || action.trim().isEmpty()) { action listDish; } Action target actionMap.getOrDefault(action, actionMap.get(404)); // 每个Action返回一个视图路径由Dispatcher统一转发 String view target.execute(req, resp); if (view ! null) { req.getRequestDispatcher(view).forward(req, resp); } } catch (Exception e) { throw new ServletException(请求处理失败, e); } } }这里用了一个Action接口作为所有业务处理的统一抽象execute()返回视图名称。这种设计的好处是DispatcherServlet永远不关心具体业务新增一个功能只需要加一个Action类和一个map项不需要动Servlet本体。使用WebServlet注解之后web.xml里其实可以不再显式声明DispatcherServlet但项目压缩包里通常还会保留web.xml因为JSP、welcome-file和Filter还是习惯在上面配。两种方式并存不冲突注解负责Servlet注册web.xml负责全局配置。参数说明handle方法里先做编码再取参数顺序不能反getRequestDispatcher用的是服务端转发不是sendRedirect。转发是同一个请求内跳转request里的attribute和参数都能带到JSP重定向会丢失。点餐系统里购物车数据要跨页面保留所以业务Action里把数据放到request的attribute里JSP再通过EL表达式取这里必须用forward。3. MVC三层架构在点餐系统里的具体切分3.1 Model层为什么不只是数据库代码MVC里的Model层最容易被理解成DAO数据访问对象但其实点餐系统里Model层至少包含三块实体类JavaBean、DAO接口和实现、Service业务逻辑。实体类对应数据库表结构DAO只负责增删改查Service负责业务规则——比如提交订单时要同时扣库存、算总价、生成订单记录这三步要么全成功要么全失败这就是Service层该管的。菜品实体的写法很直接字段跟数据库表一一对应public class Dish { private int id; private String name; private double price; private String category; private String imagePath; private int stock; public int getId() { return id; } public void setId(int id) { this.id id; } // 其余getter/setter省略 }为什么字段要用包装类型或基本类型这里建议价格用double还是BigDecimal值得单独说明。课程设计里用double问题不大但真实支付场景里金额计算会出现精度丢失比如0.1加0.2不等于0.3。在点餐系统这种练习项目里统一用double省事代码也直观如果你打算之后扩展成支付模块可以提前改用BigDecimal数据库字段用DECIMAL(10,2)对应。DAO层的典型写法是把连接获取放在一个JDBCUtil工具类里DAO实现类负责拼SQL、填参数、遍历ResultSet。Service层不出现任何SQL只调用DAO接口。这样切的好处是如果哪天你想把JDBC换成MyBatisDAO实现类重写Service完全不动。这就是MVC里Model层的扩展性价值也是这个项目值得做细的原因。3.2 View层用JSP还是纯HTML加Ajax这个点餐系统标题里写的是「原生ServletJDBC」没提前后端分离所以视图层常规走JSP加EL表达式加JSTL标签。菜品列表页的JSP模板大概是这样的逻辑Servlet查询出ListDish放进request的attribute叫dishListJSP用c:forEach循环渲染成表格或卡片。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title菜品列表/title /head body h2今日菜单/h2 table border1 tr th编号/th th菜名/th th价格/th th操作/th /tr c:forEach items${dishList} vardish tr td${dish.id}/td td${dish.name}/td td${dish.price}/td td a hrefcart.do?actionadddishId${dish.id}加入购物车/a /td /tr /c:forEach /table /body /htmlJSP里尽量不要写Java脚本片段% %这是很多老项目的通病。脚本片段会让页面里混入业务代码改样式时误伤逻辑而且JSP编译报错时定位困难。用EL表达式取数据、用JSTL做循环和判断页面结构干净职责也清晰。EL表达式找不到属性时不会抛异常输出空字符串这个特性在调试时容易掩盖问题——所以回头排查问题时优先看Servlet有没有把数据set进request。如果你觉得JSP这套太老也可以让Servlet直接输出JSON、前端用Fetch渲染。但这会偏离标题里「原生ServletJDBC」的学习目标因为后面你会发现前后端分离意味着要额外处理跨域、JSON序列化、异步错误处理这些复杂度对理解MVC没有帮助。我的建议是先按JSP把这套跑通再考虑改成Ajax版本作为进阶练习。3.3 点餐系统的表结构设计MVC落地的第一步不是写代码而是把表结构定下来。点餐系统的核心表至少有三张用户表、菜品表、订单表。订单和菜品是多对多关系所以还需要一张订单明细表。表结构设计直接影响DAO的写法和Service的事务边界先把这几张表的字段想清楚后面能少改很多代码。用户表userid、username、password、phone、create_time。密码字段注意长度如果之后想存MD5或SHA256加密串varchar(32)或varchar(64)才够。菜品表dishid、name、price、category、stock、image_path。商品价格用DECIMAL(10,2)在MySQL里精确存金额Java侧用BigDecimal接收练习项目用double也行但接口文档里一定要注明单位是元。订单表ordersid、user_id、total_price、status、create_time。status字段建议用int加注释说明0代表待付款、1代表已支付、2代表已出餐不要直接存中文不然SQL排序和分组会很别扭。订单明细表order_itemid、order_id、dish_id、quantity、price_snapshot。price_snapshot是在下单那一刻把菜品价格抄进来防止菜品涨价后历史订单的金额跟着变。这个字段容易被漏掉但对于订单系统来说是基础设计不是进阶优化。4. 核心业务落地从登录到下单的完整链路4.1 登录模块LoginAction与DAO查询登录是整个系统第一个要写的完整功能它的链路是JSP表单提交到user.do?actionloginDispatcherServlet分发给LoginActionLoginAction调用UserServiceUserService调UserDAO查询数据库验证通过后把用户信息塞进Session跳转到菜品列表页。public class LoginAction implements Action { Override public String execute(HttpServletRequest req, HttpServletResponse resp) throws Exception { String username req.getParameter(username); String password req.getParameter(password); UserService userService new UserService(); User user userService.login(username, password); if (user ! null) { // 登录成功把用户对象存进Session HttpSession session req.getSession(); session.setAttribute(currentUser, user); return dish.do?actionlistDish; } else { // 登录失败回到登录页并携带错误信息 req.setAttribute(loginError, 用户名或密码错误); return /login.jsp; } } }登录成功后的跳转dish.do?actionlistDish用的是重定向写法——前面我说过getRequestDispatcher是转发但这里Action里return的是完整URL路径DispatcherServlet怎么处理呢这里有一个设计约定如果返回的字符串以redirect:开头DispatcherServlet就执行sendRedirect否则执行forward。上面的dish.do...不带前缀但因为它不是以/开头的JSP路径DispatcherServlet也可以约定成统一forward到该路径。最稳妥的做法是回到DispatcherServlet里加一段判断if (view.startsWith(redirect:)) { resp.sendRedirect(req.getContextPath() view.substring(redirect:.length())); } else { req.getRequestDispatcher(view).forward(req, resp); }登录之后Session里有了currentUser菜品列表页就可以用${sessionScope.currentUser.username}显示当前登录人。这里埋了个常见的坑如果用户绕过登录页直接访问dish.doAction里需要先判断Session是否存在当前用户不存在就重定向回login.jsp。这个可以在DispatcherServlet里做一个全局的登录校验拦截所有需要登录的action比在每个Action里重复判断要省事。4.2 购物车逻辑Servlet里操作Session还是数据库购物车有两种实现方案纯内存存Session和持久化存数据库表。点餐系统这种练习项目用Session存购物车是合理且省事的做法。原因很简单购物车本身是临时数据用户关闭浏览器就消失是符合期望的如果每次加购都要写数据库购物车表还得跟用户表关联涉及删除、合并、过期清理复杂度翻倍。购物车的对象结构用一个Map存最合适key是菜品IDvalue是购买数量。放在Session里加入购物车的Action长这样public class AddCartAction implements Action { Override public String execute(HttpServletRequest req, HttpServletResponse resp) throws Exception { int dishId Integer.parseInt(req.getParameter(dishId)); HttpSession session req.getSession(); MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); if (cart null) { cart new HashMap(); } cart.merge(dishId, 1, Integer::sum); session.setAttribute(cart, cart); return redirect:dish.do?actionlistDish; } }Map.merge是Java 8的写法第一参数是key第二参数是默认值第三个参数是合并函数表示「原来有值就加1没有就放1」。Session存购物车的数据结构不涉及数据库IO加购操作响应极快这个体验比每次都查库要顺。要展示购物车列表时Action里拿Session中的Map按菜品ID批量查数据库查出菜品名称和价格再组装成展示列表。这就是为什么购物车里不直接存价格——价格是易变数据以DB为准Session里只存ID和数量。如果直接把名称、价格都塞进Session用户打开购物车时看到的价格可能和下单时不一致因为你没有重新查库。4.3 下单事务Connection要自己控制commit和rollback提交订单是这个项目里唯一涉及事务的功能也是最能体现JDBC功力的地方。伪代码逻辑是这样创建订单记录orders表、逐条插入订单明细order_item表、扣减菜品库存dish表。三步中任何一步失败前面已经插入的数据都要回滚否则会出现「订单主表存在但明细缺失」的脏数据。JDBC里实现事务的要点是同一个Connection对象关闭自动提交执行完所有SQL后统一commit异常时rollback。这个Connection绝对不能是每次DAO调用各自获取一个否则每张表各用各的连接事务根本串不起来。常见做法是在Service层获取Connection传给DAO层的方法使用或者在DAO层加一个带Connection参数的重载方法。public class OrderService { public void submitOrder(int userId, MapInteger, Integer cart) throws Exception { Connection conn null; try { conn JDBCUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 第一步插入订单主表 OrderDAO orderDAO new OrderDAO(); int orderId orderDAO.insertOrder(conn, userId, calculateTotal(cart)); // 第二步插入订单明细表 OrderItemDAO itemDAO new OrderItemDAO(); for (Map.EntryInteger, Integer entry : cart.entrySet()) { itemDAO.insertOrderItem(conn, orderId, entry.getKey(), entry.getValue()); } // 第三步扣减库存 DishDAO dishDAO new DishDAO(); for (Map.EntryInteger, Integer entry : cart.entrySet()) { int rows dishDAO.reduceStock(conn, entry.getKey(), entry.getValue()); if (rows 0) { throw new RuntimeException(库存不足菜品ID: entry.getKey()); } } conn.commit(); // 全部成功提交 } catch (Exception e) { if (conn ! null) { conn.rollback(); // 任何一步失败全部回滚 } throw e; } finally { JDBCUtil.close(conn, null, null); } } private double calculateTotal(MapInteger, Integer cart) { // 需要查询菜品价格再累加这里省略 return 0.0; } }这里有个执行顺序的陷阱先查库存再插入订单明细和先插入明细再扣库存两者并发时的表现不一样。建议先查库存数量并锁定行再执行插入最后扣减时再次校验。上面代码的写法是先插明细后扣库存如果库存不足触发回滚所有明细一起消失业务上没问题但数据库里会短暂出现无效的明细数据——有经验的开发者会在同一事务里把「预扣库存」和「校验」放在插入明细之前。练习项目按简单顺序写可以但要有这个意识。事务提交后要清空Session里的购物车否则用户刷新页面会看到购物车还是满的。这个清理动作应该放在事务成功之后而不是之前——事务还没提交购物车数据不能丢万一回滚用户还能重新尝试。4.4 菜品管理DAO的增删改查与PreparedStatement后台管理模块如果有的话需要菜品的增删改查。JDBC的PreparedStatement是这一块的核心它在预编译阶段就把SQL骨架固定住参数用占位符替换既避免SQL注入又不需要每次执行都重新编译SQL。以新增菜品为例public int insertDish(Connection conn, Dish dish) throws SQLException { String sql INSERT INTO dish (name, price, category, stock, image_path) VALUES (?, ?, ?, ?, ?); PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); ps.setString(1, dish.getName()); ps.setBigDecimal(2, dish.getPrice()); ps.setString(3, dish.getCategory()); ps.setInt(4, dish.getStock()); ps.setString(5, dish.getImagePath()); int rows ps.executeUpdate(); // 取出数据库自增的主键 ResultSet rs ps.getGeneratedKeys(); if (rs.next()) { dish.setId(rs.getInt(1)); } rs.close(); ps.close(); return rows; }为什么用prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)而不用普通的prepareStatement(sql)因为插入订单主表后下一步要拿这个orderId去插明细表而订单主表的主键是自增的。如果不用这个参数你得在插入后重新按用户和时间查一次订单表既多一次查询还可能查错。RETURN_GENERATED_KEYS是JDBC标准行为不依赖MySQL特性和方言。参数说明setBigDecimal对应数据库的DECIMAL字段setString对应VARCHAR。如果菜品的imagePath是NULL应该调用ps.setNull(5, Types.VARCHAR)而不是setString(5, null)后者在某些驱动版本下会报「Column image_path cannot be null」但报错时机很迷惑去排查时容易被带偏。另外一定不要用Statement拼SQL字符串那等于把SQL注入漏洞写进作业里答辩时被问到会很难看。5. JDBC连接与Servlet运行环境避坑从报错到跑通的排查笔记5.1 现象一ClassNotFoundException: com.mysql.jdbc.Driver这是从零跑这个点餐系统时出现频率最高的报错。原因是MySQL驱动版本和类名不匹配MySQL 5.x驱动类名是com.mysql.jdbc.DriverMySQL 8.x驱动类名改成了com.mysql.cj.jdbc.Driver。如果你用的mysql-connector-java版本是8.0以上却在代码里写旧类名JVM在加载驱动时自然找不到。解决方式一行字把Class.forName(com.mysql.jdbc.Driver)改成Class.forName(com.mysql.cj.jdbc.Driver)。但更省事的做法是连Class.forName都不用写——JDBC 4.0开始驱动包里的META-INF/services/java.sql.Driver文件会自动完成注册。前提是你把驱动的jar包放进了WEB-INF/lib目录并且IDEA或Eclipse里确认它能被编译期看到。另一个连带问题是项目里直接放了一个mysql-connector-java-5.1.47.jar但本地MySQL是8.x版本。驱动旧、数据库新连接时大概率报Communications link failure。建议去Maven中央仓库搜mysql-connector-j的8.x版本下载对应jar替换。这类驱动jar的版本不一致问题在第1章热词里「idea自动下载from maven failed」也是同一类场景——Maven下载失败通常是网络或仓库源的问题换成阿里云镜像源即可解决。5.2 现象二Access denied for user rootlocalhost驱动类和连接串都对了但连接时报用户权限被拒绝。先别急着改代码用命令行工具直接测一下MySQL账户mysql -uroot -p如果命令行能进而JDBC不能大概率是密码里有特殊字符。JDBC连接串里密码包含、#、%这类字符时会被URL解析器截断或转义导致密码识别错误。解决方式是对特殊字符做URL编码要写成%26。比如原始密码是abc123连接串里要写成jdbc:mysql://localhost:3306/ordering?passwordabc%26123。另一个坑是MySQL 8.x默认的认证插件是caching_sha2_password而mysql-connector-java 5.x只支持mysql_native_password。如果驱动版本旧、数据库认证插件新会报Unable to load authentication plugin。低成本的解决方式是把账户认证方式改回去ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;这一步能用命令行工具在MySQL里直接执行不需要动Java代码。不过更推荐直接换用8.x的驱动jar因为caching_sha2_password本身安全性更高不需要为了迁就旧驱动而降低数据库侧的账号安全标准。5.3 现象三Server returns invalid timezoneMySQL 8.x安装后默认时区不是UTC而JDBC驱动会从连接串里读取serverTimezone参数。连接串不写时区、数据库也没配全局时区就会在DriverManager.getConnection()这一步抛出The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。注意看乱码这是中文时区在GBK编码下被当成非UTF-8序列导致的。解决方式两种一是在连接串里显式指定时区jdbc:mysql://localhost:3306/ordering?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai二是修改MySQL全局时区SET GLOBAL time_zone 08:00;连接串里的useUnicodetruecharacterEncodingUTF-8这两个参数要一起出现。useUnicodetrue告诉驱动按Unicode协议处理字符流characterEncodingUTF-8指定传输编码。两个参数缺一个中文数据在插入和查询时可能会变成问号或乱码。这不是玄学是编码协商流程没走完的表现。5.4 现象四JDBC连接泄漏导致系统卡死点餐系统跑起来之后如果隔一段时间刷新页面就越来越慢最后Tomcat报Connection is not available, request timed out这是连接泄漏的典型症状。原因通常是DAO方法里获取了Connection但异常路径上没有关闭或者ResultSet和Statement关闭了、Connection没关。MySQL默认的连接超时时间是8小时连接被数据库侧断开后连接池里的引用还认为它活着。虽然原生JDBC没有连接池但你如果用了Tomcat自带的JNDI DataSource或者自己写了个简单的连接池这个问题就会出现。最常见的原因还是获取连接的代码写在方法里、关闭连接的代码放在了finally块之外public User findUserByUsername(String username) { Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn JDBCUtil.getConnection(); // 查询逻辑 } catch (SQLException e) { e.printStackTrace(); return null; } finally { // finally里才关闭缺了这一步就泄漏 JDBCUtil.close(conn, ps, rs); } return user; }JDBCUtil.close这个方法里关闭的顺序有讲究先关ResultSet再关PreparedStatement最后关Connection。反过来先关Connection虽然也能释放但某些驱动版本下会报底层socket关闭异常。JDBCUtil工具的关闭方法建议写成重载形式接收任意数量的AutoCloseable参数用try-with-resources释放代码会干净很多。5.5 现象五JSP页面能打开但图片全部显示不出来菜品列表页能正常渲染但菜品图片全部裂开。先检查image_path字段的值再检查图片文件实际放的目录。常见的坑是图片存在了/WEB-INF/images/目录下但浏览器的URL是http://localhost:8080/ordering/images/dish1.jpg。Tomcat默认不允许浏览器直接访问WEB-INF目录下的资源这是Servlet规范里的安全约定。解决方式是两种选一图片移到WebContent目录下的images/文件夹直接暴露或者在web.xml里配一个DefaultServlet映射。移目录更简单直接适合练习项目。如果你不想把图片跟JSP混在一起也可以配一个虚拟目录映射在Tomcat的server.xml里加Context。但对课程设计来说把图片放在webapp/images/下就够了连接串里的路径要跟实际部署目录对应上。这个坑的隐蔽之处在于本地用IDEA内置Tomcat跑时image_path里的绝对路径和部署后的相对路径经常不一致。建议在数据库里存相对路径比如images/dish1.jpgJSP里用${pageContext.request.contextPath}拼完整前缀这样项目挪到哪都不会裂图。6. 验证与进阶跑通之后怎么确认你的点餐系统不是纸面工程给这套点餐系统做完体检比继续堆功能更重要。第一步验证是看请求链路是否完整。浏览器打开开发者工具的Network面板依次执行登录、加购、下单观察请求顺序user.do?actionlogin、dish.do?actionlistDish、cart.do?actionadd、order.do?actionsubmit。如果任何一个请求返回404或500先看Tomcat的localhost日志——java.lang.ClassNotFoundException说明缺jar包或类名不对java.sql.SQLException说明SQL或连接有问题NullPointerException优先检查request参数名和JSP里的name属性是否一致。第二层验证是数据库侧。下单成功后登录MySQL查询SELECT * FROM orders ORDER BY create_time DESC LIMIT 5; SELECT * FROM order_item WHERE order_id 1; SELECT * FROM dish WHERE id IN (1, 2, 3);重点确认三件事订单总价小数位是否正确、订单明细里的价格快照是否等于下单时菜价、库存扣减是否生效。事务的验证方法是故意制造一个失败场景——把某个菜品的库存改成0再从前端下单看一下orders表和order_item表是否出现了半截数据。如果出现说明你的事务根本没生效回去检查Service层是否真的用了同一个Connection以及setAutoCommit(false)之后是否正确回滚。进阶方向我建议按这三步走第一步是把JDBC直连换成Druid或HikariCP连接池这是你理解连接池原理之后的自然延伸也顺便能解决第5.4节的连接泄漏问题第二步是把JSP里的EL和JSTL彻底用熟然后试着把菜品列表页改成Ajax异步加载JSONServlet里用Fastjson或Jackson输出JSON、前端用Fetch渲染体验一下MVC里V层换实现的成本第三步是给密码加盐哈希存储数据库里不要存明文密码。到这一步你手里的项目已经从「能跑通的课程设计」进化成「具备基础工程素养的作品」。我当年第一次跑类似的Servlet项目卡在时区报错整整一个下午最后发现是连接串少写了serverTimezoneAsia/Shanghai。后来养成的习惯是先确认MySQL版本和驱动版本匹配再写代码顺序颠倒会浪费大量排错时间。希望这篇笔记能帮你把那些坑提前填上把精力花在真正值得研究的事务和分层设计上——那才是这个项目对你最大的价值。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询