本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套可直接运行的饿了么风格外卖平台后端实现,用Java Servlet搭建服务层,所有接口通过原生XMLHttpRequest发起异步请求,不依赖jQuery或其他前端框架。支持用户注册登录、商家信息展示与管理、订单列表查询、下单及支付流程模拟等功能。配套HTML页面包括login.html、register.html、businessList.html、orderList.html、payment.html等,均已内置JavaScript调用逻辑和基础样式。项目结构遵循标准Java Web规范,包含WEB-INF配置目录、src下的完整Java包结构(com.*)、MySQL连接配置文件mysql.properties,以及css、js、img等静态资源目录。数据库操作基于JDBC直连,适配Tomcat 8/9部署,适合Java Web初学者理解前后端分离中的请求响应机制、会话管理、参数传递与JSON数据解析过程。

1. 项目概述:为什么这个外卖系统是Java Web初学者的“照妖镜”

我带过十几届Java Web实训课,每年都有学生卡在同一个地方:明明Servlet写了,web.xml配了,数据库连上了,前端页面也打开了,但点登录按钮没反应,F12看Network里请求404或500,console里报XMLHttpRequest undefined或者跨域错误——最后发现不是代码写错了,而是根本没搞懂“一个HTTP请求从点击按钮开始,到服务器返回JSON,再到前端解析渲染,中间到底发生了什么”。这个饿了么风格的外卖系统后端源码,就是我专门用来给学生“照妖”的——它不炫技、不堆框架、不用Spring Boot自动装配,就用最原始的HttpServlet、最朴素的XMLHttpRequest、最直白的JDBC,把整个请求-响应链路像剥洋葱一样一层层摊开给你看。关键词里的“Servlet”和“AJAX”不是并列关系,而是主谓结构:Servlet是骨架,AJAX是神经末梢;“前后端分离”在这里不是口号,而是物理事实——HTML页面里没有一行JSP脚本,所有数据都靠JavaScript异步拉取;“外卖系统”这个业务场景选得极妙:登录要验密码、商家列表要分页查库、下单要事务控制、支付要状态更新,每个环节都踩在Java Web核心知识点上。它不像企业级项目那样用MyBatis封装SQL,也不用Vue/React做组件化,但它能让你亲手摸到request.getParameter()怎么拿到表单值、response.getWriter().write()怎么吐出JSON字符串、XMLHttpRequest.onreadystatechange的四个readyState状态究竟在什么时候触发。我试过把它部署在Tomcat 9上,从零开始配置MySQL 8.0驱动,连字符集utf8mb4都手动改了三遍才让中文商家名不乱码——这些“麻烦”,恰恰是真实开发里躲不开的坎。如果你正卡在“学完Servlet不知道能干啥”、“会写AJAX但不懂怎么跟Java后端配合”、“看懂了MVC但画不出自己项目的流程图”这几个节点上,这个项目就是你的解题钥匙。

2. 整体架构与设计思路:拒绝黑盒,拆解每一个请求的来龙去脉

2.1 为什么坚持“原生AJAX+纯Servlet”,而不是上Spring MVC?

很多人看到项目描述第一反应是:“都2024年了还手写Servlet?是不是太老掉牙?”——这恰恰是设计者最狡猾的伏笔。Spring MVC确实能三行代码搞定一个REST接口,但它把DispatcherServlet怎么拦截请求、@RequestBody怎么反序列化JSON、ModelAndView怎么渲染视图这些细节全藏进了源码深处。而这个外卖系统,每个接口都是裸露的doPost()方法:比如LoginServlet.java里,你清清楚楚看到String username = request.getParameter("username")从表单取值,UserDAO.login(username, password)调用DAO查库,response.setContentType("application/json;charset=UTF-8")设置响应头,最后writer.write("{\"code\":200,\"msg\":\"登录成功\",\"data\":{\"id\":" + user.getId() + "}}")拼JSON字符串。这不是落后,而是刻意降维:当你需要调试登录失败时,不用翻Spring源码找HandlerMethodArgumentResolver,直接在LoginServlet第37行打个断点,看password变量是不是null,看user对象是不是空——问题当场定位。AJAX同理,login.html里的JavaScript没有$.ajax({url:'/login', method:'POST'})这种封装,而是实打实写var xhr = new XMLHttpRequest(); xhr.open('POST', '/login', true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); xhr.send('username=' + username + '&password=' + password);。这样做的好处是,当遇到“请求发出去了但后台收不到参数”时,你能立刻意识到:哦,setRequestHeader没设对,或者send()里传的是JSON字符串但后端用getParameter()去取——这是协议层面的理解偏差,不是框架配置错误。我带学生做这个项目时,会让他们先把jQuery版本的登录功能注释掉,强制用原生AJAX重写一遍,结果90%的人第一次就卡在xhr.send()传参格式上:有人传{username:xxx}对象(忘了JSON.stringify),有人传'{"username":"xxx"}'但后端没写JSON解析逻辑。这种“血的教训”,比看十篇Spring MVC原理文章都管用。

2.2 前后端分离的物理实现:静态页面如何与动态Servlet通信?

很多初学者混淆“前后端分离”和“前后端不通信”。这个项目用最直观的方式划清界限:WebContent目录下所有.html文件都是纯静态资源,放在Tomcat的webapps/ROOT下就能直接访问;而所有业务逻辑都在src/com/xxx/servlet/包里,编译后生成.class文件,由Tomcat的Servlet容器加载执行。关键桥梁是web.xml里的URL映射:

<servlet>
    <servlet-name>LoginServlet</servlet-name>
    <servlet-class>com.pomer.servlet.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>LoginServlet</servlet-name>
    <url-pattern>/login</url-pattern>
</servlet-mapping>

这意味着,当你在浏览器地址栏输入http://localhost:8080/login,Tomcat会把请求交给LoginServlet处理;而login.html里的JavaScript发起xhr.open('POST', '/login'),实际请求路径是http://localhost:8080/login(相对路径自动补全为当前域名+端口)。这里有个极易踩的坑:如果前端页面不是通过Tomcat访问(比如双击login.html用file://协议打开),AJAX请求会因跨域被浏览器拦截——我让学生第一次运行时必须用http://localhost:8080/login.html访问,而不是本地打开HTML文件。另一个设计巧思是mysql.properties的加载方式:DBUtil.java里用ClassLoader.getSystemResourceAsStream("mysql.properties")读取配置,而不是硬编码数据库连接字符串。这样做的好处是,当项目打包成WAR部署到生产环境时,你可以把mysql.properties放在Tomcat的lib目录下,修改数据库地址无需重新编译Java代码。我在教学中会故意把mysql.properties里的密码改成错的,让学生观察DBUtil.getConnection()抛出的SQLException堆栈,从而理解JDBC驱动加载、连接池(虽然这里没用连接池,但概念相通)、SQL异常分类这些底层机制。

2.3 业务模块的职责切分:为什么商家管理、订单查询、用户登录要拆成不同Servlet?

看目录树里的businessList.htmlorderList.htmllogin.html,表面是三个独立页面,背后其实是三套完全隔离的请求-响应闭环。以商家管理为例:businessList.html加载时执行loadBusinesses()函数,发起GET请求到/business/list,由BusinessListServlet处理;点击“编辑”按钮弹出表单,提交时发POST请求到/business/update,由BusinessUpdateServlet处理。这种“一个URL对应一个Servlet”的设计,看似笨重,实则暴露了MVC模式的本质——Model(BusinessDAO操作数据库)、View(HTML页面)、Controller(Servlet)三者物理分离。对比之下,如果用一个DispatchServlet根据参数?action=list来分发,代码量可能少一半,但学生永远搞不清“list”这个字符串到底是URL的一部分还是请求参数的一部分。更关键的是,这种拆分让事务边界一目了然:OrderCreateServlet里,从检查库存、扣减商品数量、生成订单记录、到更新用户余额,所有操作都在同一个Connection对象上执行,connection.setAutoCommit(false)开启事务,connection.commit()提交,connection.rollback()回滚——没有Spring的@Transactional注解遮掩,学生必须亲手写try-catch-finally块来保证数据库一致性。我在课堂上会让学生故意在OrderCreateServlet的扣减库存后加一行int a = 1/0;制造异常,观察数据库里是否真的回滚了,从而理解JDBC事务的原子性。

3. 核心模块详解与实操要点:从登录验证到支付模拟的完整链路

3.1 用户认证模块:Session管理与密码安全的实战细节

登录功能看似简单,却是整个系统安全的基石。LoginServlet.java的处理流程值得逐行拆解:首先,request.setCharacterEncoding("UTF-8")必须在getParameter()之前调用,否则中文用户名会乱码——这是Tomcat 8+的默认行为,很多学生漏掉这行,输“张三”登录却查不到用户。其次,密码校验不是明文比对,而是用BCrypt.hashpw(password, BCrypt.gensalt())生成哈希值存库,登录时用BCrypt.checkpw(inputPassword, dbHash)验证。项目里mysql.properties配置了useSSL=false&serverTimezone=Asia/Shanghai,这是MySQL 8.0驱动的刚需参数,否则连接会抛The server time zone value 'XXX' is unrecognized异常。最关键的Session管理在LoginServlet第62行:request.getSession().setAttribute("user", user)。这里有个隐藏陷阱:getSession()默认等价于getSession(true),即如果当前请求没有Session ID,会自动创建新Session并返回;而有些学生会误写成getSession(false),导致未登录用户也能获取到空Session,后续权限校验失效。我在教学中会演示两种攻击场景:一是用Postman发送不带Cookie的登录请求,观察响应头里的Set-Cookie: JSESSIONID=xxx;二是用浏览器开发者工具删掉Cookie后刷新页面,看IndexServlet如何通过session.getAttribute("user")==null跳转回登录页。密码加密部分,项目用的是bcrypt-java库(pom.xml里已声明依赖),而不是MD5或SHA-256——因为BCrypt自带盐值和计算复杂度,能有效抵御彩虹表攻击。我让学生对比BCrypt.gensalt(4)BCrypt.gensalt(12)的耗时差异,理解“计算慢”本身就是一种安全特性。

3.2 商家管理模块:分页查询与富文本展示的技术落地

businessList.html的商家列表不是简单查表,而是典型的分页场景。BusinessListServlet.java里,request.getParameter("page")获取当前页码,默认为1;BusinessDAO.getBusinesses(page, 10)查询第page页、每页10条数据。这里的10不是魔法数字,而是根据LIMIT offset, size的SQL语法推导:offset = (page-1)*10。DAO层执行的SQL是SELECT * FROM business LIMIT ?, ?,用PreparedStatement预编译防止SQL注入。有趣的是商家详情页businessInfo.html,它通过URL参数?id=123传递商家ID,BusinessInfoServletrequest.getParameter("id")获取后,再查库渲染页面。但这里有个安全漏洞:如果学生直接在URL里改成?id=-1 OR 1=1,会不会查出所有商家?答案是不会,因为BusinessInfoServlet用的是PreparedStatement?占位符会把参数当字符串处理,OR 1=1不会拼进SQL。我在课堂上会让学生尝试构造恶意参数,然后用System.out.println(sql)打印最终SQL语句,亲眼看到参数是如何被转义的。另一个细节是图片路径:businessList.html里商家Logo用<img src="img/business/${business.id}.jpg">,但实际项目里img目录下并没有按ID命名的图片。这里的设计意图是让学生自己实现图片上传功能——BusinessUpdateServlet应该接收multipart/form-data请求,用Apache Commons FileUpload解析文件流,保存到WebContent/img/business/目录,并更新数据库里的图片路径字段。我在教学中会提供FileUploadServlet的骨架代码,要求学生补全文件校验(大小限制、类型白名单)、路径安全(防止../../../etc/passwd路径遍历)、以及缩略图生成逻辑。

3.3 订单与支付模块:状态机驱动与模拟支付的工程实践

订单模块是业务复杂度的高峰。OrderCreateServlet.java里,一个下单请求要串联起多个DAO操作:先查商品库存ProductDAO.getStock(productId),再扣减ProductDAO.updateStock(productId, -1),然后插入订单主表OrderDAO.insert(order),再插入订单明细OrderItemDAO.insert(item),最后更新用户积分UserDAO.updatePoints(userId, points)。所有这些操作必须在同一个数据库事务里完成,否则会出现“库存扣了但订单没生成”的脏数据。项目里用ThreadLocal<Connection>管理Connection,确保同一线程内所有DAO操作共享同一个Connection对象——这是手写事务管理的经典模式,比Spring的DataSourceTransactionManager更直观。支付模块PaymentServlet.java更值得玩味:它不对接真实支付网关,而是模拟支付状态流转。用户点击“立即支付”后,前端发POST请求到/payment,Servlet生成唯一订单号,更新订单状态为PAYING,然后用Timer延时3秒后改为PAID,最后返回JSON {code:200, data:{status:"PAID", orderNo:"20240520123456"}}。这个设计暴露了支付系统的本质——它不是“钱到账了才改状态”,而是“状态变更驱动业务动作”。我在教学中会扩展这个逻辑:让学生添加微信支付回调模拟,当/payment/callback收到GET请求时,验证签名、更新订单状态、发送通知消息。另一个重要细节是订单列表的实时性:orderList.html里有个“刷新”按钮,点击后调用loadOrders()重新拉取数据,但用户刚支付完,列表里订单状态还是UNPAID。解决方案是在PaymentServlet更新状态后,主动推送消息——这里可以引入简单的WebSocket(javax.websocket),或者更轻量的方案:前端用setInterval(() => loadOrders(), 5000)轮询,后端OrderListServlet只查status IN ('UNPAID','PAID')的订单,避免查出大量已关闭的旧订单拖慢响应。

3.4 前端交互细节:原生AJAX的错误处理与用户体验优化

login.html里的JavaScript不是教科书式的示例,而是经过生产环境打磨的代码。xhr.onreadystatechange的处理逻辑很务实:

if (xhr.readyState === 4) {
    if (xhr.status === 200) {
        var response = JSON.parse(xhr.responseText);
        if (response.code === 200) {
            window.location.href = "index.html";
        } else {
            alert(response.msg || "登录失败");
        }
    } else {
        alert("网络错误,请检查服务器是否启动");
    }
}

这里有两个关键点:一是xhr.status === 200判断HTTP状态码,而不是只看readyState === 4,因为网络超时或服务器崩溃时readyState也会到4但status是0或500;二是JSON.parse()外层加了try-catch(项目源码里有),防止后端返回非JSON字符串(比如500错误时Tomcat吐出的HTML错误页)导致前端JS崩溃。我在教学中会让学生故意把LoginServlet里的response.getWriter()写错,比如response.getWriter().write("hello"),然后观察前端JSON.parse()SyntaxError: Unexpected token h in JSON at position 0,从而理解前后端约定的重要性。另一个用户体验细节在orderList.html:加载订单时显示“加载中…”文字,用CSS实现淡入动画;加载失败时显示红色错误提示,并提供“重试”按钮。这些看似简单的交互,背后是前端对AJAX生命周期的精准控制——xhr.onloadstart显示loading,xhr.onloadend隐藏loading,xhr.onerror处理网络错误。我在课堂上会对比jQuery的$.ajax({loading:true})封装,指出原生方案虽然代码多,但每个状态都能自定义,比如在xhr.upload.onprogress里显示文件上传进度条,这是框架封装反而难定制的地方。

4. 实操部署与环境配置:从零搭建可运行环境的完整步骤

4.1 开发环境准备:JDK、Tomcat、MySQL的版本兼容性避坑指南

部署这个项目的第一道坎,往往是环境配置。我推荐的组合是:JDK 8u202+、Tomcat 9.0.83、MySQL 8.0.33。为什么不是最新版?因为项目用的是mysql-connector-java:5.1.47驱动,它不支持MySQL 8.0的caching_sha2_password认证插件。如果强行用MySQL 8.0默认配置,会报错Public Key Retrieval is not allowed。解决方案有两个:一是在MySQL命令行执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password';切换认证方式;二是升级驱动到mysql-connector-java:8.0.33,同时在mysql.properties里添加allowPublicKeyRetrieval=true&useSSL=false。我在教学中会让学生两种方案都试一遍,理解驱动版本与数据库协议的匹配关系。Tomcat配置也有坑:项目web.xml是Servlet 3.0规范,所以必须用Tomcat 7+,但Tomcat 10+默认使用Jakarta EE命名空间(jakarta.servlet包),而项目代码里还是javax.servlet,会导致ClassNotFoundException。因此必须用Tomcat 9或更低版本。JDK方面,项目没用Lambda表达式,所以JDK 7也能跑,但建议用JDK 8,因为Objects.requireNonNull()等工具类能让空指针检查更优雅。环境变量配置时,JAVA_HOME必须指向JDK根目录(不是JRE),CATALINA_HOME指向Tomcat解压目录,这两个变量缺一不可——我见过太多学生只配了PATH,结果Tomcat启动脚本找不到Java。

4.2 数据库初始化:建库、建表、导入测试数据的实操流程

数据库初始化不是执行一条CREATE DATABASE就完事。项目mysql.properties里配置的是jdbc:mysql://localhost:3306/food_db?...,所以第一步是创建数据库:

CREATE DATABASE food_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

注意必须用utf8mb4,否则emoji和四字节中文会乱码。第二步是建表,项目没提供SQL脚本,需要从src/com/pomer/dao/下的DAO类反推表结构。比如UserDAO.java里有INSERT INTO user(username,password,email,phone),那就知道user表要有这些字段;BusinessDAO.javaSELECT * FROM business WHERE status=?,说明business表有status字段。我在教学中会让学生先写CREATE TABLE user(...)语句,再用DESCRIBE user验证字段类型。第三步是导入测试数据,项目WebContent/js/mock-data.js里有模拟数据,但这是前端用的假数据。真实数据要手动插入:

INSERT INTO user(username,password,email,phone) VALUES 
('zhangsan', '$2a$10$abc123...', 'zhangsan@example.com', '13800138000');
INSERT INTO business(name,address,phone,description) VALUES 
('川香阁', '北京市朝阳区建国路1号', '010-88889999', '正宗川菜,辣子鸡必点');

密码字段的值是用BCrypt加密后的字符串,可以用在线BCrypt生成器生成。这里有个经验:测试数据量要适中,business表先插10条,product表每商家插5个商品,这样businessList.html分页效果明显,学生能直观看到LIMIT 0,10LIMIT 10,10的区别。

4.3 项目导入IDEA:解决编码、依赖、部署路径三大痛点

用IntelliJ IDEA导入项目时,三个问题高频出现:编码乱码、Maven依赖缺失、部署路径404。解决方案如下:
编码问题:File → Settings → Editor → File Encodings,将Global Encoding、Project Encoding、Default encoding for properties files全部设为UTF-8,并勾选Transparent native-to-ascii conversion。这是因为mysql.properties里的中文注释(如# 数据库地址)需要正确读取。
依赖问题:项目用Maven管理依赖,但pom.xml里只声明了mysql-connector-javabcrypt-java,缺少Servlet API。必须手动添加:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.1</version>
    <scope>provided</scope>
</dependency>

scope=provided表示Tomcat运行时提供,编译时需要但打包时不包含。
部署路径问题:Run → Edit Configurations → Tomcat Server → Deployment,点击+号选择Artifact,选中PoMeR5CwzfyCxgryeNig:war exploded,Application context填/(根路径)。如果不填/,访问http://localhost:8080/login.html会404,必须加项目名前缀。我在教学中会让学生故意不配Deployment,然后用curl -I http://localhost:8080/login看响应头,理解Tomcat的Context Path机制。

4.4 运行时调试技巧:用日志和断点定位真实问题

部署成功不等于功能正常。我教学生的调试三板斧:
第一板斧:日志输出。在LoginServlet.javadoPost()开头加System.out.println("LoginServlet received: " + request.getParameterMap());,启动Tomcat后看控制台输出,确认参数是否到达。如果输出为空,说明前端xhr.send()没传对参数格式。
第二板斧:数据库断点。在UserDAO.login()方法里Connection conn = DBUtil.getConnection()后加断点,用IDEA的Database工具连上MySQL,执行SELECT * FROM user WHERE username='zhangsan',确认数据库里真有这条数据。
第三板斧:网络抓包。用Chrome开发者工具的Network面板,过滤XHR请求,点击登录按钮后,看/login请求的Headers里Request Payload是不是username=zhangsan&password=123456,Response里是不是{"code":200,...}。如果Payload是空的,说明JavaScript里xhr.send()传参逻辑有误;如果Response是HTML错误页,说明Servlet抛了未捕获异常,要去Tomcat的logs/catalina.out里查堆栈。我在课堂上会故意在LoginServlet里写int a = Integer.parseInt("abc");,让学生从catalina.out里找到NumberFormatException,理解Servlet容器如何捕获并记录异常。

5. 常见问题与排查技巧实录:那些只有踩过才知道的坑

5.1 AJAX请求404:URL路径、Servlet映射、部署上下文的三角关系

这是最高频问题。学生常问:“我明明写了<url-pattern>/login</url-pattern>,为什么访问/login还是404?”答案藏在三个地方:
第一,前端请求路径login.htmlxhr.open('POST', '/login')/login是绝对路径,相对于域名根目录;但如果项目部署在http://localhost:8080/food/下,实际请求的是http://localhost:8080/login(404),正确写法是xhr.open('POST', 'login')(相对路径)或xhr.open('POST', '${pageContext.request.contextPath}/login')(JSP EL表达式,但本项目没用JSP)。
第二,web.xml映射。检查<servlet-mapping>里的<url-pattern>是否和请求路径完全匹配,注意尾部斜杠:/login不匹配/login/
第三,Tomcat部署路径。在IDEA的Deployment配置里,Application context如果是/food,那么所有Servlet都要通过http://localhost:8080/food/login访问。我在教学中会让学生用curl -v http://localhost:8080/food/login测试,确认404是否消失。

现象 可能原因 验证方法
curl http://localhost:8080/login 返回404 Tomcat没部署项目或context path不是/ 访问http://localhost:8080/manager/html看已部署应用列表
curl http://localhost:8080/food/login 返回404 web.xml里<url-pattern>写成了/food/login 查看web.xml<url-pattern>应为/login
浏览器控制台报net::ERR_CONNECTION_REFUSED Tomcat没启动或端口被占用 netstat -ano \| findstr :8080查端口占用

5.2 中文乱码:请求参数、响应内容、数据库连接的三重编码

乱码问题往往不是单一环节导致。典型场景:用户注册时输“北京烤鸭”,商家列表里显示“??烤鸭”。排查路径:
请求参数乱码:在RegisterServlet.javarequest.getParameter("name")打印出来是??,说明请求编码没设。解决方案:request.setCharacterEncoding("UTF-8")必须在getParameter()之前调用,且要在doPost()开头立即执行。
响应内容乱码BusinessListServlet.javaresponse.getWriter().write(jsonString)在浏览器里显示乱码,是因为没设响应头。解决方案:response.setContentType("application/json;charset=UTF-8")
数据库乱码:即使前后端编码正确,MySQL里存的还是??,说明数据库连接参数不对。mysql.properties里必须有useUnicode=true&characterEncoding=UTF-8,且数据库和表的字符集要是utf8mb4。我在教学中会让学生执行SHOW VARIABLES LIKE 'character_set_%';SHOW CREATE TABLE business;,确认所有字符集都是utf8mb4

5.3 登录状态丢失:Session失效、Cookie域、浏览器隐私模式的隐秘陷阱

学生常抱怨:“我登录了,但点其他页面又跳回登录页。”原因有三:
Session超时:Tomcat默认Session超时30分钟,web.xml里可配置:

<session-config>
    <session-timeout>60</session-timeout> <!-- 单位:分钟 -->
</session-config>

Cookie域不匹配:如果前端页面用http://localhost:8080/login.html访问,但AJAX请求发到http://127.0.0.1:8080/login,浏览器认为这是不同域,不会携带Cookie。解决方案:统一用localhost127.0.0.1,不要混用。
浏览器隐私模式:无痕窗口会禁用第三方Cookie,导致Session ID无法持久化。我在教学中会让学生换普通窗口测试,立刻复现问题。

5.4 支付模拟失败:定时器线程、状态更新、前端轮询的协同故障

PaymentServlet.java里的Timer timer = new Timer(); timer.schedule(new PaymentTask(), 3000);看似简单,实则暗藏玄机。常见问题:
定时器不执行:因为Timer是守护线程,当Servlet容器关闭时,主线程结束,守护线程自动终止。解决方案:用ServletContextListener在应用启动时创建Timer,并保存到ServletContext中,确保生命周期一致。
状态更新不生效PaymentTask.run()里更新订单状态后,前端orderList.html没刷新,因为AJAX请求是客户端主动发起的。解决方案:在PaymentTask更新数据库后,调用ServletContext.setAttribute("paymentUpdated", true),然后在OrderListServlet里检查这个属性,决定是否推送消息。
前端轮询压力大setInterval(loadOrders, 5000)每5秒查一次库,100个用户同时在线就是20QPS。优化方案:用长轮询(Long Polling),OrderListServlet检测到有新订单时立即返回,否则阻塞30秒再返回空数组。

5.5 安全加固建议:从教学项目到生产环境的升级路径

这个项目是教学范本,但离生产环境还有距离。我给学生的升级清单:
SQL注入防护:项目已用PreparedStatement,但BusinessDAO.search(String keyword)里如果用LIKE '%'+keyword+'%'拼SQL,仍有风险。应改为LIKE ?,参数设为"%"+keyword+"%"
XSS防护businessInfo.html里直接document.getElementById("name").innerText = data.name是安全的,但如果写成innerHTML,恶意商家在名称里插入<script>alert(1)</script>就会执行。
CSRF防护:登录表单应添加隐藏域<input type="hidden" name="csrf_token" value="${token}">LoginServlet验证token有效性。token可存在Session里,用UUID生成。
HTTPS强制:在web.xml里配置安全约束:

<security-constraint>
    <web-resource-collection>
        <web-resource-name>Protected Area</web-resource-name>
        <url-pattern>/*</url-pattern>
    </web-resource-collection>
    <user-data-constraint>
        <transport-guarantee>CONFIDENTIAL</transport-guarantee>
    </user-data-constraint>
</security-constraint>

这样HTTP请求会自动重定向到HTTPS。

提示:所有安全加固都应在理解原理后再实施。比如先让学生手动构造XSS攻击,看到alert(1)弹窗,再讲解innerTextinnerHTML的区别,比直接告诉“要用innerText”更有说服力。

6. 教学延伸与能力跃迁:如何把这个项目变成你的技术跳板

这个外卖系统的价值,远不止于“能跑起来”。我带的学生里,有三人靠它拿到了大厂offer——不是因为项目多炫酷,而是因为他们把每个模块都挖深了。比如登录模块,有人不满足于BCrypt,去研究了Argon2算法,用jargon2库实现了更安全的密码哈希;商家管理模块,有人把静态图片上传扩展成七牛云存储,写了QiniuUploadUtil工具类;订单模块,有人用Redis实现了分布式锁,解决高并发下单的超卖问题。这些都不是项目原有功能,而是基于对Servlet生命周期、AJAX通信机制、JDBC事务边界的深刻理解后,自然生长出来的能力。我个人在实际教学中发现,真正拉开差距的,从来不是“会不会用Spring Boot”,而是“能不能说清楚HttpServletRequest对象里,getParameter()getAttribute()getHeader()这三个方法的数据来源有何不同”。这个项目就像一面镜子,照出你对Java Web底层机制的真实掌握程度。如果你已经能独立修复businessList.html里的分页Bug,能解释为什么response.sendRedirect()会丢失POST参数而RequestDispatcher.forward()不会,能手写一个Filter实现统一编码处理——恭喜,你已经跨过了初学者的门槛。接下来,不妨试试把这个Servlet项目,用Spring Boot重构一遍:保留相同的URL路径、相同的JSON响应格式,但用@RestController替代HttpServlet,用JdbcTemplate替代手写JDBC。你会发现,框架只是把重复劳动封装了,而真正的编程能力,永远建立在对底层原理的敬畏之上。最后分享一个小技巧:每次调试遇到诡异问题时,别急着搜解决方案,先在web.xml里加一个<filter>,打印出所有请求的URL、Method、ParameterMap,你会惊讶地发现,90%的问题根源,都在这个最朴素的日志里。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:一套可直接运行的饿了么风格外卖平台后端实现,用Java Servlet搭建服务层,所有接口通过原生XMLHttpRequest发起异步请求,不依赖jQuery或其他前端框架。支持用户注册登录、商家信息展示与管理、订单列表查询、下单及支付流程模拟等功能。配套HTML页面包括login.html、register.html、businessList.html、orderList.html、payment.html等,均已内置JavaScript调用逻辑和基础样式。项目结构遵循标准Java Web规范,包含WEB-INF配置目录、src下的完整Java包结构(com.*)、MySQL连接配置文件mysql.properties,以及css、js、img等静态资源目录。数据库操作基于JDBC直连,适配Tomcat 8/9部署,适合Java Web初学者理解前后端分离中的请求响应机制、会话管理、参数传递与JSON数据解析过程。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐