ARTICLE DETAIL

资讯详情

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

Java+JSP+Servlet+MySQL活动管理系统:数据库设计、JDBC事务与部署避坑

Java+JSP+Servlet+MySQL活动管理系统:数据库设计、JDBC事务与部署避坑 简介一套基于Java技术栈的前后台分离式活动管理系统面向学习JavaWeb的开发者也适合作为课程设计或毕业设计参考。系统内置管理员与普通用户两种角色管理员可进行登录、个人信息维护、活动类型管理、活动发布与报名审核并支持游客管理用户端则可按分类浏览活动、查看活动详情、在线提交报名、确认报名状态及管理个人报名记录前后台联动完整覆盖活动管理业务闭环。资源包为RAR压缩格式共214个文件约10.56MB内含55个Java源码及对应class文件、27个JSP页面、数据库SQL脚本以及jQuery、Bootstrap、CSS、JS等前端资源另有部分jpg预览图和配置文件能够直接导入IDE运行。已有564人学习下载借助这套项目可重点练习JSP、Servlet、Bootstrap三者的整合理解MVC分层、接口调用和基础权限控制的实现思路便于后续二次扩展。1. 现在还有必要做 JavaJSPBootstrapServletMySQL 这套活动管理系统吗活动管理系统、Java、JSP、Bootstrap、Servlet、MySQL——如果要在简历上写一个完整项目这套组合其实是很好的底子。它没有 Spring Boot 全家桶也没有前后端分离但恰好因为技术栈老从浏览器请求到 Servlet 再到 JDBC 和数据库的整条链路全部裸露适合把 Java Web 基础吃透。活动管理系统的核心业务不复杂用户登录、活动发布、用户报名、后台管理几张表就能撑起一个可演示的闭环。适合正在找基于 JSP 毕设选题的学生也适合只想快速验证业务原型、不想引入重框架的开发者。这套栈跑通容易但乱码、连不上库、war 包缺资源这类坑每一个都能卡住你半天。2. 活动管理系统的表怎么建六张表把“用户—活动—报名”串起来2.1 为什么最少要建六张表去掉角色表会后悔活动管理系统的业务主链路是“用户登录 → 管理员发布活动 → 用户报名 → 查看报名记录”。单看核心数据用户表、活动表、报名表三张表就能跑但我一般会再加三张角色表、活动类型表、操作日志表。角色表解决的是“这个用户能不能进后台”的问题。把 role_id 直接塞在用户表里用 1、2、3 这种数字代表管理员和普通用户短期内能跑但后面要加“活动审核员”这种角色时就得改 Java 代码而不是改数据。活动类型表的价值在列表页筛选和统计报表比如按“讲座、比赛、志愿活动”分类统计参与人数没有类型表就只能写死下拉框。操作日志表是给答辩和排错用的谁在什么时间把活动状态从草稿改成了发布留一条记录出问题时不用对着数据库 binlog 猜。你如果只想压缩到五张去掉操作日志表是最合理的角色表可以简化成用户表里的一个字段但我建议保留因为活动管理系统的权限差异是刚需——普通用户只能报名管理员才能编辑活动。建表的顺序也有讲究先建角色表和类型表再建用户表和活动表最后建报名表和日志表这样外键关联不会报“表不存在”。2.2 关键字段设计与类型选择状态机、唯一索引、默认值活动表是整个系统的核心设计时最容易翻车的是状态和名额这两个字段。状态字段用 TINYINT 而不是 VARCHAR。我的定义是0 草稿、1 报名中、2 进行中、3 已结束、4 已取消。用数字的好处是排序、比较、维护都方便但页面上不能直接显示数字所以 Java 端需要一个状态枚举或者字典映射把 0 翻译成“草稿”把 1 翻译成“报名中”。名额字段建议建两个quota 表示总名额remaining_quota 表示剩余名额。quota 为 0 表示不限名额这个约定要在代码里写明。建表时给 remaining_quota 设默认值 0避免插入数据时漏传导致 NULL后续做remaining_quota 0条件判断时 NULL 会让结果永远为假。报名表上必须加唯一索引这是防重复报名的第一道防线CREATE TABLE activity_signup ( id BIGINT AUTO_INCREMENT PRIMARY KEY, activity_id BIGINT NOT NULL COMMENT 活动ID, user_id BIGINT NOT NULL COMMENT 用户ID, signup_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 报名时间, UNIQUE KEY uk_activity_user (activity_id, user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT活动报名表;唯一索引uk_activity_user保证了同一个人对同一个活动只能有一条报名记录。这里有一个隐藏很深的坑如果你想支持“取消报名后还能再报”就不要设计成cancel_flag标记取消状态因为取消标记加唯一索引会导致同一个人第二次报名时插入两条记录严格模式下直接报唯一索引冲突。我的处理方式是取消报名就 DELETE 这条记录重新报名就是全新 INSERT简单且不会被约束卡住。2.3 可直接执行的初始化 SQL建库建表加测试数据下面这份 SQL 是完整可执行的库名、表名、字段名都按 Java 端习惯命名。用 MySQL Workbench 或命令行执行前确认你的 MySQL 服务已启动执行时选择 utf8mb4 字符集否则后面写中文进表会变问号。CREATE DATABASE IF NOT EXISTS activity_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE activity_system; CREATE TABLE sys_role ( id INT PRIMARY KEY, role_code VARCHAR(30) NOT NULL COMMENT 角色编码, role_name VARCHAR(30) NOT NULL COMMENT 角色名称 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT角色表; CREATE TABLE activity_type ( id INT AUTO_INCREMENT PRIMARY KEY, type_name VARCHAR(50) NOT NULL UNIQUE COMMENT 类型名称 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT活动类型表; CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(128) NOT NULL COMMENT 密码散列值, real_name VARCHAR(50) COMMENT 姓名, role_id INT NOT NULL DEFAULT 2 COMMENT 角色ID1管理员 2普通用户, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1启用 0禁用, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE activity ( id BIGINT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL COMMENT 活动标题, description TEXT COMMENT 活动详情, location VARCHAR(200) COMMENT 活动地点, type_id INT COMMENT 活动类型ID, start_time DATETIME NOT NULL COMMENT 开始时间, end_time DATETIME NOT NULL COMMENT 结束时间, quota INT NOT NULL DEFAULT 0 COMMENT 总名额0表示不限, remaining_quota INT NOT NULL DEFAULT 0 COMMENT 剩余名额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1报名中 2进行中 3已结束 4已取消, created_by BIGINT NOT NULL COMMENT 创建人ID, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_status_time (status, start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT活动表; CREATE TABLE activity_signup ( id BIGINT AUTO_INCREMENT PRIMARY KEY, activity_id BIGINT NOT NULL COMMENT 活动ID, user_id BIGINT NOT NULL COMMENT 用户ID, signup_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 报名时间, UNIQUE KEY uk_activity_user (activity_id, user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT活动报名表; CREATE TABLE operation_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL COMMENT 操作人ID, action VARCHAR(50) NOT NULL COMMENT 操作动作, detail VARCHAR(500) COMMENT 操作详情, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT操作日志表; INSERT INTO sys_role (id, role_code, role_name) VALUES (1, ADMIN, 管理员), (2, USER, 普通用户); INSERT INTO activity_type (type_name) VALUES (讲座), (比赛), (志愿活动), (团建); INSERT INTO sys_user (username, password, real_name, role_id) VALUES (admin, 240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9, 系统管理员, 1), (zhangsan, 240be518fabd2724ddb6f04eeb1da5967448d7e831c08c8fa822809f74c720a9, 张三, 2);说明几个容易忽略的参数。字符集统一用utf8mb4而不是utf8因为utf8在 MySQL 里最多存 3 字节用户昵称里出现 emoji 就插不进去。时间字段用DATETIME不要用VARCHAR存时间字符串否则 Java 端排序、范围查询都要做字符串转换还会出现“2024-1-1”和“2024-01-01”这种不一致的数据。remaining_quota默认值 0 配合业务约定“0 表示不限名额”代码里查询可报名活动时要用(quota 0 OR remaining_quota 0)这个条件漏掉quota 0会把不限名额的活动过滤掉。初始化脚本里的密码列是admin123的 SHA-256 散列值演示项目可以直接用正式系统建议换成 BCrypt 加盐散列后面 Java 端统一用工具类生成不要再出现明文密码。3. 用 Servlet JSP 把登录和活动列表跑通整条请求链路怎么串3.1 项目骨架怎么搭Maven war 结构与 Tomcat 版本配对标题里的技术栈决定项目形态Servlet 做控制器、JSP 做视图、Bootstrap 做样式MySQL 做持久化。我建议用 Maven 的 war 包结构而不是 IDEA 里直接新建一个空 Web 项目。Maven 的好处是依赖版本可控换机器后mvn clean package能稳定还原环境。搭骨架前先确认 Tomcat 版本。Tomcat 9 对应javax.servlet包名Tomcat 10.1 及以上对应jakarta.servlet包名二者不兼容。如果你照着网上的老教程写import javax.servlet.http.HttpServlet却用到 Tomcat 10编译直接找不到类。我的选择是 Tomcat 9 Maven最省事。pom.xml 里关键的依赖配法如下dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependenciesservlet-api的 scope 必须设为provided。原因是 Tomcat 自带 Servlet 实现如果你打成 war 包又把 Servlet API 打进WEB-INF/lib运行时类加载器会出现两个同名的HttpServlet轻则告警重则 NoSuchMethodError。JSTL 依赖则必须打包进去很多 JSP 页面空白的问题就是 Tomcat 自带 JSTL 但项目没有声明依赖。有人用 VSCode 配 Tomcat 写 Servlet能跑但调试信息分散在多个终端遇到问题时排查路径长。想要入手简单直接用 IDEA 社区版加 Maven安装一个 Tomcat 插件或者用mvn tomcat7:run这种插件方式启动都能把断点打到 Servlet 代码里。目录结构保持 Maven 标准src/main/java放包名如com.example.activity下的代码src/main/webapp放 JSP、静态资源src/main/resources放druid.properties这些配置文件。JSP 放在WEB-INF下可以防止用户直接输入 URL 访问未登录页面但登录页本身要放外面因为用户需要先访问它才能提交表单。3.2 编码过滤器先写好乱码问题一半在这里JSP 页面不设置编码表单提交中文用户名到 Servlet 后取出来就是乱码。我习惯在项目最开始就写一个全局编码过滤器而不是在每个 Servlet 里重复写request.setCharacterEncoding。package com.example.activity.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletResponse; import java.io.IOException; WebFilter(/*) public class CharacterEncodingFilter implements Filter { Override public void init(FilterConfig filterConfig) { // 过滤器初始化时不需要额外逻辑 } Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); HttpServletResponse resp (HttpServletResponse) response; resp.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); } Override public void destroy() { // Tomcat 正常关闭时由容器调用 } }这段代码的逻辑是在请求进入任何 Servlet 之前先把请求体的解码字符集设为 UTF-8再把响应的编码和 Content-Type 设好之后chain.doFilter放行到真正的业务代码。这样 Servlet 里直接req.getParameter(title)拿到的就是正确的中文。参数上要注意一个偏差POST 请求的编码由request.setCharacterEncoding决定但 GET 请求的参数是拼在 URL 里的编码由 Tomcat 的URIEncoding决定。Tomcat 8.5 之后的默认值已经是 UTF-8老教程让你改 server.xml 的URIEncodingUTF-8在 Tomcat 9 上写不写区别不大如果你还在用 Tomcat 7这行配置必须有。我一般不管 GET 还是 POST统一用 POST 提交表单避免 URL 里带中文参数的解析问题。3.3 登录与会话Servlet 处理表单、Session 存用户登录功能是活动管理系统的入口。流程是login.jsp 提交用户名密码到/loginLoginServlet 校验通过后把用户对象放进 Session然后重定向到活动列表校验失败则转发回登录页并显示错误信息。package com.example.activity.servlet; import com.example.activity.entity.User; import com.example.activity.service.UserService; 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 javax.servlet.http.HttpSession; import java.io.IOException; WebServlet(/login) public class LoginServlet extends HttpServlet { private final UserService userService new UserService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); User user userService.login(username, password); if (user null) { req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } HttpSession session req.getSession(); session.setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /activity/list); } }这里最值得说的是“失败用转发、成功用重定向”的设计。转发是服务器内部跳转地址栏还是/login刷新时会重复提交表单重定向是让浏览器重新发起一次 GET 请求地址栏变成/activity/list用户刷新页面也不会再次提交登录表单。这是老 Java Web 项目里处理表单提交的通用套路。Session 里存的是 User 对象而不是 userId 字符串因为 JSP 页面可能展示“欢迎你张三”从sessionScope.loginUser.realName取值比再查一次库方便。注意 User 对象需要实现序列化接口吗Tomcat 的 Session 钝化场景下建议实现Serializable但单机演示项目不钝化也能跑写了更好。对应的登录拦截 Filter 也不能漏否则用户直接访问/activity/list就能绕过登录package com.example.activity.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; WebFilter(urlPatterns {/activity/*, /user/*}) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); if (session null || session.getAttribute(loginUser) null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }写这个 Filter 时有一个细节调用req.getSession(false)而不是req.getSession()。带 false 的意思是“如果当前没有 Session 就返回 null”不带参数的版本会强制创建一个新的 Session。如果登录过滤器里用了getSession()那么所有未登录的匿名请求都会被创建 Session浪费服务器内存还会让 session 超时的排查变得混乱。3.4 活动列表页JSTL 渲染 Bootstrap 表格JSP 里尽量不写 Java活动列表的 Servlet 负责查数据库并分页然后把结果放 request 属性里转发到 JSP。JSP 里负责用 JSTL 和 EL 渲染不要把% %脚本片段写得到处都是。package com.example.activity.servlet; import com.example.activity.entity.Activity; import com.example.activity.service.ActivityService; import com.example.activity.util.PageResult; 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; import java.util.List; WebServlet(/activity/list) public class ActivityListServlet extends HttpServlet { private final ActivityService activityService new ActivityService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int page 1; try { page Integer.parseInt(req.getParameter(page)); } catch (NumberFormatException ignored) { // 参数缺失或非法时默认第一页 } PageResultActivity pageResult activityService.listByPage(page, 10); req.setAttribute(page, pageResult); req.getRequestDispatcher(/WEB-INF/jsp/activityList.jsp).forward(req, resp); } }JSP 页面核心片段如下Bootstrap 只负责样式业务数据全部由 JSTL 标签输出% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % div classcontainer mt-4 table classtable table-striped table-hover thead tr th活动名称/th th类型/th th开始时间/th th剩余名额/th th状态/th th操作/th /tr /thead tbody c:forEach items${page.list} varact tr td${act.title}/td td${act.typeName}/td tdfmt:formatDate value${act.startTime} patternyyyy-MM-dd HH:mm//td td${act.remainingQuota}/td td${act.statusText}/td td a href${pageContext.request.contextPath}/activity/signup?id${act.id} classbtn btn-sm btn-primary报名/a /td /tr /c:forEach /tbody /table /divBootstrap 的使用方法记住一条栅格系统加预置类不要自己写 CSS。container控制页面最大宽度和居中table-striped让奇偶行颜色不同table-hover行悬停高亮按钮的大小和颜色由btn-sm btn-primary决定。引入方式直接用 CDNlink hrefhttps://cdn.jsdelivr.net/npm/bootstrap5.3.0/dist/css/bootstrap.min.css relstylesheet script srchttps://cdn.jsdelivr.net/npm/bootstrap5.3.0/dist/js/bootstrap.bundle.min.js/script这里提一个 JSP 里常见的头疼问题fmt:formatDate报错说标签不认识多半是页面忘了声明% taglib prefixfmt ... %这一行。JSTL 由 core、fmt、fn 等多个标签库组成用哪个标签就要声明哪个前缀没有例外。4. JDBC 怎么写才不拖后腿连接池、预编译语句与报名事务4.1 DriverManager 能跑为什么还要换 Druid 连接池很多入门教程教你DriverManager.getConnection(url, user, password)拿连接用完conn.close()。这个写法在教程里没问题但放到活动管理系统里用户点击“报名”按钮时每次请求都新建物理连接、用完再销毁MySQL 的握手认证开销比 SQL 执行本身还大。并发一高数据库连接数即用即焚应用侧表现为页面转圈、报 Too many connections。连接池干的事是提前建好一批连接放在池子里请求来了借走、用完了归还。Druid 在国内用得最多配置直观自带监控。这里不是不能用 DriverManager而是“能跑”和“能扛”是两回事。课设答辩时如果被问到“并发场景你怎么办”接上连接池比现场改代码从容得多。4.2 配置 Druid 连接池druid.properties 与 JdbcUtils在src/main/resources下新建druid.propertiesdriverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://127.0.0.1:3306/activity_system?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8allowPublicKeyRetrievaltrue usernameroot password你的数据库密码 initialSize5 maxActive20 maxWait30000一行一行解释driverClassName用com.mysql.cj.jdbc.Driver这是 MySQL 8.x 版本的驱动类名老教程里的com.mysql.jdbc.Driver已经被移除了。URL 里四个参数都有明确的用处useSSLfalse关掉 SSL 握手MySQL 8 默认开启 SSL不关会报 “Communications link failure” 或一直告警serverTimezoneAsia/Shanghai解决驱动读取数据库时间时遇到的时区报错characterEncodingutf8保证写入和读取中文不乱码allowPublicKeyRetrievaltrue配合 MySQL 8 默认的caching_sha2_password认证插件否则连接时可能报 “Public Key Retrieval is not allowed”。注意一个细节properties 文件里不需要转义但这段 URL 如果写在 web.xml 或 Spring XML 里必须写成amp;。很多人在 xml 里配 JDBC 链接报 XML 解析错误就是这个原因。对应的工具类package com.example.activity.util; import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; public class JdbcUtils { private static DataSource dataSource; static { try (InputStream in JdbcUtils.class.getClassLoader() .getResourceAsStream(druid.properties)) { Properties props new Properties(); props.load(in); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r ! null) { try { r.close(); } catch (Exception ignored) { // 关闭资源失败不影响主流程 } } } } }静态代码块在类加载时执行一次负责读取配置并创建连接池。getConnection返回的是池化连接close方法接收可变参数一次关掉 ResultSet、Statement、Connection。这里有个习惯要养写 DAO 时关资源的代码放在 finally 块里并且从后往前关先结果集再语句最后连接。用这个工具类就能写JdbcUtils.close(rs, stmt, conn)一行搞定。4.3 报名扣名额事务和条件更新怎么保住数据一致性报名是活动管理系统里唯一一个必须用事务的地方。业务要求是剩余名额减一同时插入一条报名记录两个操作要么都成功、要么都失败。如果不用事务出现“名额扣了但报名记录没插进去”用户会觉得自己报了名但列表里查不到管理员更头疼。package com.example.activity.dao; import com.example.activity.util.JdbcUtils; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; public class SignupDao { public boolean signUp(Long activityId, Long userId) { String deductSql UPDATE activity SET remaining_quota remaining_quota - 1 WHERE id ? AND remaining_quota 0 AND status 1; String insertSql INSERT INTO activity_signup (activity_id, user_id) VALUES (?, ?); Connection conn null; try { conn JdbcUtils.getConnection(); conn.setAutoCommit(false); try (PreparedStatement ps1 conn.prepareStatement(deductSql); PreparedStatement ps2 conn.prepareStatement(insertSql)) { ps1.setLong(1, activityId); int rows ps1.executeUpdate(); if (rows 0) { conn.rollback(); return false; } ps2.setLong(1, activityId); ps2.setLong(2, userId); ps2.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } } catch (SQLException e) { throw new RuntimeException(报名失败请稍后重试, e); } finally { JdbcUtils.close(conn); } } }这段代码的核心逻辑在deductSql的条件更新UPDATE ... WHERE id ? AND remaining_quota 0 AND status 1。affected rows 为 0 说明名额已满、活动已取消或活动不在报名中状态此时回滚并返回 false。这就是用数据库的原子更新解决并发问题两个用户同时报名最后一个名额时数据库层面只会让一条 UPDATE 成功不会出现超卖。手动事务有三个关键点不要漏。第一conn.setAutoCommit(false)必须在执行 UPDATE 之前调用否则每条 SQL 各自独立提交事务形同虚设。第二catch 块里必须调用conn.rollback()否则连接归还连接池时带着未提交的事务下次被借出可能把旧数据一起提交。第三finally 里的close会把连接归还池子Druid 会自动回滚未提交的事务但显式 rollback 是更稳妥的习惯。PreparedStatement全部用?占位符而不是字符串拼接 SQL两个原因防 SQL 注入以及避免拼字符串时漏掉单引号导致语法错误。setLong绑定参数时索引从 1 开始这个和数组下标从 0 开始的习惯不一样写错会导致Parameter index out of range。5. 传统 JSP 项目避坑实录乱码、MySQL 连不上、war 包 4045.1 页面和数据库全是中文乱码现象登录后页面显示“欢迎你???”打开数据库表username 字段存的是问号或乱码从 JSP 表单提交中文到 Servlet控制台打印出来就是乱的。原因乱码是三层问题叠加的结果。第一层浏览器提交的请求体编码不是 UTF-8Servlet 拿getParameter时按默认 ISO-8859-1 解码第二层JDBC URL 里没有characterEncodingutf8驱动按平台默认编码读写第三层数据库表本身字符集是latin1UTF-8 数据往里写直接被截断成问号。解决三层分别处理。请求编码用第 3.2 节的全局 Filter 统一设置JDBC URL 加characterEncodingutf8建库建表时指定DEFAULT CHARSETutf8mb4。对于已经建错的表执行ALTER TABLE sys_user CONVERT TO CHARACTER SET utf8mb4可以补救但之前已经写入的乱码数据不会自动恢复。排查顺序建议先查数据库表字符集再查 JDBC URL最后查 Filter因为数据库层的问题最隐蔽。5.2 error 2002 (HY000)mysql.sock 找不到现象写好的代码本地能连数据库部署到 Linux 服务器后启动 Tomcat报error 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock。原因这个报错说明 Java 通过 JDBC 连接localhost:3306时客户端选择走 Unix socket 文件而不是 TCP 端口。Linux 上的 MySQL socket 路径可能不在/tmp/mysql.sock或者 MySQL 服务根本没启动。如果服务没启动连/tmp/mysql.sock这个文件都不存在。解决先确认 MySQL 服务状态再决定连接方式。命令行执行systemctl status mysqld systemctl start mysqld mysql -h127.0.0.1 -P3306 -uroot -p关键点在最后一条命令用127.0.0.1而不是localhost。Java JDBC URL 里同样把jdbc:mysql://localhost:3306改成jdbc:mysql://127.0.0.1:3306这会强制客户端走 TCP 协议绕开 socket 文件路径问题。数据库密码特别长或者包含特殊字符的建议在 properties 文件里用环境变量占位防止特殊字符被解析错位。5.3 war 包发布后静态资源 404现象IDEA 里开发模式下页面正常执行mvn clean package打出 war 包扔到 Tomcat 的webapps目录启动后页面能打开但 CSS、JS 全部失效点“报名”按钮 404。原因最常见的是两个。第一Bootstrap 的href写成了绝对路径/static/bootstrap.min.css在 IDEA 里开发时项目上下文是根路径部署后应用上下文变成了activity-system资源实际地址是/activity-system/static/bootstrap.min.css于是 404第二IDEA 打 war 时没有把依赖的 jar 包放进WEB-INF/lib编译期 Servlet、Druid 类不缺失但运行期找不到。解决JSP 里所有路径都用${pageContext.request.contextPath}拼前缀link href${pageContext.request.contextPath}/static/bootstrap/css/bootstrap.min.css relstylesheet在 IDEA 传统项目中检查 Project Structure → Artifacts → Output Layout确认右边列表里有lib目录且包含 druid、mysql-connector 等 jar。如果没有右键项目依赖选择 Put into Output Root。用 Maven 的话确认 pom.xml 里没有把druid或mysql-connector的 scope 写成provided这会把运行期依赖排除在 war 包外。5.4 Servlet 生命周期异常init 报错、destroy 不执行现象Tomcat 启动后访问登录页直接 500日志里能看到Servlet.init() for servlet LoginServlet threw exception或者你明明在 LoginServlet 的destroy()里写了日志重启 Tomcat 后日志没出现。原因第一init()里做了重活比如读取数据库配置、测试连接池一旦配置有误Servlet 初始化失败整个应用不可用。第二Tomcat 类加载机制下Servlet 默认在第一次请求时初始化不是 Tomcat 启动时就初始化。第三destroy()只有在 Tomcat 正常关闭时才会被调用你直接 kill 进程或者 IDE 里强制停止容器根本不给你执行销毁方法的机会。Servlet 生命周期是单实例多线程一个 Servlet 只有一个实例service()方法被多个线程并发调用init()和destroy()各执行一次。解决把init()里的逻辑降到最轻。需要预加载的数据放到静态代码块或者单独的初始化 Servlet 里init()只做log(init)级别的轻量操作真正的连接池初始化交给JdbcUtils的静态块。验证destroy()时用catalina.sh stop正常关闭 Tomcat不要用 kill -9。最容易被面试官追问的一点是Servlet 里不要写可变的实例字段来存业务状态因为多线程会同时读写这个字段产生数据竞争。5.5 JSP 页面空白但控制台没报错现象登录成功跳转活动列表页面渲染出来是空白的表格一行都没有Tomcat 控制台没有任何异常F12 看响应没有 500。原因第一JSTL 标签库里urihttp://java.sun.com/jsp/jstl/core在较新容器上解析失败页面被渲染成空白但日志不显眼第二EL 表达式被当成普通文本输出页面上出现${act.title}而不是活动标题第三Servlet 里 setAttribute 用的 key 和 JSP 里 EL 写的 key 不一致取值结果全是 null。解决按顺序排查。先在 JSP 顶部写一行测试输出${pageScope}看有没有数据没有数据就检查 Servlet 是否已经跳转跳转用的forward和sendRedirect对 request 属性的存活期影响不同——sendRedirect之后的 request 属性全丢必须用forward。EL 不解析的问题看 web.xml 头部web-app版本如果版本低于 2.5需要在 JSP 里显式写% page isELIgnoredfalse %开启 EL。JSTL 依赖缺失的在 pom.xml 里把jstl 1.2的依赖补上清理本地 Maven 仓库后重新构建。6. 把活动管理系统收尾验证JSP 减重、状态流转检查和快速自测6.1 用 JSTL 把 JSP 里的 Java 脚本片段清出去活动列表页逻辑一多新手会忍不住写这种代码% if (activity.getStatus() 1) { % span classbadge bg-success报名中/span % } %能跑但页面越来越乱Java 代码和 HTML 混在一起改一个按钮颜色都可能碰到缩进错乱。JSTL 的c:choose可以直接替代这种分支c:choose c:when test${act.status 1} span classbadge bg-success报名中/span /c:when c:when test${act.status 2} span classbadge bg-primary进行中/span /c:when c:otherwise span classbadge bg-secondary已结束/span /c:otherwise /c:choose如果状态文字在多个页面都要用更好的做法是在实体类里加一个getStatusText()方法把状态转文字的职责从 JSP 挪到 Java。这是把 JSP 当模板用而不是当页面逻辑用的关键一步。6.2 状态校验写在 Service 而不是页面里活动系统里最常见的越权操作是“活动不是报名中也能报名”“活动已结束还能编辑”。页面上的按钮禁用只能挡正常人直接构造 HTTP 请求就能绕过。状态校验必须放在 Service 里用条件 UPDATE 来实现比先查询再判断更可靠UPDATE activity SET status 1 WHERE id ? AND status 0执行结果受影响行数为 1 才说明状态流转合法只有草稿能变成报名中只有报名中的活动能变成进行中。代码里收到 0 就直接抛出业务异常前端再显示“操作失败当前活动状态不允许此操作”。这套思路和前面报名扣名额的条件更新是同一个套路核心是让数据库来判断条件是否满足而不是在 Java 里先 SELECT 再 UPDATE——两步之间有并发窗口。6.3 一套十分钟手工回归路径每次改动上线前我习惯走一遍这条路径启动 MySQL → 启动 Tomcat → 用 zhangsan 账号登录 → 创建一个活动草稿 → 切换管理员账号发布活动 → 用 zhangsan 报名 → 用管理员查看报名记录 → 再尝试用 zhangsan 重复报名同一次活动。验证点有三个重复报名必须被唯一索引拦下报名后remaining_quota减一管理员把活动状态改成已结束后报名按钮消失但历史报名记录还在。配合数据库命令行查报名表SELECT u.real_name, a.title FROM activity_signup s JOIN sys_user u ON s.user_id u.id JOIN activity a ON s.activity_id a.id;这套组合项目翻车率最高的不是代码逻辑而是环境MySQL 服务没起、Tomcat 端口被占、JDK 版本不对。我早期做课设时把 SQL 拼接成字符串、事务忘记提交、连接用完不关答辩被问到“并发报名怎么保证数据一致”时直接卡壳。后来养成三个习惯先设计表再写代码每个写操作先想清楚事务边界上线前留十分钟手工回归。这十分钟能救你一次希望帮到你。本文还有配套的精品资源点击获取
返回列表