简介:一份面向 Java Web 初学者的电影院售票管理系统完整源码,基于 Java + Servlet + JSP + JDBC + MySQL 构建,采用 B/S 架构,覆盖电影信息管理、场次与座位管理、在线购票、订单生成、用户及管理员后台等模块,适合作为毕业设计或期末大作业参考。压缩包共 103 个文件,约 2.31MB,包含 23 个 Java 源文件及对应 class 文件、10 个 XML 配置、8 个 SQL 脚本,另有 17 个 zbak 备份、项目文档与界面截图,目录结构清晰,便于直接导入与调试。源码带有较详细注释,个人手打 98 分项目,界面简洁、操作直观;附带的数据库脚本和文档能帮助快速搭建运行环境,梳理购票流程与管理逻辑。目前已有 86 人学习,适合需要完整案例来理解 Servlet/JSP/JDBC 整合、用户权限控制及基础数据表设计的读者参考。
1. 电影院售票管理系统:为什么这套JavaWeb组合值得拆一遍
如果你拿到的毕业设计题目是“电影院售票管理系统”,打开源码包那一刻先别急着跑,大概率会遇到Tomcat启动失败、MySQL密码不匹配、页面中文乱码三连击。这套Java+Servlet+JSP+JDBC+MySQL的组合看起来不算新,但它把一个请求从浏览器到Servlet、再到DAO、再到数据库、最后返回JSP页面的完整路径全暴露在代码里,没有框架替你藏住任何细节,每一层都能在答辩台上讲清楚。这篇文章拆三件事:为什么选这套技术栈、五张表怎么设计、核心代码怎么组织,中间穿插我部署和调试时踩过的坑。准备拿这个题目做课程设计或毕业设计的同学,建议跟着把链路一步步跑通,那比直接下源码跑起来有价值得多。
2. 架构与数据库设计:五张表如何支撑整条购票链路
2.1 技术选型:Servlet+JSP这套老组合为什么还够用
先说结论:毕业设计看重的不是技术多新,而是你能否把“为什么这么选”讲清楚。
Spring Boot已经把太多细节封装掉了——你写一个@RestController加@Autowired就完事,但老师追问“请求从浏览器到服务端中间经历了什么”,你可能答不上来。用Servlet+JSP+JDBC手写,你可以在黑板上画出完整链路:
浏览器发HTTP请求 → Tomcat解析请求并匹配
@WebServlet注册的映射 → 对应Servlet的doGet/doPost被调用 → 读取request参数 → 通过JDBC从MySQL取数据 → 把结果放进request或session作用域 →forward到JSP → JSP模板渲染HTML → 响应写回浏览器。
这条链路画完,Web开发的基本功就全在台面上了。Spring Boot帮你省掉的东西,恰好也是答辩评委最爱追问的东西。用这套组合,你反而能把底层说透。
另外要理解JSP的本质:一个index.jsp在第一次被访问时,Tomcat会把它编译成一个继承自HttpJspBase的Java类,而HttpJspBase再往上追还是HttpServlet。也就是说JSP不是独立技术,它就是Servlet的一种模板化写法,页面里的HTML和标签会在编译阶段转成Java代码来输出。这不是玄学,你打开Tomcat的work目录就能看到编译产物。
各层的职责划分大概是这样:
- Servlet:接请求、解析参数、做简单校验、调DAO、决定页面跳转
- JSP:只做展示,配合EL表达式和JSTL标签输出数据
- DAO:封装SQL执行,返回实体对象或List
- MySQL:存储用户、电影、场次、座位、订单五类数据
这个复杂度不需要一个单独的Service层。购票这种涉及多步更新的逻辑直接写在Servlet里,用事务包住,反而更直观。
2.2 项目目录结构:代码按职责分开摆放
一个规范的源码包,目录结构一般是这样的:
cinema/ ├── src/com/cinema/ │ ├── entity/ │ │ ├── User.java │ │ ├── Movie.java │ │ ├── Schedule.java │ │ ├── Seat.java │ │ └── Order.java │ ├── dao/ │ │ ├── UserDao.java │ │ ├── MovieDao.java │ │ ├── ScheduleDao.java │ │ └── OrderDao.java │ ├── util/ │ │ ├── DBUtil.java │ │ └── OrderNoGenerator.java │ └── servlet/ │ ├── LoginServlet.java │ ├── RegisterServlet.java │ ├── MovieListServlet.java │ ├── MovieDetailServlet.java │ └── OrderServlet.java ├── WebContent/ │ ├── jsp/ │ │ ├── login.jsp │ │ ├── register.jsp │ │ ├── movieList.jsp │ │ ├── movieDetail.jsp │ │ └── orderResult.jsp │ ├── css/ │ ├── js/ │ └── WEB-INF/ │ └── web.xml ├── lib/ │ ├── mysql-connector-java-8.0.x.jar │ └── jstl-1.2.jar └── sql/ └── cinema.sql在这个结构里,改一个字段的路径是固定的:实体类加属性、DAO改SQL、JSP加展示,逻辑上不交叉。很多同学拿到源码先找web.xml,我反而建议先看entity目录,把几个实体类字段过一遍,再回头看SQL建表语句,整个系统的数据模型在脑子里就有了轮廓。
实体类和数据库表一一对应,但有一个小习惯值得注意。比如Movie实体里release_time字段,我建议直接声明成String,在DAO层把Date转好再放进去。这样JSP页面直接用${movie.releaseTime}输出,不用在页面上做日期格式化,省掉一批麻烦。
2.3 数据库设计:用户、电影、场次、座位、订单五张表
电影院售票系统最核心的数据模型就是五张表。先看整体对应关系:
| 表名 | 业务含义 | 核心字段 |
|---|---|---|
| user | 注册用户 | username, password, phone, realname |
| movie | 电影信息 | name, duration, release_date, status |
| schedule | 场次安排 | movie_id, hall, show_time, price |
| seat | 座位的实时状态 | schedule_id, row_no, col_no, status |
| orders | 购票订单 | order_no, user_id, schedule_id, seat_ids, total_price |
建表语句里最先要注意的是字符集。MySQL 8.0下直接建库最好用utf8mb4,它兼容全量Unicode字符,也比utf8在排序时更稳。看下面这段:
CREATE DATABASE cinema DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE cinema; CREATE TABLE `user` ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, realname VARCHAR(50), phone VARCHAR(20), created_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;user是MySQL里的保留字,建表必须用反引号包住,或者干脆改名t_user,否则导入sql文件时会报语法错误。这个细节看起来小,但坑过不少人——sql文件导入失败,整个项目就卡在第一步。
场次、座位和订单三张表的关联关系是重点:
CREATE TABLE schedule ( id INT AUTO_INCREMENT PRIMARY KEY, movie_id INT NOT NULL, hall VARCHAR(20), show_time DATETIME, price DECIMAL(8,2), INDEX idx_show_time (show_time), FOREIGN KEY (movie_id) REFERENCES movie(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE seat ( id INT AUTO_INCREMENT PRIMARY KEY, schedule_id INT NOT NULL, row_no VARCHAR(5), col_no INT, status TINYINT DEFAULT 0 COMMENT '0-空闲 1-锁定 2-已售出', UNIQUE KEY uk_schedule_seat (schedule_id, row_no, col_no), FOREIGN KEY (schedule_id) REFERENCES schedule(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE orders ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, schedule_id INT NOT NULL, seat_ids VARCHAR(255) COMMENT '冗余座位ID,逗号分隔', total_price DECIMAL(10,2), status TINYINT DEFAULT 0 COMMENT '0-待支付 1-已支付 2-已取消', created_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (schedule_id) REFERENCES schedule(id), INDEX idx_user_status (user_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;座位表的status字段,很多版本只做了0和1,我建议拆成三种状态:空闲、锁定、已售出。用户选座但还没支付时,座位要临时锁定,防止两个人在同一时间选同一个位置导致超卖。这个细节论文里值得单独写一段。
orders表里冗余一个seat_ids字符串字段,是一种实用取舍。按规范可能要拆一张订座明细表,但对课程设计来说,订单详情页直接拆字符串展示座位号,比多表关联简单得多,答辩也讲得清。
3. 核心代码实现:从JDBC工具类到DAO层到登录再到列表页
3.1 JDBC工具类:连接管理和资源释放是地基
整个项目里最值得先抄下来的代码就是DBUtil。它把驱动加载、连接获取、资源释放统一收到一个类里,后面的DAO不用每一处都写Class.forName:
package com.cinema.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/cinema" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs != null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt != null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }这段代码里的参数每个都有理由。serverTimezone=Asia/Shanghai是MySQL 8驱动的要求,不指定时区,驱动默认拿UTC,查询出来的DATETIME字段会差8小时。场次时间错8小时,意味着页面显示和数据库存的对不上,答辩演示时直接翻车。
驱动类名com.mysql.cj.jdbc.Driver是8.x版本的,老教程里写的com.mysql.jdbc.Driver在8.0.11之后已经被移除了。如果你的lib目录下是8.x的jar,类名必须用新的;如果用5.x的jar,类名可以不变,但5.x驱动连MySQL 8的默认认证插件会有兼容问题,所以建议统一用8.x。
资源释放的顺序也有讲究:先关ResultSet,再关Statement,最后关Connection。连接从连接池拿的时候,close只是归还给池,不会真正断开,此时ResultSet和Statement不手动关,内存里会堆一堆没释放的结果集对象。
3.2 DAO层:把SQL从Servlet里拆出去
很多不规范的源码喜欢在Servlet里直接写SQL,看起来省事,但一旦加需求就要大面积改动。标准做法是SQL只出现在DAO层。看UserDao的写法:
package com.cinema.dao; import com.cinema.entity.User; import com.cinema.util.DBUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class UserDao { public User findByUsername(String username) throws SQLException { String sql = "SELECT id, username, password, realname, phone " + "FROM user WHERE username = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { return mapRow(rs); } } } return null; } public boolean insert(User user) throws SQLException { String sql = "INSERT INTO user(username, password, realname, phone) " + "VALUES(?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getRealname()); ps.setString(4, user.getPhone()); return ps.executeUpdate() > 0; } } private User mapRow(ResultSet rs) throws SQLException { User u = new User(); u.setId(rs.getInt("id")); u.setUsername(rs.getString("username")); u.setPassword(rs.getString("password")); u.setRealname(rs.getString("realname")); u.setPhone(rs.getString("phone")); return u; } }这里用了JDK 7的try-with-resources,Connection和PreparedStatement都实现了AutoCloseable,方法结束自动关闭,代码比手写finally干净很多。mapRow抽成一个私有方法,以后加字段只改一处,不用每个方法都改rs.getXxx。
注意PreparedStatement的占位符。传参一律用setString、setInt,不要拼字符串。拼字符串的问题在下面登录代码里会专门展开,防SQL注入是答辩时最稳的一个加分点。
3.3 登录与注册:Servlet、Session和页面跳转
登录是系统入口,也是Servlet代码最典型的一段。看LoginServlet:
package com.cinema.servlet; import com.cinema.dao.UserDao; import com.cinema.entity.User; 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 java.io.IOException; @WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); if (username == null || username.trim().isEmpty() || password == null || password.isEmpty()) { request.setAttribute("error", "用户名和密码不能为空"); request.getRequestDispatcher("jsp/login.jsp").forward(request, response); return; } UserDao userDao = new UserDao(); try { User user = userDao.findByUsername(username); if (user != null && user.getPassword().equals(password)) { request.getSession().setAttribute("loginUser", user.getUsername()); request.getSession().setAttribute("userId", user.getId()); response.sendRedirect(request.getContextPath() + "/movieList"); } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("jsp/login.jsp").forward(request, response); } } catch (Exception e) { e.printStackTrace(); throw new ServletException("登录时数据库操作异常", e); } } }request.setCharacterEncoding("UTF-8")必须放在读取参数之前,因为POST请求的编码由请求体决定,晚一步参数就乱码了。GET请求的乱码问题是另一条路径,要改Tomcat的server.xml,这个后面避坑章节详细说。
这里查用户用的是findByUsername,取回来再比对密码。为什么不直接WHERE username=? AND password=??因为后面要改成密码加盐存储,到时候SQL条件没法直接比原始密码,必须先拿到用户记录再校验。先在DAO里留出这个结构,后面优化会省事很多。
代码里有两条跳转路径:
- 登录失败:
forward到login.jsp,携带error提示 - 登录成功:
sendRedirect到movieList
forward和sendRedirect的区别是高频面试题,也是答辩大概率会问的点。forward是服务端内部跳转,浏览器地址栏不变,request中的数据能带到目标页;sendRedirect是告诉浏览器重新发起一次GET请求,地址栏变化,原request里的数据全部丢失。登录成功后必须用重定向,否则刷新页面会重复提交表单,后果在避坑章节会讲。
3.4 JSP视图层:EL表达式和JSTL把Java代码请出页面
登录成功后进入电影列表页。业务逻辑在MovieListServlet里:
@WebServlet("/movieList") public class MovieListServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { List<Movie> movieList = MovieDao.findByStatus(1); request.setAttribute("movieList", movieList); request.getRequestDispatcher("jsp/movieList.jsp").forward(request, response); } }对应的JSP页面:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>正在热映</title> </head> <body> <h2>正在热映</h2> <c:forEach var="movie" items="${movieList}"> <div class="movie-card"> <h3>${movie.name}</h3> <p>片长:${movie.duration} 分钟</p> <p>${movie.description}</p> <a href="${pageContext.request.contextPath}/movieDetail?id=${movie.id}">选座购票</a> </div> </c:forEach> </body> </html>EL表达式${movie.name}走的是Movie.getName()方法,不是直接读字段。所以实体类加字段后忘了生成getter,页面上会取到null,这是那种“找不到原因但就是显示空白”的问题,排查很久最后发现是个getter缺失。
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>这一行依赖jstl-1.2.jar,这个jar在lib目录里必须存在。很多源码包不带这个jar,JSP一运行就报org.apache.jasper.JasperException: Unable to load class for JSP,不是你的代码问题,是依赖缺失。
工程里的URL拼接用了${pageContext.request.contextPath},这个一定要习惯——它动态获取应用上下文路径,这样部署到任何Tomcat目录下都不会因为路径问题404。
到这里,登录、查列表两个核心链路已经通了。接下来单独开一章说那四个让人debug到怀疑人生的坑。
4. 避坑与排查:四个让我熬夜调试的问题
4.1 ClassNotFoundException:MySQL 8的驱动类名迁移
现象:Tomcat能启动,但访问任何涉及数据库的接口,控制台直接报ClassNotFoundException: com.mysql.jdbc.Driver。
原因:lib目录下的mysql-connector-java.jar是8.x版本,8.0.11之后官方把驱动类从com.mysql.jdbc.Driver迁移到了com.mysql.cj.jdbc.Driver,旧类名直接删掉。源码里如果还写旧类名,Class.forName瞬间崩。
解决:改DBUtil里的驱动类名,同时连接串补上serverTimezone参数。如果你的数据库账号加密方式还是caching_sha2_password,而驱动版本偏老,再执行一条命令把认证插件换回mysql_native_password:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';这个坑排查起来其实很快:看jar包版本,再看代码里的类名,两个不匹配就对了。
4.2 中文乱码:编码链路每一环都要通
现象:登录后电影名称显示成????,后台打印SQL参数也是乱码。
原因:中文乱码是链条式问题,任何一环断了都会出。最常踩的三处:JSP头部没指明contentType、Servlet没调用setCharacterEncoding("UTF-8")、数据库连接串缺characterEncoding=utf8。另外还有一个隐藏点——Tomcat对GET请求默认编码是ISO-8859-1,GET参数里的中文到了Servlet已经乱了,这时候在Servlet里设置编码也救不回来。
解决:四层一起配,基本能根治:
- JSP页面头部:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> - Servlet的doGet和doPost第一行各加:
request.setCharacterEncoding("UTF-8");和response.setCharacterEncoding("UTF-8"); - JDBC连接串加上:
useUnicode=true&characterEncoding=utf8 - Tomcat的
conf/server.xml里Connector节点加:URIEncoding="UTF-8"
我的习惯是配完这四层,写一个测试页插入一条中文数据再读出来,把整个编码链路验证通了再继续往下开发,而不是写完所有页面才发现乱码,再回过来排查是哪一层漏了。
4.3 刷新页面导致重复下单:POST之后必须重定向
现象:用户点“确认购票”生成了订单,按一下F5刷新,数据库里多了一条一模一样的订单。
原因:购票提交走了forward到结果页。forward不会改变浏览器地址栏URL,当前地址还是那个处理订单的Servlet地址,F5刷新等于又发了一次POST,服务端又把下单逻辑执行了一遍。
解决:所有写操作处理完之后,一律sendRedirect到查询型页面。比如下单成功后重定向到订单详情页,URL带上订单号,新的请求是GET,不会执行第二次下单。这是Web开发里很经典的POST-Redirect-GET模式,答辩时把这个词说出来,评委就知道你是真跑过项目踩过坑的人。
4.4 连接没关导致数据库连接数被打满
现象:系统跑了一段时间,控制台开始报Connection is not available, request timed out,同时MySQL的show processlist能看到一大堆Sleep状态的连接。
原因:DAO里每次getConnection()之后,如果前面有SQL异常,后面的close()可能根本没走到。加上某些源码直接new Connection而不用连接池,高并发请求一多,数据库的151个默认连接数瞬间就被占满。
解决:所有close()放进finally块,或者用try-with-resources自动关闭。基础写法:
Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); ps = conn.prepareStatement(sql); // 执行操作 } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, ps, conn); }再进一步,接一个连接池,这个问题就彻底不出现了。连接池的接入方式放到最后一章讲,那是答辩前值得做的事。
5. 部署与购票事务:从Tomcat跑通到下单不丢数据
5.1 部署到Tomcat:版本选择和war包方案
先说一个2024年之后特别容易踩的坑:Tomcat版本。Tomcat 10开始把Java EE的命名空间从javax.*改成了jakarta.*,Servlet+JSP老项目如果用了javax.servlet.http.HttpServlet,直接丢到Tomcat 10运行,编译期报一堆ClassNotFoundException。对于这套JDBC+Servlet项目,最稳的是Tomcat 9,和javax.*完全兼容。如果老师指定了高版本Tomcat,那就得把所有javax.servlet导入改成jakarta.servlet,工作量不小,一般不值得。
部署方式两种:
第一种,把整个WebContent目录连同lib下的jar包,打包成war文件。eclipse里Export→WAR file,或者直接jar -cvf cinema.war *,然后丢到Tomcat的webapps目录下,启动时自动解压。这种方式适合最终交付。
第二种,开发期直接在IDE里配置Tomcat Server,把项目添加进去,IDE自动完成部署和热更新。调试阶段用这个,改完JSP刷新就能看效果。
启动之前检查三件事:MySQL服务是否在跑、DBUtil里的用户名密码是否匹配、lib目录下的mysql驱动jar是否复制到了Tomcat的classpath。最后一个问题常见于直接把项目文件夹拷到webapps下但忘记带lib目录,启动时数据库操作全报ClassNotFoundException: com.mysql.jdbc.Driver,控制台刷屏但Tomcat本身没崩,看半天才能定位到是lib缺失。
5.2 购票事务:多步更新必须包在同一个事务里
购票这个动作牵扯三步:检查座位状态、更新座位为已售、插入订单记录。这三步必须在一个事务里完成,否则中间任何一步失败,数据库就处于脏数据状态——座位卖出去了但没订单,或者订单生成了但座位还是空的。
看一个事务的标准写法:
Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 1. 检查座位状态 String checkSql = "SELECT status FROM seat WHERE id = ? AND schedule_id = ?"; // 2. 更新座位状态为已售 String updateSql = "UPDATE seat SET status = 2 WHERE id = ? AND status != 2"; // 3. 插入订单记录 String insertSql = "INSERT INTO orders(order_no, user_id, schedule_id, seat_ids, total_price) VALUES(?, ?, ?, ?, ?)"; conn.commit(); // 全部成功,统一提交 } catch (SQLException e) { try { if (conn != null) conn.rollback(); // 任何一步失败,全部回滚 } catch (SQLException ignored) {} e.printStackTrace(); } finally { DBUtil.close(null, null, conn); }这里有一个关键点:更新座位时SQL里带了status != 2条件,executeUpdate返回0说明座位已经被别人买走了,这种情况要主动抛异常触发回滚。这就是乐观锁的思路——不提前锁表,更新时检查条件,冲突直接放弃。用SELECT ... FOR UPDATE是更强硬的做法,但课程设计里容易引出死锁问题,我一般建议用status != 2这种条件更新方式。
事务代码写完之后,可以手动做一个测试验证批量性能:开两个浏览器窗口同时操作同一个座位,一个成功另一个必须看到“已被购买”的提示,而不是两个都下单成功。
5.3 账户与连接参数:部署前的最后配置检查
部署前最后过一遍配置文件:
| 配置项 | 建议值 | 说明 |
|---|---|---|
| mysql连接串 | useSSL=false | 本地开发不需要SSL |
| serverTimezone | Asia/Shanghai | 避免时间差8小时 |
| MySQL password | 与代码一致 | 两个地方必须同步 |
| Tomcat端口 | 8080 | 被占用时改server.xml |
| JSP页面编码 | UTF-8 | 三处编码保持一致 |
MySQL 8的默认认证插件对老驱动不友好,如果你发现本地跑通的项目换一台机器Access denied报错,优先检查认证插件和驱动版本的兼容组合。平时准备一个好用的MySQL管理工具,比如Navicat或DBeaver,导入sql、看数据、调整权限都比命令行高效很多。
6. 答辩前值得做的三个优化:连接池、密码加盐和一个验证习惯
6.1 用Druid连接池替换原生JDBC连接
原生DriverManager.getConnection每调用一次就建一个物理连接,性能差且连接数不可控。答辩前花半小时接入一个连接池,效果立竿见影。以Druid为例,把DBUtil改造一下:
private static DruidDataSource dataSource; static { dataSource = new DruidDataSource(); dataSource.setUrl(URL); dataSource.setUsername(USER); dataSource.setPassword(PASSWORD); dataSource.setInitialSize(5); dataSource.setMaxActive(20); dataSource.setMinIdle(5); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); }DBUtil.close不用改,因为池化连接在close时只是归还给连接池,不是真的断开,但你仍然需要手动关闭ResultSet和Statement,否则长期跑下来结果集对象堆积。接入连接池后,之前说的“连接打满”问题基本绝迹。
6.2 用户密码加盐存储
直接明文存密码是最容易被答辩老师挑刺的一点。把密码改成加盐存储:
public static String encrypt(String password, String salt) { String input = salt + password; return DigestUtils.md5Hex(input); // 或用SHA-256 }注册时生成随机salt存到用户表的salt字段。登录时按用户名查出salt,再对输入密码做同样的加密,与库里存的密文比对。这个改动只涉及注册和登录两处逻辑,DAO层SQL不用大改,但加了这个细节,整个项目的安全意识就上一个档次。
6.3 一个验证习惯
做完以上优化,建议你走一遍完整的验收清单:注册新账号、用错误密码登录一次、正常登录、查看电影列表、选座下单、支付、查订单、刷新页面确认没有重复订单,全部过一遍,再准备答辩。
我之前就吃过亏:项目本地跑得好好的,答辩前换了台演示机器,Tomcat和MySQL装好后忘了改DBUtil里的数据库密码,演示时第一个登录接口就报错,在台上满头大汗改配置,那感觉太狼狈了。从那以后,我每次换机器部署都强制走一遍这个清单,先从数据库连接验证开始,再一个接口一个接口测,确认无误才敢上台。这个习惯救了我好几次,希望帮到你。
本文还有配套的精品资源,点击获取