简介:这是一份基于Java+JSP+MySQL实现网上订餐系统的完整源码包,适合Java Web初学者、课程设计或毕业设计参考。源码覆盖用户登录注册、菜单展示、购物车与订单提交等核心流程,采用JSP构建视图层、Servlet处理业务请求、MySQL完成数据持久化,并融入了MVC分层设计思想。压缩包共1262个文件、47.38MB,以css样式、png/jpg图片、jsp页面、java源文件、class文件、jar依赖库、xml配置和sql脚本为主,附带数据库初始化脚本222.sql以及Eclipse/IntelliJ IDEA的工程配置,便于直接导入和调试。已有384人学习下载,有助于从项目结构、前后端交互和数据库设计中快速掌握传统Java Web开发的基础实践。
1. 一个能跑的java+jsp+mysql网上订餐系统源码,先别急着删
做Java课程设计或者毕设选题时,“网上订餐系统”是出现频率最高的题目之一。这套基于java+jsp+mysql的简单网上订餐系统源码,不是那种一行都跑不动的半成品,而是标准的Servlet+JSP三层JavaWeb项目:用户端有注册登录、菜品浏览、购物车、下单,后台有菜品管理、订单管理、用户管理,业务闭环完整。它没有Spring Boot把一切封装成黑匣子,代码量适中,正适合想搞清楚JavaWeb请求链路的人。对准备交课程设计、应付答辩、或者想顺着java面试题里的Servlet生命周期去深挖的读者,这套源码是很好的练手对象。这篇笔记会把版本配平、数据库初始化、核心代码、常见坑和扩展方向讲透,让源码在你电脑上真正跑起来。
2. 先看懂这套源码的底子:JSP+Servlet+MySQL的请求链路与三层结构
2.1 还在用JSP不是落后:课程设计选型和面试考点的双重原因
很多第一次下载这套源码的人会问:现在都Spring Boot了,为什么课程设计还在用JSP?答案不复杂:高校的java课程设计案例源码里,JSP+Servlet依然是教学主力。它把请求处理、页面渲染、数据库访问拆成三层,每一层都肉眼可见,适合考核java基础掌握程度。Spring Boot固然好,但自动配置把很多东西变成了黑匣子,答辩时老师一问DispatcherServlet和Servlet的区别,答不上来反而扣分。
更深一层,java面试题里Servlet生命周期、Session原理、Filter拦截器这些考点,恰恰只有在这种老式JSP项目里能直接看到。用框架时你只是在调用,用JSP+Servlet时你是在写它们本身。所以这套源码不是过时,而是“教学友好”。它比你在IDEA新建jsp项目后从零敲代码省事得多,也比直接抄一个Spring Boot全家桶更贴近课程考核点。对jsp入门阶段的人,先用这种项目把请求链路摸清楚,后面学框架才不会一头雾水。
2.2 跟一条请求的流转:从浏览器地址栏到MySQL再回JSP页面
拿到源码后别急着点运行,先把一条请求跟一遍。用户在浏览器输入 http://localhost:8080/ordering/food/list ,Tomcat收到后先按web.xml里的Servlet映射或@WebServlet注解找到对应的Servlet类。Servlet调DAO层查数据库,把结果放进request或session,最后转发或重定向到JSP页面。JSP做的是渲染:用EL表达式和JSTL标签把数据铺到HTML里,再返回给浏览器。
JSP的机制值得多说一句:它第一次被访问时,Tomcat会把JSP翻译成一个Servlet源码,编译成class后再执行。这也是为什么JSP页面修改后需要重新部署才生效的原因。一个规范的项目里,JSP里不应该塞满<% %>脚本片段,而是用标签库取数据。这套源码如果写法规范,你会看到类似这样的页面片段:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <body> <c:forEach items="${foodList}" var="food"> <div class="food-card"> <span>${food.name}</span> <span>¥${food.price}</span> <a href="${pageContext.request.contextPath}/cart/add?foodId=${food.id}">加入购物车</a> </div> </c:forEach> </body>这段JSP里有几个固定写法要认识。${food.name}是EL表达式,从后端塞进request的foodList里取每个对象的name属性;c:forEach是JSTL的循环标签,items是集合,var是循环变量;pageContext.request.contextPath是当前应用的上下文路径,页面里所有链接都用它拼前缀,避免部署后路径对不上。JSTL和EL是jsp入门的标配,大部分课程设计源码都会用,看到不认识不要慌,记住这三个点就够了。
Servlet生命周期在源码里也能实际看到。一个Servlet从init()到service()再到destroy(),init只执行一次,service每次请求都执行,容器负责实例化和销毁。java面试题里反复出现这个考点,而这套源码里你能用日志输出看到它真实跑一遍,比背八股文扎实。
2.3 数据表建模:用户、菜品、订单五张表怎么设计
这套订餐系统的核心表通常是五张,最少也是四张。拿到SQL脚本后先打开看一眼,表结构大致如下:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| user | 用户表 | id, username, password, phone, address |
| food | 菜品表 | id, name, price, image, description, category |
| orders | 订单主表 | id, user_id, total_price, create_time, status |
| order_item | 订单明细表 | id, order_id, food_id, food_name, price, count |
| cart | 购物车表(可选) | id, user_id, food_id, count |
订单拆成主表和明细表是这套设计最值得学的地方。一个订单包含多个菜品,如果只放一张表,每个菜品一行都要重复存用户id、总价、地址,冗余严重,而且“这个订单是一个整体”的概念会丢失。拆成两张表后,orders表存订单整体,order_item表存每个菜品的明细行。建表脚本通常长这样:
CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, create_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 1 ); CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, food_id INT NOT NULL, food_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, count INT NOT NULL, KEY idx_order (order_id) );注意order_item里存了food_name和price这两个冗余字段,这是刻意设计:菜品价格和名称以后可以改,但已下单的记录必须保留下单那一刻的快照,否则后台一改价,历史订单的金额就跟着变,对账会乱。这类细节就是答辩时能说出来的加分点。status字段用TINYINT存数字,1待处理、2已接单、3已完成,比直接存字符串省空间,也方便扩展。如果源码表结构里没有cart表,说明购物车存在Session里,第四章会细讲两种方案的区别。
2.4 拿到源码先扫这三个目录,再点运行不迟
解压zip后,先按这个顺序扫一遍源码结构。第一是src下的Java源码包,一般按工具类、数据库访问、控制器分层,用IDEA左侧项目树数一下有几个Servlet,就能判断系统有哪些功能。第二是web目录下的WEB-INF,里面是web.xml,这是老式JSP项目的入口配置。Servlet映射、过滤器、欢迎页都在这里,加新页面时也要改它。web.xml核心内容一般是:
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="3.1"> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <filter> <filter-name>LoginFilter</filter-name> <filter-class>com.ordering.filter.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>welcome-file-list指定访问根路径时默认打开哪个页面,filter-mapping把一个过滤器挂到全站路径上。第三是SQL文件目录,一般叫sql或database,里面是建库建表脚本。这一眼扫完,系统能做什么、代码放哪里、数据库怎么初始化就有了底。很多人在这一步省掉,直接点运行,结果数据库没导入、驱动没配置,报一堆错还以为源码有问题,其实只是顺序不对。
3. 把源码跑起来:JDK、Tomcat、MySQL版本配平与数据库初始化
3.1 三件套版本怎么配:JDK 8 + Tomcat 8.5 + MySQL 5.7 的搭配逻辑
老式JSP项目对版本敏感,最稳的组合是JDK 1.8配Tomcat 8.5配MySQL 5.7。不是新版本不行,而是老源码在编译和运行时的坑最少。java环境变量配置如果还没做过,先确认JDK装好后JAVA_HOME指向安装目录,PATH里追加了%JAVA_HOME%\bin,命令行敲java -version能输出版本号再往下走。
| 组件 | 推荐版本 | 原因 |
|---|---|---|
| JDK | 1.8 | 老代码按JDK8语法编译,避免模块化带来的兼容问题 |
| Tomcat | 8.5.x | 对应Servlet 3.1规范,javax.servlet命名空间,老代码零改动 |
| MySQL | 5.7 | 与mysql-connector-java 5.1.49驱动长期配合稳定 |
| IDEA | 2019以上 | Community版也能跑,不需要Ultimate |
| 数据库客户端 | Navicat或MySQL Workbench | 导入SQL、看表结构都方便 |
这里有个血泪经验:如果机器上已经装了MySQL 8.0,不要急着卸载,驱动换成8.0.x版本,JDBC连接串加useSSL=false和allowPublicKeyRetrieval=true,大多数源码也能跑。真正容易翻车的是Tomcat版本,比如Tomcat 10把javax.servlet改成jakarta.servlet,老项目的import语句全部失效,这个问题在5.4节详细说。
提示:这套源码里引用的mysql-connector-java驱动jar包,一般在WEB-INF/lib目录下。如果没有,去Maven仓库下载5.1.49版本放进去,版本宁老勿新。
3.2 在IDEA里导入源码并启动Tomcat:一步步配下来
导入步骤按顺序做,缺一步项目就起不来。第一步,File -> Open选中解压后的源码目录,IDEA会自动识别项目类型。如果打开后没看到Web模块,右键项目根目录选Add Framework Support,勾选Web Application。第二步,打开Project Structure(Ctrl+Alt+Shift+S),在Artifacts选项卡里点加号,选Web Application: Exploded,把项目里的web目录指定为Web Resource Directory。第三步,配置Tomcat:Run -> Edit Configurations,点加号选Tomcat Server -> Local,在Server选项卡里选到本机Tomcat安装目录;然后在Deployment选项卡点加号添加刚才建的Artifact,Application context默认是/xxx,建议改成/ordering,这样访问URL和源码里JSP的上下文路径能对上。
最后点运行,浏览器访问 http://localhost:8080/ordering 。如果IDEA里没有Tomcat选项,先确认Run配置面板的加号菜单里找得到Tomcat Server,找不到说明IDEA版本太老或安装的是精简版。Community版也自带Tomcat集成,出现“部署成功但页面404”时先别怀疑代码,去Tomcat的logs/catalina.out日志看一眼实际报错。
这一步的逻辑要搞清楚:IDEA不负责编译JSP,它只是把项目打成war包或exploded目录交给Tomcat,真正跑网站的是Tomcat的catalina进程。理解了这一点,后面所有部署类问题都能按“容器日志优先”的思路排查,而不是盯着代码反复看。
3.3 数据库初始化:Navicat导入SQL脚本的两条路
按mysql安装配置教程装好MySQL后,先确认服务启动。用Navicat连本地MySQL,新建一个数据库,名字和源码里的连接串保持一致,通常是ordering。然后右键数据库选“运行SQL文件”,选中源码附带的.sql脚本,执行完刷新表列表,看到user、food、orders这些表就成功了。
不想用图形客户端的话,命令行也很快。先建库再导入:
CREATE DATABASE IF NOT EXISTS ordering DEFAULT CHARACTER SET utf8mb4; USE ordering; SOURCE /绝对路径/ordering.sql;然后验证导入结果:
SHOW TABLES; SELECT * FROM user LIMIT 5;看到返回记录,说明数据表结构和初始数据都导入成功。如果脚本里没有CREATE DATABASE语句,说明建库动作要你自己做,直接运行脚本会报No database selected。还有一点:脚本里如果有DROP TABLE语句,执行时会把旧表删掉重建,所以千万别在正式数据库上乱跑课程设计脚本,本地测试环境无所谓。
3.4 数据库连接配置集中在这几处,改错一处就全断
老式JSP项目的数据库连接配置一般在src目录下的db.properties里,或者直接写在DBUtil.java的常量里。找到后改成你自己的数据库用户名密码,统一格式:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/ordering?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你的密码对应的DBUtil读取配置的代码通常是这样的:
public class DBUtil { private static String url; private static String username; private static String password; static { try { // 读取db.properties里的连接参数 Properties props = new Properties(); InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("db.properties"); props.load(in); url = props.getProperty("jdbc.url"); username = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); // 加载驱动类,只做一次 Class.forName(props.getProperty("jdbc.driver")); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } // 每次需要连接时调用,用完必须关闭 public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } }这段代码的逻辑:静态代码块在类第一次被加载时执行,读配置和加载驱动只做一次,后续请求不再重复。getConnection每次返回一个物理连接,调用方用完要close。url里的参数每个都有用:useUnicode和characterEncoding解决中文乱码;useSSL=false关掉MySQL 8.0的SSL握手警告;serverTimezone=Asia/Shanghai是MySQL 8.0的时区要求,不写会报时区无法识别的错。如果源码里把连接串写死在代码里而不是properties文件,也要同步改这里。
4. 核心模块拆解:登录、购物车、下单与后台管理的可复用写法
4.1 用户登录Servlet:从表单取值到Session写入
登录模块最容易抄出问题,因为涉及请求编码、密码校验、Session写入、跳转方式四个环节。典型的登录Servlet是这样写的:
@WebServlet("/login") public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // POST请求必须先设置编码,否则中文参数乱码 req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); UserDao dao = new UserDao(); User user = dao.findByUsernameAndPassword(username, password); if (user != null) { // 登录成功:用户对象放进session,后续请求都能取到 HttpSession session = req.getSession(); session.setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/index.jsp"); } else { // 登录失败:用请求转发带错误提示,注意不是重定向 req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("login.jsp").forward(req, resp); } } }这段代码有两个面试常考的点藏在里面。一是sendRedirect和forward的区别:重定向是浏览器再发一次新请求,地址栏会变,request里的属性会丢;转发是服务器内部直接派发,地址栏不变,request.setAttribute带过去的值还能读到。二是session和request的生命周期:session是会话级别,浏览器不关就还在;request是单次请求级别,用完即销毁。所以登录用户放session,错误提示放request,不是随便放的。
检查源码时重点看DAO层的findByUsernameAndPassword是怎么写的。规范写法是PreparedStatement预编译加参数绑定,防止SQL注入;如果看到字符串拼接SQL的写法,答辩时可以主动指出这个安全改进点。翻一翻user表存密码的字段,如果存的是明文,也可以作为“我会加盐哈希加密”的改进方向讲。
4.2 用Filter拦未登录请求:一个拦截器挡掉所有页面
购物车、下单、后台管理这些页面,没登录绝对不能访问。老项目里最常见的偷懒做法是在每个Servlet里复制粘贴一段session判断,更好的写法是一个过滤器配一次URL规则,全站生效:
@WebFilter("/*") public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 放行登录注册页、静态资源、首页 if (uri.contains("login") || uri.contains("register") || uri.endsWith(".css") || uri.endsWith(".js") || uri.endsWith("index.jsp")) { chain.doFilter(req, resp); return; } // 未登录:跳回登录页 if (request.getSession().getAttribute("loginUser") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }过滤器的拦截规则用/*表示所有请求先进这一关。chain.doFilter是链式调用的关键:当前过滤器完成自己的逻辑后,把请求交给下一个过滤器或目标Servlet,这一行不调用,请求就断在这里。这个模式在java面试题里叫FilterChain,在Spring里叫拦截器,原理是同一套。判断放行名单时,如果源码里有/admin开头的后台路径,注意一定要单独要求管理员身份,而不是只登录就行。
4.3 BaseDao把JDBC重复代码收拢:三种通用方法
订餐系统里菜品查询、用户查询、订单写入都要用JDBC,每个DAO都写一遍Connection、PreparedStatement、ResultSet,代码会膨胀到没法维护。常见做法是先写一个BaseDao把重复逻辑收拢:
public class BaseDao { // 通用增删改:传入SQL和可变参数,返回受影响行数 public int update(String sql, Object... params) { try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("数据库更新失败", e); } } // 通用查询:配合RowMapper把结果集转成对象列表 public <T> List<T> query(String sql, RowMapper<T> mapper, Object... params) { List<T> list = new ArrayList<>(); try (Connection conn = DBUtil.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(mapper.map(rs)); } } } catch (SQLException e) { throw new RuntimeException("数据库查询失败", e); } return list; } // 结果集映射接口 public interface RowMapper<T> { T map(ResultSet rs) throws SQLException; } }这里有几个参数细节值得说。Object... params是可变参数,传入顺序必须和SQL里?占位符的顺序一一对应,这是新手最容易踩的坑,多传一个少传一个都会在setObject时下标越界。try-with-resources语法保证Connection、PreparedStatement、ResultSet在方法结束时自动关闭,不会把数据库连接泄漏光。RowMapper是一个函数接口,真正的DAO实现里会写rs.getInt("id")、rs.getString("name")这样的映射逻辑。如果源码用的还是手写连接的旧写法,也别急着嫌弃,那恰好说明教学性强。理解了BaseDao的封装思路,以后换成mysql的数据库连接池或MyBatis,思想是同一套。
4.4 购物车到底存Session还是存数据库:两种选型的取舍
加购物车是订餐系统的核心交互,设计上有经典分歧:购物车数据放Session还是放数据库表。这套简单源码通常用Session方案,因为实现成本最低:
// 从Session取购物车,Map的key是菜品id,value是数量 HttpSession session = req.getSession(); Map<Integer, Integer> cart = (Map<Integer, Integer>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<Integer, Integer>(); session.setAttribute("cart", cart); } int foodId = Integer.parseInt(req.getParameter("foodId")); cart.put(foodId, cart.getOrDefault(foodId, 0) + 1); // 加购完成跳回菜品列表 resp.sendRedirect(req.getContextPath() + "/food/list");Session方案的优点是开发快、不用建表、不用单独做购物车记录的增删改查;缺点是用户清Cookie或换浏览器购物车就没了,Session里对象存多了还占服务器内存。如果源码表结构里有cart表,那就是数据库方案:登录后把购物车记录写入数据库,用户随时回来数据都还在。
两种方案在答辩时都能讲出道理,关键是说清楚“为什么这样选”。简单的订餐系统用Session够用,下单即清空,购物车不是长期数据;做生产级商城才考虑落库,或者两者结合:未登录先用Session,登录后合并进数据库表。
提示:Session方案里,购物车Map的key是Integer类型的菜品id,从request取参数时用Integer.parseInt转一次,不转会报NumberFormatException。这类类型转换异常是老项目里出现频率极高的运行时错误。
4.5 下单事务:订单主表和明细表必须一起成功
下单是整个系统里唯一必须保证数据一致性的操作。如果订单头写进了orders表,明细写order_item时失败,用户会看到一个没有内容的订单,这就是脏数据。解决办法是手动控制事务:
Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,后续操作合并成一个事务 // 1. 插入订单主表,拿回自增主键 PreparedStatement ps1 = conn.prepareStatement( "INSERT INTO orders(user_id, total_price, create_time, status) VALUES(?,?,?,?)", Statement.RETURN_GENERATED_KEYS); ps1.setInt(1, userId); ps1.setBigDecimal(2, totalPrice); ps1.setTimestamp(3, new Timestamp(System.currentTimeMillis())); ps1.setInt(4, 1); // 1表示新订单待处理 ps1.executeUpdate(); int orderId = 0; ResultSet rs = ps1.getGeneratedKeys(); if (rs.next()) orderId = rs.getInt(1); // 2. 循环写入订单明细,用批量执行减少网络往返 PreparedStatement ps2 = conn.prepareStatement( "INSERT INTO order_item(order_id, food_id, food_name, price, count) VALUES(?,?,?,?,?)"); for (CartItem item : cartItems) { ps2.setInt(1, orderId); ps2.setInt(2, item.getFoodId()); ps2.setString(3, item.getFoodName()); ps2.setBigDecimal(4, item.getPrice()); ps2.setInt(5, item.getCount()); ps2.addBatch(); } ps2.executeBatch(); conn.commit(); // 全部成功才提交,两张表一起生效 } catch (SQLException e) { if (conn != null) conn.rollback(); // 任何一步失败,全部回滚 throw new RuntimeException("下单失败,订单已回滚", e); } finally { if (conn != null) conn.close(); }这个写法有三个关键点。setAutoCommit(false)是事务开关,打开后JDBC不再每执行一条SQL就自动提交,而是等最后的commit统一提交。getGeneratedKeys用于拿自增主键,订单明细必须引用这个新生成的orderId。addBatch和executeBatch是批量操作,把多条INSERT合并成一次数据库往返,数据量大时性能差距明显。rollback要包在catch里,任何一条SQL失败,前面已经执行的所有写入全部撤销。
提示:事务必须在同一个Connection上执行,中途不能换连接。这也是为什么下单方法要自己拿连接,而不是调用4.3节BaseDao里每次新建连接的便捷方法。
理解了下单事务,java面试题里“怎么保证数据一致性”这类问题就有了实例答案。这套源码里可能只有下单用到事务,但把这一处讲透,比背十遍框架事务注解都管用。
5. 常见的翻车现场:环境、编码、JDBC与部署避坑排查清单
5.1 中文全变问号:请求和响应两个方向都要设UTF-8
现象:页面标题、用户注册的昵称、订单里的菜品名全部显示成???,查看数据库里存的也是问号。
原因:中文乱码有三个独立环节,任何一个掉了都会翻车。一是HTTP请求参数没解码,Tomcat默认按ISO-8859-1处理POST参数;二是Servlet响应没指定编码,浏览器按默认编码解析;三是JDBC连接串没带characterEncoding参数,驱动写入数据库时按错误编码转换。
解决:按链路上三个点逐一修。Servlet里在读取任何参数之前先执行req.setCharacterEncoding("UTF-8"),响应开头写resp.setContentType("text/html;charset=UTF-8");JSP页面顶部保持<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>;数据库连接串加useUnicode=true&characterEncoding=utf8。三个点全对上,中文才能走完全程。用Navicat看数据时如果还是乱码,把连接编码也切成UTF-8,这是查看端的问题,不是程序问题。
5.2 JDBC报Communications link failure:先把SSL和时区两个参数加上
现象:启动项目后访问菜品列表,页面报java.sql.SQLException: Communications link failure,下面跟着SSL连接握手失败,或者The server time zone value...无法识别。
原因:MySQL 8.0默认开启SSL和严格时区校验,而老源码里的驱动和连接串都停留在MySQL 5.7时代。驱动版本老了不认新协议,连接串又没告诉服务器用哪个时区,握手就断在半路。
解决:连接串改成jdbc:mysql://localhost:3306/ordering?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai。useSSL=false关掉SSL握手,serverTimezone=Asia/Shanghai把时区对齐到东八区。如果驱动是mysql-connector-java 8.0.x,还会要求allowPublicKeyRetrieval=true才能处理新的密码认证,一并加上。改完记得重启Tomcat,只刷新页面没用,连接参数是在类加载时读进去的。
5.3 mysql命令报Error 2002 (HY000):服务没起来和socket路径是两回事
现象:在终端敲mysql -u root -p,报ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock'。
原因:这行报错的本质是MySQL服务进程没启动,或者客户端在找socket文件而服务端只监听TCP端口。Windows上常见于MySQL服务没注册成系统服务、手动启动后又被关掉;Linux上常见于安装后没执行service mysql start。
解决:先确认服务状态。Windows打开服务管理器找MySQL项点启动;Linux执行sudo service mysql start或systemctl start mysqld。如果服务在但还报错,连接时显式走TCP:mysql -h 127.0.0.1 -P 3306 -u root -p。这个坑跟Java代码没有关系,是纯粹的数据库服务没就位。排查时先绕开Java,直接用命令行连一次MySQL,能连上再回头看代码,一步就定位到问题在谁身上。
5.4 Tomcat 10跑老项目:javax.servlet全家报ClassNotFoundException
现象:项目部署后访问任何页面直接500,控制台一堆ClassNotFoundException,类名全是javax.servlet.***。
原因:Tomcat 10把Servlet规范命名空间从javax.servlet迁移到了jakarta.servlet。老项目里所有import javax.servlet.http.HttpServlet的代码,在Tomcat 10上找不到对应类。这是容器的破坏性升级,跟源码本身没有关系。
解决:换回Tomcat 8.5或9.x,老代码一行不用改。如果机器上只有Tomcat 10,不要去全局替换源码里的import,工程量大且容易漏;直接在IDEA的Run配置里把Tomcat home指向8.5版本就行。这个现象解释了前面反复强调版本配平的原因,很多“源码跑不起来”的案例最后都定位在容器版本上。
5.5 传统JSP项目打包war部署后404:上下文路径和访问路径对不上
现象:本地IDEA跑得好好的,打成war丢到Tomcat的webapps目录,启动后访问 http://localhost:8080/xxx/ 却404。
原因:war部署后Tomcat以war文件名作为上下文路径。如果war包叫ordering.war,访问根是/ordering;如果代码里页面跳转写死了/index.jsp,没有带上下文前缀,浏览器请求的就是根应用下的路径,自然404。另一种常见情况是war包解压失败,WEB-INF/lib下的驱动jar没打进去,页面报的错就成了NoClassDefFoundError。
解决:部署后先确认应用上下文,访问http://localhost:8080/war包名/ 试试。源码里所有Servlet重定向和JSP里的链接,规范做法是通过${pageContext.request.contextPath}动态拼上下文路径,不是写死/index.jsp。传统JSP项目打包war本身不难:IDEA里Build -> Build Artifacts选war包,但路径问题才是部署后真正的坎。顺手提醒一句,往Linux服务器部署时,注意webapps目录的写权限和端口放行,老项目在这些小地方花的排错时间往往比写代码还多。
6. 把订餐源码改造成毕业设计:扩展点、验证技巧与答辩准备
6.1 三个低成本高回报的扩展点
第一个是菜品搜索和分类筛选。加一个/search的Servlet,SQL里用LIKE拼name字段,再在导航栏加一个搜索框,工作量半天,功能却是评委最爱演示的入口。第二个是订单状态流转。在orders表status字段上做文章,后台把“已下单”改成“已接单”再改成“已送达”,前端订单列表按状态显示不同文案,涉及一次UPDATE和一次页面判断,但讲起来能覆盖状态机概念。第三个是简单的统计报表。后台首页加一行SQL:
SELECT DATE(create_time) AS d, COUNT(*) AS orderCount FROM orders GROUP BY DATE(create_time) ORDER BY d DESC;把结果渲染成表格,就是“数据可视化”的小亮点。这三个扩展都不需要动表结构的大手术,代码量小,但对答辩评分的作用很明显,而且每一个都能对应到一个java面试题或数据库考点。
6.2 用一张功能测试清单验证闭环
改造完别直接交,按业务顺序跑一遍回归测试,确认原始功能没被改坏:
| 用例 | 操作 | 预期结果 |
|---|---|---|
| 注册 | 访问register.jsp填写信息 | 跳转登录页,user表新增记录 |
| 登录 | 正确账号密码登录 | 进入首页,导航栏显示用户名 |
| 加购 | 菜品页点“加入购物车” | 购物车数量加一 |
| 下单 | 购物车提交订单 | orders和order_item同时插入新记录 |
| 后台处理 | 管理员登录后台更新订单状态 | 前台订单状态同步变化 |
| 未登录拦截 | 退出后直接访问购物车URL | 被过滤器重定向到登录页 |
拿到这套源码后,我习惯先跑一遍这个清单再动手改代码,而不是上来就拆。原因很简单:只有先确认原始版本全绿,后面报错才知道责任在自己改的代码上,而不是怀疑原包有问题。每改完一个扩展点,再把相关用例重新跑一遍,比最后统一排查省时得多。
6.3 交付前多花十分钟:导出war和写README
最后一步是把项目导出成war包,在干净的Tomcat上部署一遍。这一步能暴露一堆IDE里看不到的问题:缺驱动包、路径写死、JSP编译错误。同时写一个README,把环境版本、数据库初始化步骤、默认管理员账号密码写清楚。我自己养成的习惯是:凡是经手过的源码包,一定在电脑上留一份“能跑版本+说明文档”的压缩包,防止三个月后答辩前突然打不开再来回找资料。这套java+jsp+mysql的订餐系统,跑通它不难,但把它讲透、改好、交付得干净,才是它真正值钱的地方。希望帮到你。
本文还有配套的精品资源,点击获取