简介:这是一份基于JavaWeb的活动管理系统完整项目,采用JSP+Servlet+Bootstrap+MySQL技术栈,分为前后台,覆盖管理员与普通用户两类角色。管理员端包含登录、个人信息维护、活动管理、活动类型管理、报名管理、游客管理等功能;用户端支持按分类查看活动、注册登录、查看活动详情、提交报名与确认报名。项目环境明确,适配JDK1.8、Tomcat7/8/9及MySQL5.7,且非Maven项目,导入IDE即可运行,适合JavaWeb学习者、课程设计或毕业设计参考。资源共214个文件,包含55个Java类和55个class编译文件、27个JSP页面,以及jar依赖、XML配置、JS/CSS、图片、SQL脚本等,压缩包大小10.56MB,目录结构清晰,便于阅读部署。内容预览中可见Goods/Order/User等核心实体与对应Dao、Service类,覆盖数据访问与业务逻辑分层。目前已有564人学习下载,对想要快速启动同类管理系统开发的读者有直接参考价值。
1. 活动管理系统这一套JavaWeb组合,为什么到现在还是课程设计与面试的热门
每到毕业设计、课程设计季,我身边总有人拿着《活动管理系统(java+JSP+bootstrap+servlet+Mysql)》这类题目来问:这技术栈是不是过时了?我的回答通常是——这个组合恰好是最适合从零到一跑通JavaWeb的入门方案,没有之一。活动管理系统本身业务边界清晰,无非是活动发布、报名、审核、统计;JSP做页面、Servlet做控制、Bootstrap管样式、MySQL存数据,四者分工明确。把这样一个项目完整写一遍,请求-响应链路、Session状态管理、JDBC数据访问、表单验证这些基础能力就全摸过一遍了,而它们恰恰是Java后端面试八股文里反复出现的底层题源。
它适合两类人:一类是做课程设计、毕设但还不想上Spring全家桶堆砌的学生,另一类是准备面试,想补上Servlet与JSP底层功底的初级开发。本文从环境搭建讲起,按“建表 → 写DAO → 写Servlet → 写JSP页面”的路线把可运行的代码一路落下来,最后把版本兼容、中文乱码、路径丢失这些翻车点一个个拆开。看完你拿到的不是一张架构图,而是一套能照着敲出来的方案。
2. 环境与工程结构:JDK、Tomcat、MySQL之间的版本搭配是第一个坑
2.1 版本选不对,项目永远跑不起来
很多人学JavaWeb翻车,第一原因不是代码,是版本。JDK、Tomcat、Servlet API、MySQL驱动这四者的版本必须相互兼容,尤其不能拿着新版本的Tomcat去跑旧教材里的代码。常见做法是:JDK 8 + Tomcat 8.5 + Servlet 3.1 + JSP 2.3 + MySQL 5.7/8.0。这套组合最稳,网上能找到的javaweb头歌实训答案、课程设计案例源码也基本基于这个版本范围。
注意一点,Tomcat 10之后包名从javax.servlet改成了jakarta.servlet,如果你用的是Tomcat 10,那么网上绝大多数代码导入javax.servlet.http.HttpServlet时会直接报编译错误。我建议课程设计阶段老老实实用Tomcat 8.5,避开这个毫无价值的坑。JDK也不要追新,用JDK 8或JDK 11即可,JDK 17+编译出来的class文件(比如版本61),Tomcat 8.5自带的ecj解析时可能会不认。MySQL用5.7或8.0都行,但8.0需要额外注意驱动类名和时区参数,这一点在第五章专门讲。
2.2 不用Maven也能建工程:手工搭建目录与第一个Servlet
对于活动管理系统这种规模的项目,不用Maven反而更能看清class文件去哪了、lib目录放什么。我用IDEA给你演示一个最朴素的建法:新建一个普通Java项目,添加Web支持后,项目结构会变成src、web(或WebContent)。需要在web/WEB-INF/lib下放入mysql-connector-java.jar,在Project Structure里把该jar加入依赖。
工程目录大致长这样:
src/com/example/activity/ controller/ (Servlet类) dao/ (数据访问) entity/ (活动、用户实体) util/ (JDBC连接工具) web/ WEB-INF/ web.xml index.jsp activityList.jsp login.jsp第一个Servlet先别写业务,跑通一个HelloWorld再说。新建HelloServlet.java:
package com.example.activity.controller; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); resp.getWriter().write("<h2>activity system is running</h2>"); } }然后在web.xml里注册这个Servlet的映射:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <display-name>ActivityManagement</display-name> <servlet> <servlet-name>helloServlet</servlet-name> <servlet-class>com.example.activity.controller.HelloServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>helloServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>这里解释一下两段配置的逻辑:Servlet的生命周期由Tomcat容器管理。<servlet>标签声明“有这个类”,<servlet-mapping>声明“URL路径和这个类的关系”。浏览器访问http://localhost:8080/项目名/hello,Tomcat按映射找到类并执行doGet。至少2个参数要知道:<url-pattern>必须以/开头;<welcome-file>决定了访问根路径时默认显示哪个文件。Servlet 3.1支持用@WebServlet("/hello")注解替代web.xml配置,不过课程设计交代码时保留web.xml反而更直观,也方便考官看路由。
2.3 把服务跑起来:IDEA里配置Tomcat与验证步骤
配置Tomcat时有一个极容易被忽略的字段——Application context(应用上下文)。它默认是/项目名。如果你在这里填的是/activity,访问地址就是http://localhost:8080/activity/hello,少写或多写都会404。我建议统一设置为/,这样读代码的人访问根路径即可,不用猜项目名。
运行前的最后检查项:
- Tomcat的
conf/server.xml里<Connector port="8080"没有被占用; - MySQL服务已启动,Windows下可以用
net start mysql,Linux/Mac可以用systemctl status mysqld看状态; - mysql驱动jar放在了
WEB-INF/lib,而不是只加到了IDEA的依赖里。从catalina日志里看到ClassNotFoundException: com.mysql.jdbc.Driver,八成就是jar只进编译路径没进运行目录。
一切就绪后启动Tomcat,浏览器访问HelloServlet页面出现activity system is running,说明Java环境、Web容器、Servlet生命周期这三层已经打通。后面的所有代码,都是在这条链路上加业务。
3. 活动管理系统的数据库设计:先立表,再写代码
3.1 核心表结构:用户、活动、报名记录三张表
活动管理系统的业务模型不复杂,但至少需要三张表才能支撑“发布活动-用户报名-管理员审核”这条主线。第一张表是用户表,管理登录和角色;第二张是活动表,保存活动标题、时间、地点、人数上限;第三张是报名表,关联用户和活动,记录报名状态。设计表时不要把所有字段都塞进一张表,报名作为多对多关系统一拆出去,后面做统计会省很多事。
下面是建议的建表SQL,直接可以在MySQL命令行或Navicat里执行:
CREATE DATABASE IF NOT EXISTS activity_system DEFAULT CHARACTER SET utf8mb4; USE activity_system; CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL COMMENT '登录名', `password` VARCHAR(100) NOT NULL COMMENT '密码,建议密文存储', `real_name` VARCHAR(50) DEFAULT NULL, `role` TINYINT NOT NULL DEFAULT 0 COMMENT '0=普通用户 1=管理员', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `activity` ( `id` INT NOT NULL AUTO_INCREMENT, `title` VARCHAR(200) NOT NULL COMMENT '活动标题', `location` VARCHAR(200) DEFAULT NULL, `start_time` DATETIME NOT NULL, `end_time` DATETIME DEFAULT NULL, `capacity` INT DEFAULT 0 COMMENT '人数上限,0表示不限', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1=报名中 2=已结束 3=已取消', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `signup` ( `id` INT NOT NULL AUTO_INCREMENT, `activity_id` INT NOT NULL, `user_id` INT NOT NULL, `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0=待审核 1=通过 2=拒绝', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_activity_user` (`activity_id`, `user_id`), CONSTRAINT `fk_signup_activity` FOREIGN KEY (`activity_id`) REFERENCES `activity`(`id`), CONSTRAINT `fk_signup_user` FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;参数与设计逻辑说明:外键约束在课程设计里建议保留,它能让数据完整性由数据库兜底,避免代码里写一堆if判断;uk_activity_user这个唯一索引保证了同一个用户不会对同一活动重复报名,业务上比Java代码判重更可靠。role字段用TINYINT而不是用字符串“admin/user”,一是省空间,二是比较时不易出错。capacity用0表示不限人数,如果按照上文SQL建表,后续查询“还能报吗”的逻辑就是count(signup) < capacity OR capacity = 0。
3.2 写一个JDBC工具类:连接驱动的参数可以抄作业
MySQL 8.0和5.7在连接URL上有一个明显差异。5.7用com.mysql.jdbc.Driver,8.0必须改成com.mysql.cj.jdbc.Driver,并且建议附上useSSL=false和serverTimezone=Asia/Shanghai参数,否则会报时区错误。下列工具类是我项目里直接沿用下来的写法:
package com.example.activity.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/activity_system" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL驱动加载失败", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, java.sql.Statement stmt, java.sql.ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException ignored) {} try { if (stmt != null) stmt.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } }Class.forName在MySQL 8.0驱动(Connector/J 8.x)下可以省略,因为SPI机制会自动注册驱动,但保留这行代码能兼容老驱动,也不会出错。close方法里用了三个单独的try-catch,目的是关闭ResultSet、Statement、Connection时互不影响——比如rs关闭抛出异常,stmt和conn仍然会被执行到。这是数据访问层一个很容易忽略的细节,很多人只关了Connection,导致连接池(如果有的话)很快耗尽。
3.3 DAO层用PreparedStatement:防SQL注入顺便把代码写干净
表建好了,连接也有了,接下来是数据访问对象(DAO)。活动列表的查询可以暴露一个最常见的坑:字符串拼接SQL。初学者容易写成"SELECT * FROM activity WHERE title LIKE '%" + keyword + "%'"。一旦用户在搜索框输入%' OR 1=1 --,整个表的记录都被查出来。这就是SQL注入。正确做法是使用PreparedStatement:
package com.example.activity.dao; import com.example.activity.entity.Activity; import com.example.activity.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class ActivityDao { public List<Activity> findByPage(int pageNo, int pageSize, String keyword) { List<Activity> list = new ArrayList<>(); String sql = "SELECT id, title, location, start_time, end_time, capacity, status " + "FROM activity WHERE title LIKE ? ORDER BY start_time DESC LIMIT ?, ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setInt(2, (pageNo - 1) * pageSize); ps.setInt(3, pageSize); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Activity a = new Activity(); a.setId(rs.getInt("id")); a.setTitle(rs.getString("title")); a.setStatus(rs.getInt("status")); list.add(a); } } } catch (SQLException e) { e.printStackTrace(); } return list; } }参数说明:LIMIT ?, ?第一个参数是偏移量,第二个是每页条数。常见错误是把pageNo直接传进去,导致第一页正确,第二页从第2条开始查,永远漏数据。(pageNo - 1) * pageSize这个换算不要省。Java 7之后try-with-resources写法会自动关闭PreparedStatement和Connection,所以代码里不再需要手动调用DBUtil.close方法。懒人可以直接复制这段,但记得把e.printStackTrace()替换成日志输出,每次请求都打印堆栈会对Tomcat产生不小的IO压力。
4. 核心流程串起来:登录、活动列表、报名与状态流转
4.1 用Model2思想拆分控制器与页面逻辑
热词里提到的“JSP Model2思想”,本质就是让JSP只做展示、Servlet只做控制、JavaBean承担业务逻辑。放在活动管理系统中,登录流程的拆法是这样:浏览器提交表单到LoginServlet;Servlet调用UserDao查询用户,校验通过后把用户信息放进Session,再重定向到活动列表页面;JSP页面只负责渲染。
先建用户DAO查询方法:
package com.example.activity.dao; import com.example.activity.entity.User; import com.example.activity.util.DBUtil; import java.sql.*; public class UserDao { public User findByUsernameAndPassword(String username, String password) { String sql = "SELECT id, username, password, real_name, role FROM user " + "WHERE username = ? AND password = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User u = new User(); u.setId(rs.getInt("id")); u.setUsername(rs.getString("username")); u.setPassword(rs.getString("password")); u.setRealName(rs.getString("real_name")); u.setRole(rs.getInt("role")); return u; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }再写LoginServlet:
package com.example.activity.controller; import com.example.activity.dao.UserDao; import com.example.activity.entity.User; import javax.servlet.ServletException; import javax.servlet.http.*; import java.io.IOException; public class LoginServlet extends HttpServlet { private UserDao userDao = new UserDao(); @Override 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 = userDao.findByUsernameAndPassword(username, password); if (user != null) { HttpSession session = req.getSession(); session.setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/activity/list"); } else { req.setAttribute("errorMsg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }这段代码的注意点:req.setCharacterEncoding("UTF-8")必须放在读取参数之前,否则表单里的中文用户名到后端就变成乱码。登录成功用sendRedirect,失败用forward。两者区别在于重定向是让浏览器再发一次请求,地址栏会变,能防止刷新页面重复提交;转发在服务器内部跳转,地址栏不变,适合携带错误提示。session默认有效期是30分钟,在Tomcat的web.xml里配置,也可以在项目的web.xml里覆盖,后面第六章会提到。
4.2 活动列表分页:Servlet计算参数,JSP输出翻页链接
分页查询在上一章已经给出了DAO层写法,这里补上Servlet层的参数处理和JSP层的翻页链接生成。我的思路很简单,ActivityListServlet接收pageNo和keyword两个请求参数,传给DAO,再把总记录数、总页数一起塞进request方便JSP读取。
package com.example.activity.controller; import com.example.activity.dao.ActivityDao; import com.example.activity.entity.Activity; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; public class ActivityListServlet extends HttpServlet { private ActivityDao activityDao = new ActivityDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int pageNo = 1; int pageSize = 10; String pageNoStr = req.getParameter("pageNo"); String keyword = req.getParameter("keyword"); if (keyword == null) keyword = ""; try { pageNo = Integer.parseInt(pageNoStr); } catch (NumberFormatException ignored) { // 参数非法时保留默认值1,不把异常抛给用户 } // 建议把pageSize放到常量配置中,而不是散落在各Servlet里 List<Activity> list = activityDao.findByPage(pageNo, pageSize, keyword); int totalCount = activityDao.countByKeyword(keyword); int totalPages = (int) Math.ceil(totalCount * 1.0 / pageSize); req.setAttribute("list", list); req.setAttribute("totalPages", totalPages); req.setAttribute("currentPage", pageNo); req.setAttribute("keyword", keyword); req.getRequestDispatcher("/activityList.jsp").forward(req, resp); } }注意两点。第一,pageNo参数解析必须用try-catch包住,用户手动改URL传入?pageNo=abc时,未捕获的NumberFormatException会导致500错误;第二,totalPages计算要用Math.ceil,直接totalCount / pageSize整数相除会小数全丢。业务上不妨再加个上限控制,比如页数超过100就强制回第1页,避免恶意请求消耗数据库资源。
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> <link rel="stylesheet" href="${pageContext.request.contextPath}/css/bootstrap.min.css"> </head> <body class="container mt-3"> <h2>活动列表</h2> <table class="table table-bordered table-hover"> <thead class="thead-dark"> <tr> <th>编号</th> <th>标题</th> <th>地点</th> <th>状态</th> <th>操作</th> </tr> </thead> <tbody> <c:forEach items="${list}" var="activity"> <tr> <td>${activity.id}</td> <td>${activity.title}</td> <td>${activity.location}</td> <td> <c:choose> <c:when test="${activity.status == 1}"> <span class="badge badge-success">报名中</span> </c:when> <c:when test="${activity.status == 2}"> <span class="badge badge-secondary">已结束</span> </c:when> <c:otherwise> <span class="badge badge-danger">已取消</span> </c:otherwise> </c:choose> </td> <td> <a href="${pageContext.request.contextPath}/activity/signup?id=${activity.id}" class="btn btn-sm btn-primary">报名</a> </td> </tr> </c:forEach> </tbody> </table> <c:if test="${totalPages > 1}"> <nav> <ul class="pagination"> <c:forEach begin="1" end="${totalPages}" var="i"> <li class="page-item ${i == currentPage ? 'active' : ''}"> <a class="page-link" href="${pageContext.request.contextPath}/activity/list?pageNo=${i}&keyword=${keyword}"> ${i} </a> </li> </c:forEach> </ul> </nav> </c:if> </body> </html>JSP页面最关键的是所有URL前都拼了${pageContext.request.contextPath}。如果漏掉这一截,部署到/activity上下文后,href写成/css/bootstrap.min.css,浏览器会去根目录找CSS,样式全丢。Bootstrap的CDN是另一条路,但如果验收环境不允许访问外网,本地bootstrap.min.css文件放进web/css/目录是更稳的做法。
4.3 报名状态机:唯一索引兜底,代码给用户反馈
报名逻辑不需要单独一个表去存状态历史,signup表的status字段就够了。Servlet里执行插入时,DuplicateKeyException和业务规则冲突的识别比较关键:
package com.example.activity.controller; import com.example.activity.dao.SignupDao; import com.example.activity.entity.User; import javax.servlet.ServletException; import javax.servlet.http.*; import java.io.IOException; import java.sql.SQLIntegrityConstraintViolationException; public class SignupServlet extends HttpServlet { private SignupDao signupDao = new SignupDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); User user = (User) session.getAttribute("loginUser"); if (user == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } int activityId = Integer.parseInt(req.getParameter("id")); try { signupDao.insert(activityId, user.getId()); resp.sendRedirect(req.getContextPath() + "/activity/list?msg=signup_ok"); } catch (SQLIntegrityConstraintViolationException e) { // 唯一索引冲突,说明已经报名过 resp.sendRedirect(req.getContextPath() + "/activity/list?msg=dup_signup"); } catch (Exception e) { resp.sendRedirect(req.getContextPath() + "/activity/list?msg=error"); } } }这段代码的价值在于把“用户已报名”和“系统异常”区分开。如果没有数据库的唯一索引,这个分支就要先查一次signup表再插入,两次操作之间存在时间窗口,并发时照样重复报名。现在直接把异常类型映射成用户提示,少一次查询还更稳。不过要注意SQLIntegrityConstraintViolationException在不同MySQL驱动版本下的包路径基本一致,但如果你用Spring的JdbcTemplate,异常会被转成DuplicateKeyException,那是另一个分支了。
5. 避坑:JSP+Servlet+MySQL项目里必然遇到的环境与代码问题
5.1 404错误:先查web.xml映射,再查application context
现象:启动Tomcat后访问Servlet地址,页面报404,控制台没有任何异常。
原因:这个错误一多半出在web.xml的<url-pattern>上。比如配置的是/hello,访问时却把http://localhost:8080/项目名/hello写成了http://localhost:8080/hello,少了一层上下文。另一小半出在Servlet类没有编译进WEB-INF/classes,IDEA里表现为classes目录下找不到class文件。
解决:先访问http://localhost:8080/项目名/确认项目根路径能打开,再逐级检查URL。如果根路径都404,说明application context和实际部署名不一致。对于“类没有编译进去”的情况,右键项目Rebuild,然后去Tomcat部署目录看WEB-INF/classes下是否生成了对应的class文件,没有就检查IDEA的Artifact配置,把项目编译输出目录加到部署包里。
5.2 中文乱码:三个地方的编码必须一致
现象:页面显示正常,但存入MySQL后中文变成问号或乱码;或者JSP页面本身中文乱码。
原因:这是JavaWeb最经典的翻车点,链路中任何一环编码不一致都会出问题。常见有三处:JSP文件头没指定pageEncoding="UTF-8";Servlet读取请求参数时没有调用req.setCharacterEncoding("UTF-8");MySQL表的字符集不是utf8mb4。还有一个隐藏点,IDEA里文件本身是GBK编码保存的话,在JSP头声明UTF-8也没用。
解决:统一链路。JSP第一行写<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>;Servlet的doPost第一行加req.setCharacterEncoding("UTF-8");建表时指定DEFAULT CHARSET=utf8mb4。最靠谱的做法是加一个过滤器,对全站请求统一设置编码,避免每个Servlet都写一遍:
package com.example.activity.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; @WebFilter("/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); chain.doFilter(request, response); } }这个过滤器生效后,所有Servlet里那行req.setCharacterEncoding都可以删掉,减少重复代码。注意filter里设置的是ServletRequest和ServletResponse,不需要强转成Http类型,因为这两个方法在HttpServletRequest里是被继承的。
5.3 MySQL 8.0的驱动和时区:ClassNotFoundException与CLIENT_PLUGIN_AUTH
现象:启动项目点击功能时报ClassNotFoundException: com.mysql.jdbc.Driver,或者提示The server time zone value 'Öйú±ê׼ʱ¼ä'。
原因:第一,用的是MySQL 8.0驱动(mysql-connector-java 8.x),驱动类名变成了com.mysql.cj.jdbc.Driver,老驱动包里的类名在新版本里被移除了。第二,MySQL 8.0对时区更严格,连接URL不带serverTimezone就直接报错。
解决:pom依赖或者手动导入jar时确认是8.0.x版本,驱动类名写成com.mysql.cj.jdbc.Driver;URL上追加?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai。如果项目里同时有多个版本的驱动jar,classpath里的顺序会决定加载哪一个,建议只保留一份,避免玄学问题。
5.4 表单提交后样式全部丢失
现象:登录页正常,点击登录跳转后页面HTML还在,但CSS完全没加载,所有按钮和表格都变成纯文本。
原因:前面JSP那段提到过,页面里所有资源路径都必须带上下文路径。如果你在活动列表页写的是href="/css/bootstrap.min.css",部署上下文是/activity,浏览器会去http://localhost:8080/css/bootstrap.min.css找文件,自然404。
解决:统一使用${pageContext.request.contextPath}拼接资源路径,例如:
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/bootstrap.min.css"> <script src="${pageContext.request.contextPath}/js/bootstrap.min.js"></script>也可以借助<base>标签设置基础路径,但每页都写base标签也麻烦。最干净的做法是抽一个common.jsp统一引入公共资源,具体页面include它。记住这一点,凡是form的action、a标签的href、重定向的URL,一律用contextPath拼,样式丢失这类问题就根除了。
6. 进阶:从能演示到能提交,这些细节决定答辩评价
项目能跑通只是及格线,答辩时老师和面试官更看重边界处理。我最后给你三个能快速加上、性价比极高的改进点。
第一个是密码加密。现在的UserDao里直接把用户输入的明文和库里的password比对,这在实际项目中是绝对禁忌。至少要用MessageDigest做一层SHA-256加盐哈希:
public static String sha256(String raw) throws NoSuchAlgorithmException { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hash = digest.digest(raw.getBytes(StandardCharsets.UTF_8)); StringBuilder hex = new StringBuilder(); for (byte b : hash) { hex.append(String.format("%02x", b)); } return hex.toString(); }第二个是Session超时与登录拦截。在web.xml里加一行:
<session-config> <session-timeout>30</session-timeout> </session-config>然后写一个登录检查过滤器,判断session.getAttribute("loginUser") == null时直接重定向到login.jsp。面试官问“怎么防止未登录用户直接访问详情页”,这就是答案。
第三个是日志输出。我直到做第二个项目才意识到,e.printStackTrace()打印到控制台在开发环境没问题,部署到服务器上就等于把堆栈丢了。我自己的习惯是至少引入java.util.logging或log4j一个,把兜底异常用logger.error("signup failed, activityId=" + activityId, e)记录等级分开的错误日志。答辩时如果被问到“你的项目有没有考虑线上问题排查”,这一个改进就足够说明你懂生产。
这三个点都做完,这个活动管理系统才真正从“课程设计”变成了“可交付的项目”。做JavaWeb这些年,我最大的体会是:以Servlet和JSP为底子走一遍完整流程,之后用Spring MVC或Spring Boot时,你会明白很多“约定大于配置”背后到底解决的是什么问题。希望帮到你,如果项目跑不通,优先怀疑版本,然后再怀疑自己的代码——这是我踩过最多次的坑。
本文还有配套的精品资源,点击获取