ARTICLE DETAIL

资讯详情

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

JavaWeb实战:从零构建校园订餐系统,打通Servlet+MySQL全链路

JavaWeb实战:从零构建校园订餐系统,打通Servlet+MySQL全链路 简介这是一份基于JavaWeb的校园订餐系统完整项目源码定位为JavaWeb学习者的实战案例也适合作为毕业设计或课程设计的参考蓝本。压缩包共723个文件大小约9.8MB其中JSP与Java等后台程序文件构成核心业务逻辑编译产物与前端样式脚本负责页面交互展示大量图片素材用于界面装饰与菜品展示SQL数据库脚本可还原表结构及初始数据依赖库文件则确保项目正常运行整体目录结构清晰便于按模块对照学习。目前已有220人学习下载。通过研读这套代码可以理解用户登录验证、菜品查询、订单提交等典型功能的实现流程掌握Servlet请求处理、JSP页面渲染以及MySQL数据库操作的协同方式同时项目中包含的配置文件与部署描述符也有助于熟悉JavaWeb项目的标准组织方式对提升实际动手能力与毕业设计完成效率都有直接帮助。1. 校园订餐系统为什么它是被低估的JavaWeb实战项目很多人觉得校园订餐系统“太简单”不过是几个JSP页面加几个Servlet。但真正动手做过的人知道把用户注册、菜品浏览、购物车、下单支付、商家接单这一整条链路跑通你已经踩过了JavaWeb项目百分之八十的坑。这个标题里的校园订餐系统说的是用JavaWeb技术栈Servlet/JSP MySQL Maven Tomcat实现一个完整的在线订餐网站覆盖从学生登录、点餐、提交订单到商家处理订单的完整业务闭环。它适合三类人正在找JavaWeb课程设计案例源码的在校生、想验证自己后端基本功的转行者、以及需要一个完整项目来补齐MySQL与Servlet协作经验的Java初学者。这个项目的价值不在于功能多花哨而在于它足够小、足够完整能让你把一条请求从浏览器到数据库的全过程彻底看清。2. 技术选型与项目骨架先想清楚再用IDEA跑起来2.1 技术选型Servlet还是SSM校园订餐系统的三种组合做校园订餐系统最常见的做法有三条路纯Servlet JSP JDBC、Servlet MyBatis、Spring MVC Spring MyBatisSSM。三者没有绝对好坏主要看你手里有多少调试时间。技术组合代码量维护难度适合场景Servlet JSP JDBC最少高课程设计、理解HTTP与JDBC底层Servlet MyBatis中等中有Maven基础、想稳一点SSM框架多低为实习或就业准备、有充足排错时间我一般建议课程设计选方案1或2。方案1的好处是你能直观看到HTTP请求如何进入Servlet、SQL如何被提交到MySQL执行这是javaweb项目完整案例里最值得保留的教学价值。方案2则让你提前熟悉MyBatis的mapper映射后续迁移Spring Boot时阻力小很多。至于SSM如果你没有至少两周的余量尽量不要碰配置文件互相引用的排错会让你消耗大量时间。这里有一个关键点无论选哪种数据库交互层的连接管理不要裸写JDBC。至少接一个连接池Druid或HikariCP否则每次请求都new ConnectionTomcat在高并发下几分钟就崩。另外如果这个项目要作为JavaWeb课程设计案例源码提交答辩时老师一定会问Servlet的线程模型——Servlet默认是多线程的多个请求会并发执行同一个Servlet实例的service方法所以实例变量不能存请求相关的数据。这个点几乎每次答辩都会问到。2.2 数据库设计用户、菜品、订单、订单明细的四张核心表校园订餐系统的业务不复杂核心表就四张用户表、菜品表、订单表、订单明细表。但字段设计上有几个坑值得注意。用户表CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 密码加盐哈希, real_name VARCHAR(50) DEFAULT NULL COMMENT 收餐人姓名, phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, address VARCHAR(255) DEFAULT NULL COMMENT 默认送餐地址, role TINYINT DEFAULT 1 COMMENT 1-学生 2-商家 3-管理员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;一个常见错误是用户名不建唯一索引。校园订餐系统如果允许多个同名用户注册登录时查库就会返回多条记录Session里存的user对象到底是哪一条全看数据库返回顺序纯玄学。另外password字段别用varchar(32)存明文至少做一次MD5加盐。虽然校园项目不会有人来攻击但把这个习惯养成了面试时问到密码存储你不会心虚。菜品表、订单表和明细表的字段要对应好CREATE TABLE dish ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 菜名, price DECIMAL(10,2) NOT NULL COMMENT 单价, image VARCHAR(255) DEFAULT NULL COMMENT 图片路径, category VARCHAR(20) DEFAULT NULL COMMENT 分类快餐/饮品/主食, stock INT DEFAULT 0 COMMENT 库存, status TINYINT DEFAULT 1 COMMENT 1-上架 0-下架, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号, user_id INT NOT NULL COMMENT 下单用户, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0-待支付 1-已支付待出餐 2-已出餐 3-已送达 4-已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id INT NOT NULL AUTO_INCREMENT, order_id BIGINT NOT NULL, dish_id INT NOT NULL, price DECIMAL(10,2) NOT NULL COMMENT 下单时的快照价格, quantity INT NOT NULL DEFAULT 1, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单明细里的price存的是下单那一刻的价格快照而不是关联菜品的实时价格。菜品后来涨价用户的历史订单不能跟着变。这个细节面试官很爱问很多人没做对。另外表和表之间不要建数据库层面的外键约束课程设计里画ER图方便但实际运行中外键会让每次插入和删除都多一层约束检查订单表写入频繁时性能损耗明显。业务一致性交给service层代码去保证就好。2.3 Maven项目结构与IDEA运行配置工程结构推荐Maven单模块的标准布局IDEA里跑JavaWeb项目配置这件事很多新手卡在目录结构不对campus-order/ ├── pom.xml └── src └── main ├── java │ └── com/campus/order │ ├── servlet/ # 所有Servlet │ ├── service/ # 业务逻辑 │ ├── dao/ # 数据访问 │ ├── entity/ # 实体类 │ └── util/ # 工具类 ├── resources │ └── db.properties # 数据库配置 └── webapp ├── WEB-INF │ └── web.xml ├── css/ js/ img/ # 静态资源 └── jsp/ # 页面文件pom.xml里的核心依赖很少最常见的一套dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies逻辑说明Servlet和JSP的依赖scope必须设为provided因为Tomcat自带这两套实现。如果你把它写成compile并打进了war包部署到Tomcat会出现类冲突报错指向NoSuchMethodError排错路径非常绕。参数说明mysql-connector-java用5.1.49还是8.x取决于你的MySQL版本。MySQL 5.7用5.1.x没问题MySQL 8.0建议换8.0.x驱动否则连库时可能报Public Key Retrieval is not allowed。IDEA里运行这个项目的核心配置就三步Project SDK选JDK 1.8或11Maven用IDEA自带的即可Edit Configurations里添加Tomcat ServerDeployment选项卡把war包挂上去Application context填/campus。这个值决定JSP里相对路径的前缀我习惯填/前端代码不用带项目名前缀但如果一个Tomcat里跑多个项目还是用带名字的路径更清晰。注意IDEA里Tomcat端口被占用时去Tomcat安装目录conf/server.xml找Connector节点改port别盲目杀进程。很多教程让人改IDEA里的HTTP port但Connector上的port才是真正监听端口。另外如果你跟的是教程视频黑马javaweb笔记里推荐的目录分层和这里基本一致。核心是把Servlet、service、dao三类职责分开哪怕代码写得糙后续重构也有地方下手。3. 核心功能实现登录、购物车与订单状态流转3.1 用户登录与Session管理拦截器不是可选项登录模块是整个系统里最容易出安全漏洞的地方。常规做法是登录成功后把user对象放进HttpSession再用Filter统一拦截所有未登录请求。不要在Servlet里重复写session判断代码散一地后面加一个需要登录的接口你就得逐个人肉复制。登录Servlet的核心逻辑WebServlet(/login) public class LoginServlet extends HttpServlet { private UserService userService new UserService(); protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); User user userService.login(username, password); if (user null) { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/jsp/login.jsp).forward(req, resp); return; } HttpSession session req.getSession(); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(30 * 60); resp.sendRedirect(req.getContextPath() /jsp/index.jsp); } }逻辑说明先统一设置请求编码再取出用户名密码交给UserService校验。user为null说明查无此人或密码错误用forward转发回登录页并带出错误信息。登录成功就把user对象放进Session30分钟无操作过期然后重定向到首页。参数说明setMaxInactiveInterval的单位是秒30分钟对订餐场景合理。设太短用户选餐途中Session被清提交订单时被拦截器踢回登录页那体验就是灾难。重定向用sendRedirect参数必须拼上getContextPath()否则在带项目名的部署环境下会跳到一个不存在的路径。拦截器必须用Filter实现这是很多人第一次做JavaWeb项目遗漏的环节WebFilter(/*) public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String path request.getRequestURI(); if (path.endsWith(/login) || path.endsWith(/register) || path.startsWith(/css/) || path.startsWith(/js/) || path.startsWith(/img/)) { chain.doFilter(req, resp); return; } HttpSession session request.getSession(false); if (session null || session.getAttribute(loginUser) null) { response.sendRedirect(request.getContextPath() /jsp/login.jsp); return; } chain.doFilter(req, resp); } }逻辑说明getRequestURI返回的是带项目名的完整路径用endsWith判断Servlet映射比contains更严谨避免/loginxxx.jsp这种路径被意外放行。静态资源css/js/img要放行否则登录页加载后没有样式。参数说明request.getSession(false)是拿已有Session而不是新建一个。如果这个请求还没登录getSession(true)会强行创建一个空Session导致拦截逻辑失效。这里false是必须的。静态资源的URL前缀取决于前端怎么写如果前端写的是${ctx}/css/style.css这段判断就用startsWith(/css/)。3.2 购物车用HttpSession还是数据库表购物车有两种实现路线放HttpSession里或建shopping_cart数据库表。课程设计和绝大多数校园场景我推荐Session做购物车。用户未下单前购物车是临时数据不需要持久化存数据库要建表、写增删改查业务价值几乎为零。Session购物车的核心逻辑// 加入购物车 MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); if (cart null) { cart new LinkedHashMapInteger, Integer(); } Integer dishId Integer.valueOf(request.getParameter(dishId)); int quantity Integer.parseInt(request.getParameter(quantity)); cart.put(dishId, cart.getOrDefault(dishId, 0) quantity); session.setAttribute(cart, cart);逻辑说明cart是以菜品id为key、数量为value的Map。重复点击加入购物车时不会新增key而是把已有数量加一。购物车图标的角标数字就是所有value加总而不是size()因为不同菜品数量可能大于1。参数说明这里用LinkedHashMap而不是HashMap。HashMap本身无序你往购物车加菜的顺序在页面上foreach遍历时会错乱。用户说“我明明先加的回锅肉显示出来排在最后面”这就是HashMap的哈希顺序在作怪LinkedHashMap按插入顺序遍历体验完全不同。getOrDefault是JDK 8的方法如果你还在用JDK 7需要先判断containsKey再取值。下单结算时从Session取出cart逐条写入order_item表注意必须在同一个事务里完成。这里有个边界Session购物车有一个天然缺陷用户换设备或清除浏览器缓存购物车就没了。对校园订餐这种场景可以接受但你要在答辩时能说清楚这个取舍。3.3 订单生成与事务控制一个接口里的四条SQL下单动作涉及四步生成订单主记录、写入订单明细、扣减菜品库存、清空购物车。任何一步失败都不能留下半截数据。这里必须用事务把四步包起来。手写JDBC事务是理解事务边界最直接的方式public boolean createOrder(Order order, MapInteger, Integer cart, MapInteger, BigDecimal priceMap) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); PreparedStatement ps1 conn.prepareStatement( INSERT INTO orders (order_no, user_id, total_price, status) VALUES (?,?,?,0), Statement.RETURN_GENERATED_KEYS); ps1.setString(1, order.getOrderNo()); ps1.setInt(2, order.getUserId()); ps1.setBigDecimal(3, order.getTotalPrice()); ps1.executeUpdate(); ResultSet keys ps1.getGeneratedKeys(); int orderId 0; if (keys.next()) orderId keys.getInt(1); PreparedStatement ps2 conn.prepareStatement( INSERT INTO order_item (order_id, dish_id, price, quantity) VALUES (?,?,?,?)); for (Map.EntryInteger, Integer entry : cart.entrySet()) { ps2.setInt(1, orderId); ps2.setInt(2, entry.getKey()); ps2.setBigDecimal(3, priceMap.get(entry.getKey())); ps2.setInt(4, entry.getValue()); ps2.addBatch(); } ps2.executeBatch(); PreparedStatement ps3 conn.prepareStatement( UPDATE dish SET stock stock - ? WHERE id ? AND stock ?); for (Map.EntryInteger, Integer entry : cart.entrySet()) { ps3.setInt(1, entry.getValue()); ps3.setInt(2, entry.getKey()); ps3.setInt(3, entry.getValue()); ps3.addBatch(); } int[] updateCounts ps3.executeBatch(); for (int count : updateCounts) { if (count 0) { conn.rollback(); return false; } } conn.commit(); return true; } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } throw new RuntimeException(下单失败, e); } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }逻辑说明三个动作按顺序执行在同一个Connection上。扣库存的SQL带stock ?条件是经典的安全扣减只有当库存够用时才更新成功executeBatch返回的int数组里出现0就说明某道菜库存不足触发rollback。参数说明Statement.RETURN_GENERATED_KEYS让数据库返回自增主键用于关联明细表的外键。executeBatch()批量执行比循环里调executeUpdate快很多购物车有几十个品类时差距明显。finally里必须先setAutoCommit(true)再close因为连接池复用时如果不还原自动提交状态下一次拿到的连接可能还开着事务。还有一个细节select菜品价格必须放在事务里最好在事务开始时用SELECT price FROM dish WHERE id ? FOR UPDATE锁住菜品行。否则下单过程中管理员改了价格订单金额就对不上。这个坑在并发测试里才会暴露平时单机调功能根本看不出来。4. 避坑指南能让你半夜改代码的5个经典问题4.1 中文乱码从JSP到MySQL的全链路编码不一致现象页面上菜名显示成“????”或者“菜名变成乱码符号”提交的订单备注里的中文全部丢失。原因三个环节编码不一致。JSP页面没有指定pageEncodingTomcat读取请求参数时默认按ISO-8859-1解析MySQL连接串没指定characterEncoding三个环节各用各的编码就全乱套。解决三层同时做。JSP顶部写% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %web.xml里配置CharacterEncodingFilter强制所有请求和响应用UTF-8MySQL连接串加上characterEncodingutf8mb4。如果用Druid连接池在properties里写url加useUnicodetruecharacterEncodingutf8mb4。三步做完乱码基本绝迹。排查顺序从JSP到Filter再到数据库连接串每一步单独验证不要一次性全改然后猜是哪里修复的。4.2 连接池配置每次new Connection的代价现象一个50人的班级同时登录系统Tomcat直接卡死控制台报Too many connections或Connection refused。原因Servlet里每次请求都DriverManager.getConnection()用完直接close没有复用。MySQL默认max_connections是15150人同时操作很容易打满。解决用Druid或HikariCP接管连接。Druid的关键参数就三个initialSize、maxActive、maxWait。校园项目initialSize设5maxActive设20maxWait设5000毫秒足够。连接池不是越大越好maxActive设100在校园场景反而会因为Tomcat自身线程池不足导致连接获取超时。单机Tomcat配20个活跃连接足够支撑几百人的并发访问瓶颈通常在Tomcat线程池而不是数据库连接数。验证方法压力测试过程中用show processlist观察数据库连接数是否达到maxActive上限。4.3 JSP里的路径问题/和相对路径的三种上下文现象在IDEA里跑一切正常部署到Linux服务器后点击菜单跳404页面上的CSS和图片全部丢失。原因JSP里用了hrefcss/style.css这类相对路径。当URL从/campus/jsp/index.jsp变成/campus/dish/list时浏览器解析相对路径的基准目录变了资源请求就发到了不存在的位置。解决在JSP页面统一用绝对路径。常见做法是每个JSP开头加c:set varctx value${pageContext.request.contextPath} /然后所有资源路径写成${ctx}/css/style.css。如果你的容器上下文是/campus生成的路径就是/campus/css/style.css。这个教训我是在项目上线第一天就踩到的页面HTML源码看着没有任何问题但所有静态资源全部404。排查方法浏览器F12看Network标签凡是带两个项目名的请求都是相对路径写错了。4.4 事务失效连接复用状态下忘记还原AutoCommit现象下单接口偶尔报“Transaction already active”或“Connection is closed”订单数据出现半截——订单主表有记录明细表缺失。原因Druid连接池复用连接时上一次事务异常退出后没有把setAutoCommit(true)恢复回去下一次从池里拿到的连接仍然处于事务模式后续操作莫名其妙都被包在同一个事务里。解决finally块里必须执行setAutoCommit(true)再close。更稳妥的做法是service层方法内部用try-catch-finally同时处理rollback和restore两个动作。这里没有捷径每次从连接池拿连接都要当它是一张被之前操作污染过的白纸来对待。记住一个习惯事务代码的finally里先restore再close顺序不能反。4.5 图片上传与加载路径IDEA工作目录和Tomcat部署目录不一致现象在IDEA里上传的菜品图片显示正常打成war包部署到Linux后所有图片丢失或404。原因IDEA里Tomcat的工作目录指向项目的target目录上传文件写到了D:/workspace/campus/target/uploads而部署到独立Tomcat后相对路径变成/opt/tomcat/bin下的某处两者完全不是一个位置。解决外部存储路径不要让程序自己猜。在properties里配置upload.path/data/campus/uploads代码里读取这个配置拼接文件路径。同时Tomcat的server.xml里为这个目录加一个Context映射让/uploadsURL指向磁盘目录。自己本地写死相对路径确实爽但上线必翻车。这个问题的排查方法很简单登录服务器find / -name uploadedImg*.jpg 2/dev/null看看图片到底写到哪里去了。5. 从本地到线上部署、验证与下一步的三种玩法5.1 部署到Linux最小可行步骤用Maven打包war文件生成的campus-order.war放进Tomcat的webapps目录启动Tomcat后war会自动解压。JSP编译报了错优先看Tomcat logs目录下localhost开头的日志里面有精确到行的错误信息。部署前确认JDK和Tomcat版本匹配Java 8配Tomcat 8/9Java 11配Tomcat 9/10。版本不匹配时会出现“JSP compiler does not support the current version”这类信息看着像代码错误实际是版本问题。数据库连接串里的localhost也要改成服务器IP这个细节很多人第一次部署漏掉。5.2 压测与验证不要只看页面能打开验证工具用JMeter最直接。创建线程组50个线程循环10次对登录接口和菜品列表接口做并发请求。关注P95响应时间和错误率两个指标。P95超过2秒先看数据库慢查询日志再看菜品查询有没有走索引最后才考虑调连接池数量。这个排查顺序能省下大量时间。另外可以用curl -I检查静态资源响应头是否正常确认服务器上CSS和图片的Content-Type没有变成text/html。5.3 两种延伸改造做完这个项目两个常见延伸方向一是把Servlet替换为Spring Boot的Controller数据访问层从JDBC换成MyBatis项目直接从JavaWeb升级到更接近企业开发的体系简历上可以写的东西多不少。二是加一个简单管理后台菜品上下架和订单状态修改通过管理端接口操作而不是改数据库。这两个方向的业务逻辑都已经在这个源码里了改造成本不高收获实实在在。我自己第一次做完校园订餐系统后留了一个习惯每次遇到部署环境不一致导致的404和乱码都先想清楚是请求路径、编码还是存储位置的问题再动手改代码。这个项目最值得投入的地方不是功能做得多花哨而是把一条HTTP请求从浏览器到MySQL的完整链路彻底弄明白。希望帮到你。本文还有配套的精品资源点击获取
返回列表