简介这是一份基于JavaWeb的活动管理系统完整项目采用JSPServletBootstrapMySQL技术栈分为前后台覆盖管理员与普通用户两类角色。管理员端包含登录、个人信息维护、活动管理、活动类型管理、报名管理、游客管理等功能用户端支持按分类查看活动、注册登录、查看活动详情、提交报名与确认报名。项目环境明确适配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组合为什么到现在还是课程设计与面试的热门每到毕业设计、课程设计季我身边总有人拿着《活动管理系统javaJSPbootstrapservletMysql》这类题目来问这技术栈是不是过时了我的回答通常是——这个组合恰好是最适合从零到一跑通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文件比如版本61Tomcat 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.javapackage 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;charsetUTF-8); resp.getWriter().write(h2activity system is running/h2); } }然后在web.xml里注册这个Servlet的映射?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 display-nameActivityManagement/display-name servlet servlet-namehelloServlet/servlet-name servlet-classcom.example.activity.controller.HelloServlet/servlet-class /servlet servlet-mapping servlet-namehelloServlet/servlet-name url-pattern/hello/url-pattern /servlet-mapping welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list /web-app这里解释一下两段配置的逻辑Servlet的生命周期由Tomcat容器管理。servlet标签声明“有这个类”servlet-mapping声明“URL路径和这个类的关系”。浏览器访问http://localhost:8080/项目名/helloTomcat按映射找到类并执行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 port8080没有被占用MySQL服务已启动Windows下可以用net start mysqlLinux/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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数与设计逻辑说明外键约束在课程设计里建议保留它能让数据完整性由数据库兜底避免代码里写一堆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.Driver8.0必须改成com.mysql.cj.jdbc.Driver并且建议附上useSSLfalse和serverTimezoneAsia/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 ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/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 11 --整个表的记录都被查出来。这就是SQL注入。正确做法是使用PreparedStatementpackage 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 ListActivity findByPage(int pageNo, int pageSize, String keyword) { ListActivity 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承担业务逻辑。放在活动管理系统中登录流程的拆法是这样浏览器提交表单到LoginServletServlet调用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; } }再写LoginServletpackage 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里 ListActivity 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传入?pageNoabc时未捕获的NumberFormatException会导致500错误第二totalPages计算要用Math.ceil直接totalCount / pageSize整数相除会小数全丢。业务上不妨再加个上限控制比如页数超过100就强制回第1页避免恶意请求消耗数据库资源。JSP页面这里只展示核心部分% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title活动列表/title link relstylesheet href${pageContext.request.contextPath}/css/bootstrap.min.css /head body classcontainer mt-3 h2活动列表/h2 table classtable table-bordered table-hover thead classthead-dark tr th编号/th th标题/th th地点/th th状态/th th操作/th /tr /thead tbody c:forEach items${list} varactivity tr td${activity.id}/td td${activity.title}/td td${activity.location}/td td c:choose c:when test${activity.status 1} span classbadge badge-success报名中/span /c:when c:when test${activity.status 2} span classbadge badge-secondary已结束/span /c:when c:otherwise span classbadge badge-danger已取消/span /c:otherwise /c:choose /td td a href${pageContext.request.contextPath}/activity/signup?id${activity.id} classbtn btn-sm btn-primary报名/a /td /tr /c:forEach /tbody /table c:if test${totalPages 1} nav ul classpagination c:forEach begin1 end${totalPages} vari li classpage-item ${i currentPage ? active : } a classpage-link href${pageContext.request.contextPath}/activity/list?pageNo${i}keyword${keyword} ${i} /a /li /c:forEach /ul /nav /c:if /body /htmlJSP页面最关键的是所有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?msgsignup_ok); } catch (SQLIntegrityConstraintViolationException e) { // 唯一索引冲突说明已经报名过 resp.sendRedirect(req.getContextPath() /activity/list?msgdup_signup); } catch (Exception e) { resp.sendRedirect(req.getContextPath() /activity/list?msgerror); } } }这段代码的价值在于把“用户已报名”和“系统异常”区分开。如果没有数据库的唯一索引这个分支就要先查一次signup表再插入两次操作之间存在时间窗口并发时照样重复报名。现在直接把异常类型映射成用户提示少一次查询还更稳。不过要注意SQLIntegrityConstraintViolationException在不同MySQL驱动版本下的包路径基本一致但如果你用Spring的JdbcTemplate异常会被转成DuplicateKeyException那是另一个分支了。5. 避坑JSPServletMySQL项目里必然遇到的环境与代码问题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/classesIDEA里表现为classes目录下找不到class文件。解决先访问http://localhost:8080/项目名/确认项目根路径能打开再逐级检查URL。如果根路径都404说明application context和实际部署名不一致。对于“类没有编译进去”的情况右键项目Rebuild然后去Tomcat部署目录看WEB-INF/classes下是否生成了对应的class文件没有就检查IDEA的Artifact配置把项目编译输出目录加到部署包里。5.2 中文乱码三个地方的编码必须一致现象页面显示正常但存入MySQL后中文变成问号或乱码或者JSP页面本身中文乱码。原因这是JavaWeb最经典的翻车点链路中任何一环编码不一致都会出问题。常见有三处JSP文件头没指定pageEncodingUTF-8Servlet读取请求参数时没有调用req.setCharacterEncoding(UTF-8)MySQL表的字符集不是utf8mb4。还有一个隐藏点IDEA里文件本身是GBK编码保存的话在JSP头声明UTF-8也没用。解决统一链路。JSP第一行写% page contentTypetext/html;charsetUTF-8 languagejava pageEncodingUTF-8 %Servlet的doPost第一行加req.setCharacterEncoding(UTF-8)建表时指定DEFAULT CHARSETutf8mb4。最靠谱的做法是加一个过滤器对全站请求统一设置编码避免每个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.DriverURL上追加?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai。如果项目里同时有多个版本的驱动jarclasspath里的顺序会决定加载哪一个建议只保留一份避免玄学问题。5.4 表单提交后样式全部丢失现象登录页正常点击登录跳转后页面HTML还在但CSS完全没加载所有按钮和表格都变成纯文本。原因前面JSP那段提到过页面里所有资源路径都必须带上下文路径。如果你在活动列表页写的是href/css/bootstrap.min.css部署上下文是/activity浏览器会去http://localhost:8080/css/bootstrap.min.css找文件自然404。解决统一使用${pageContext.request.contextPath}拼接资源路径例如link relstylesheet 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-timeout30/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时你会明白很多“约定大于配置”背后到底解决的是什么问题。希望帮到你如果项目跑不通优先怀疑版本然后再怀疑自己的代码——这是我踩过最多次的坑。本文还有配套的精品资源点击获取